"""Provides the base code and implementations of toggle widgets.

In particular it provides `Checkbox`, `RadioButton` and `RadioSet`.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar

from rich.console import RenderableType

from textual.binding import Binding, BindingType
from textual.content import Content, ContentText
from textual.events import Click
from textual.geometry import Size
from textual.message import Message
from textual.reactive import reactive
from textual.style import Style
from textual.widgets._static import Static

if TYPE_CHECKING:
    from typing_extensions import Self


class ToggleButton(Static, can_focus=True):
    """Base toggle button widget.

    Warning:
        `ToggleButton` should be considered to be an internal class; it
        exists to serve as the common core of [Checkbox][textual.widgets.Checkbox] and
        [RadioButton][textual.widgets.RadioButton].
    """

    ALLOW_SELECT = False
    BINDINGS: ClassVar[list[BindingType]] = [
        Binding("enter,space", "toggle_button", "Toggle", show=False),
    ]
    """
    | Key(s) | Description |
    | :- | :- |
    | enter, space | Toggle the value. |
    """

    COMPONENT_CLASSES: ClassVar[set[str]] = {
        "toggle--button",
        "toggle--label",
    }
    """
    | Class | Description |
    | :- | :- |
    | `toggle--button` | Targets the toggle button itself. |
    | `toggle--label` | Targets the text label of the toggle button. |
    """

    DEFAULT_CSS = """
    ToggleButton {
        width: auto;
        border: tall $border-blurred;
        padding: 0 1;
        background: $surface;
        text-wrap: nowrap;
        text-overflow: ellipsis;

        &.-textual-compact {
            border: none !important;
            padding: 0;
            &:focus {
                border: tall $border;
                background-tint: $foreground 5%;
                & > .toggle--label {
                    color: $block-cursor-foreground;
                    background: $block-cursor-background;
                    text-style: $block-cursor-text-style;
                }
            }
        }

        & > .toggle--button {
            color: $panel-darken-2;
            background: $panel;
        }

        &.-on > .toggle--button {
            color: $text-success;
            background: $panel;
        }

        &:focus {
            border: tall $border;
            background-tint: $foreground 5%;
            & > .toggle--label {
                color: $block-cursor-foreground;                
            }
        }
        &:blur:hover {
            & > .toggle--label {
                background: $block-hover-background;
            }
        }
    }
    """

    BUTTON_LEFT: str = "▐"
    """The character used for the left side of the toggle button."""

    BUTTON_INNER: str = "X"
    """The character used for the inside of the button."""

    BUTTON_RIGHT: str = "▌"
    """The character used for the right side of the toggle button."""

    value: reactive[bool] = reactive(False, init=False)
    """The value of the button. `True` for on, `False` for off."""

    compact: reactive[bool] = reactive(False, toggle_class="-textual-compact")
    """Enable compact display?"""

    def __init__(
        self,
        label: ContentText = "",
        value: bool = False,
        button_first: bool = True,
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
        disabled: bool = False,
        tooltip: RenderableType | None = None,
        compact: bool = False,
    ) -> None:
        """Initialise the toggle.

        Args:
            label: The label for the toggle.
            value: The initial value of the toggle.
            button_first: Should the button come before the label, or after?
            name: The name of the toggle.
            id: The ID of the toggle in the DOM.
            classes: The CSS classes of the toggle.
            disabled: Whether the button is disabled or not.
            tooltip: RenderableType | None = None,
            compact: Show a compact button.
        """
        super().__init__(name=name, id=id, classes=classes, disabled=disabled)
        self._button_first = button_first
        # NOTE: Don't send a Changed message in response to the initial set.
        with self.prevent(self.Changed):
            self.value = value
        self._label = self._make_label(label)
        if tooltip is not None:
            self.tooltip = tooltip
        self.compact = compact

    def _make_label(self, label: ContentText) -> Content:
        """Make label content.

        Args:
            label: The source value for the label.

        Returns:
            A `Content` rendering of the label for use in the button.
        """
        label = Content.from_text(label).first_line
        return label

    @property
    def label(self) -> Content:
        """The label associated with the button."""
        return self._label

    @label.setter
    def label(self, label: ContentText) -> None:
        self._label = self._make_label(label)
        self.refresh(layout=True)

    @property
    def _button(self) -> Content:
        """The button, reflecting the current value."""

        # Grab the button style.
        button_style = self.get_visual_style("toggle--button")

        # Building the style for the side characters. Note that this is
        # sensitive to the type of character used, so pay attention to
        # BUTTON_LEFT and BUTTON_RIGHT.
        side_style = Style(
            foreground=button_style.background,
            background=self.background_colors[1],
        )

        return Content.assemble(
            (self.BUTTON_LEFT, side_style),
            (self.BUTTON_INNER, button_style),
            (self.BUTTON_RIGHT, side_style),
        )

    def render(self) -> Content:
        """Render the content of the widget.

        Returns:
            The content to render for the widget.
        """
        button = self._button
        label_style = self.get_visual_style("toggle--label")
        label = self._label.stylize_before(label_style)
        spacer = " " if label else ""

        if self._button_first:
            content = Content.assemble(button, spacer, label)
        else:
            content = Content.assemble(label, spacer, button)
        return content

    def get_content_width(self, container: Size, viewport: Size) -> int:
        return (
            self._button.get_optimal_width(self.styles, 0)
            + (1 if self._label else 0)
            + self._label.get_optimal_width(self.styles, 0)
        )

    def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
        return 1

    def toggle(self) -> Self:
        """Toggle the value of the widget.

        Returns:
            The `ToggleButton` instance.
        """
        self.value = not self.value
        return self

    def action_toggle_button(self) -> None:
        """Toggle the value of the widget when called as an action.

        This would normally be used for a keyboard binding.
        """
        self.toggle()

    async def _on_click(self, _: Click) -> None:
        """Toggle the value of the widget when clicked with the mouse."""
        self.toggle()

    class Changed(Message):
        """Posted when the value of the toggle button changes."""

        def __init__(self, toggle_button: ToggleButton, value: bool) -> None:
            """Initialise the message.

            Args:
                toggle_button: The toggle button sending the message.
                value: The value of the toggle button.
            """
            super().__init__()
            self._toggle_button = toggle_button
            """A reference to the toggle button that was changed."""
            self.value = value
            """The value of the toggle button after the change."""

    def watch_value(self) -> None:
        """React to the value being changed.

        When triggered, the CSS class `-on` is applied to the widget if
        `value` has become `True`, or it is removed if it has become
        `False`. Subsequently a related `Changed` event will be posted.
        """
        self.set_class(self.value, "-on")
        self.post_message(self.Changed(self, self.value))
