from __future__ import annotations

from textual import events
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.content import Content, ContentText
from textual.css.query import NoMatches
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Static

__all__ = ["Collapsible", "CollapsibleTitle"]


class CollapsibleTitle(Static, can_focus=True):
    """Title and symbol for the Collapsible."""

    BINDING_GROUP_TITLE = "Collapsible"

    ALLOW_SELECT = False
    DEFAULT_CSS = """
    CollapsibleTitle {
        width: auto;
        height: auto;        
        padding: 0 1;        
        text-style: $block-cursor-blurred-text-style;
        color: $block-cursor-blurred-foreground;
        pointer: pointer;

        &:hover {
            background: $block-hover-background;
            color: $foreground;
        }
        &:focus {
            text-style: $block-cursor-text-style;
            background: $block-cursor-background;
            color: $block-cursor-foreground;
        }
    }
    """

    BINDINGS = [
        Binding("enter", "toggle_collapsible", "Toggle collapsible", show=False)
    ]
    """
    | Key(s) | Description |
    | :- | :- |
    | enter | Toggle the collapsible. |
    """

    collapsed = reactive(True)
    label: reactive[ContentText] = reactive(Content("Toggle"))

    def __init__(
        self,
        *,
        label: ContentText,
        collapsed_symbol: str,
        expanded_symbol: str,
        collapsed: bool,
    ) -> None:
        super().__init__()
        self.collapsed_symbol = collapsed_symbol
        self.expanded_symbol = expanded_symbol
        self.label = Content.from_text(label)
        self.collapsed = collapsed

    class Toggle(Message):
        """Request toggle."""

    async def _on_click(self, event: events.Click) -> None:
        """Inform ancestor we want to toggle."""
        event.stop()
        self.post_message(self.Toggle())

    def action_toggle_collapsible(self) -> None:
        """Toggle the state of the parent collapsible."""
        self.post_message(self.Toggle())

    def validate_label(self, label: ContentText) -> Content:
        return Content.from_text(label)

    def _update_label(self) -> None:
        assert isinstance(self.label, Content)
        if self.collapsed:
            self.update(Content.assemble(self.collapsed_symbol, " ", self.label))
        else:
            self.update(Content.assemble(self.expanded_symbol, " ", self.label))

    def _watch_label(self) -> None:
        self._update_label()

    def _watch_collapsed(self, collapsed: bool) -> None:
        self._update_label()


class Collapsible(Widget):
    """A collapsible container."""

    ALLOW_MAXIMIZE = True
    collapsed = reactive(True, init=False)
    title = reactive("Toggle")

    DEFAULT_CSS = """
    Collapsible {
        width: 1fr;
        height: auto;
        background: $surface;
        border-top: hkey $background;
        padding-bottom: 1;
        padding-left: 1;

        &:focus-within {
            background-tint: $foreground 5%;
        }

        &.-collapsed > Contents {
            display: none;   
        }
    }
    """

    class Toggled(Message):
        """Parent class subclassed by `Collapsible` messages.

        Can be handled with `on(Collapsible.Toggled)` if you want to handle expansions
        and collapsed in the same way, or you can handle the specific events individually.
        """

        def __init__(self, collapsible: Collapsible) -> None:
            """Create an instance of the message.

            Args:
                collapsible: The `Collapsible` widget that was toggled.
            """
            self.collapsible: Collapsible = collapsible
            """The collapsible that was toggled."""
            super().__init__()

        @property
        def control(self) -> Collapsible:
            """An alias for [Toggled.collapsible][textual.widgets.Collapsible.Toggled.collapsible]."""
            return self.collapsible

    class Expanded(Toggled):
        """Event sent when the `Collapsible` widget is expanded.

        Can be handled using `on_collapsible_expanded` in a subclass of
        [`Collapsible`][textual.widgets.Collapsible] or in a parent widget in the DOM.
        """

    class Collapsed(Toggled):
        """Event sent when the `Collapsible` widget is collapsed.

        Can be handled using `on_collapsible_collapsed` in a subclass of
        [`Collapsible`][textual.widgets.Collapsible] or in a parent widget in the DOM.
        """

    class Contents(Container):
        DEFAULT_CSS = """
        Contents {
            width: 100%;
            height: auto;
            padding: 1 0 0 3;            
        }
        """

    def __init__(
        self,
        *children: Widget,
        title: str = "Toggle",
        collapsed: bool = True,
        collapsed_symbol: str = "▶",
        expanded_symbol: str = "▼",
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        disabled: bool = False,
    ) -> None:
        """Initialize a Collapsible widget.

        Args:
            *children: Contents that will be collapsed/expanded.
            title: Title of the collapsed/expanded contents.
            collapsed: Default status of the contents.
            collapsed_symbol: Collapsed symbol before the title.
            expanded_symbol: Expanded symbol before the title.
            name: The name of the collapsible.
            id: The ID of the collapsible in the DOM.
            classes: The CSS classes of the collapsible.
            disabled: Whether the collapsible is disabled or not.
        """
        super().__init__(name=name, id=id, classes=classes, disabled=disabled)
        self._title = CollapsibleTitle(
            label=title,
            collapsed_symbol=collapsed_symbol,
            expanded_symbol=expanded_symbol,
            collapsed=collapsed,
        )
        self.title = title
        self._contents_list: list[Widget] = list(children)
        self.collapsed = collapsed

    def _on_collapsible_title_toggle(self, event: CollapsibleTitle.Toggle) -> None:
        event.stop()
        self.collapsed = not self.collapsed

    def _watch_collapsed(self, collapsed: bool) -> None:
        """Update collapsed state when reactive is changed."""
        self._update_collapsed(collapsed)
        if self.collapsed:
            self.post_message(self.Collapsed(self))
        else:
            self.post_message(self.Expanded(self))
        if self.is_mounted:
            self.call_after_refresh(self.scroll_visible)

    def _update_collapsed(self, collapsed: bool) -> None:
        """Update children to match collapsed state."""
        try:
            self._title.collapsed = collapsed
            self.set_class(collapsed, "-collapsed")
        except NoMatches:
            pass

    def _on_mount(self, event: events.Mount) -> None:
        """Initialise collapsed state."""
        self._update_collapsed(self.collapsed)

    def compose(self) -> ComposeResult:
        yield self._title
        with self.Contents():
            yield from self._contents_list

    def compose_add_child(self, widget: Widget) -> None:
        """When using the context manager compose syntax, we want to attach nodes to the contents.

        Args:
            widget: A Widget to add.
        """
        self._contents_list.append(widget)

    def _watch_title(self, title: str) -> None:
        self._title.label = title
