from __future__ import annotations

from dataclasses import KW_ONLY, dataclass
from typing import Any

from uniserde import JsonDoc

import rio

from .fundamental_component import KeyboardFocusableFundamentalComponent

__all__ = [
    "MultiLineTextInput",
    "MultiLineTextInputChangeEvent",
    "MultiLineTextInputConfirmEvent",
]


@dataclass
class MultiLineTextInputChangeEvent:
    text: str


@dataclass
class MultiLineTextInputConfirmEvent:
    text: str


class MultiLineTextInput(KeyboardFocusableFundamentalComponent):
    """
    # MultiLineTextInput

    A user-editable text field.

    It's similar to `TextInput`, but it allows the user to enter multiple lines of text.

    `MultiLineTextInput` allows the user to enter a short text. The text can either be
    shown in plain text or other sensitive information.


    ## Attributes:

    `text`: The text currently entered by the user.

    `label`: A short text to display next to the text input.

    `is_sensitive`: Whether the text input should respond to user input.

    `is_valid`: Visually displays to the user whether the current text is
            valid. You can use this to signal to the user that their input needs
            to be changed.

    `on_change`: Triggered when the user changes the text.

    `on_confirm`: Triggered when the user explicitly confirms their input,
            such as by pressing the "Enter" key. You can use this to trigger
            followup actions, such as logging in or submitting a form.


    ## Example:

    A minimal example of `MultiLineTextInput` with a default text of "" will be shown:
    `Note`: The text will not be updated if the user changes the text in the input field.

    ```python
    rio.MultiLineTextInput(text="")
    ```

    You can easily `bind` state variables to track changes. If you want to make your
    `MultiLineTextInput` more responsive, you can easily achieve this by adding a
    lambda function call to e.g. `on_change`:

    ```python
    class MyComponent(rio.Component):
        text: str = ""

        def build(self) -> rio.Component:
            return rio.MultiLineTextInput(
                text=self.bind().text,  # state binding
                label="Write your comments here",
                on_change=lambda event: print(event.text),
            )
    ```

    If you are not using state bindings, you can also use a lambda function for updating
    the input text:

    ```python
    class MyComponent(rio.Component):
        text: str = ""

        def build(self) -> rio.Component:
            return rio.MultiLineTextInput(
                text=self.text,
                label="Write your comments here",
                on_change=lambda event: setattr(
                    self, "text", event.text
                ),  # Update the text
            )
    ```

    You can also use a method for updating the input text and do whatever you want.
    `Note` that methods are handy if you want to do more than just updating the input
    text. For example run async code or update other components based on the input text:

    ```python
    class MyComponent(rio.Component):
        text: str = ""

        def on_change_update_text(self, event: rio.MultiLineTextInputChangeEvent):
            self.text = event.text
            # You can do whatever you want in here

        def build(self) -> rio.Component:
            return rio.MultiLineTextInput(
                text=self.text,
                label="Write your comments here",
                on_change=self.on_change_update_text,
            )
    ```
    """

    text: str = ""
    _: KW_ONLY
    label: str = ""
    is_sensitive: bool = True
    is_valid: bool = True
    on_change: rio.EventHandler[MultiLineTextInputChangeEvent] = None
    on_confirm: rio.EventHandler[MultiLineTextInputConfirmEvent] = None

    def _validate_delta_state_from_frontend(self, delta_state: JsonDoc) -> None:
        if not set(delta_state) <= {"text"}:
            raise AssertionError(
                f"Frontend tried to change `{type(self).__name__}` state: {delta_state}"
            )

        if "text" in delta_state and not self.is_sensitive:
            raise AssertionError(
                f"Frontend tried to set `MultiLineTextInput.text` even though `is_sensitive` is `False`"
            )

    async def _call_event_handlers_for_delta_state(self, delta_state: JsonDoc) -> None:
        # Trigger on_change event
        try:
            new_value = delta_state["text"]
        except KeyError:
            pass
        else:
            assert isinstance(new_value, str), new_value
            await self.call_event_handler(
                self.on_change,
                MultiLineTextInputChangeEvent(new_value),
            )

        self._apply_delta_state_from_frontend(delta_state)

    async def _on_message(self, msg: Any) -> None:
        # Listen for messages indicating the user has confirmed their input
        #
        # In addition to notifying the backend, these also include the input's
        # current value. This ensures any event handlers actually use the up-to
        # date value.
        assert isinstance(msg, dict), msg

        self._apply_delta_state_from_frontend({"text": msg["text"]})

        await self.call_event_handler(
            self.on_change,
            MultiLineTextInputChangeEvent(self.text),
        )

        await self.call_event_handler(
            self.on_confirm,
            MultiLineTextInputConfirmEvent(self.text),
        )

        # Refresh the session
        await self.session._refresh()


MultiLineTextInput._unique_id = "MultiLineTextInput-builtin"
