from __future__ import annotations

from dataclasses import dataclass
from typing import Literal

from uniserde import JsonDoc

import rio

from .fundamental_component import FundamentalComponent

__all__ = [
    "Slider",
    "SliderChangeEvent",
]


@dataclass
class SliderChangeEvent:
    value: float


class Slider(FundamentalComponent):
    """
    # Slider

    A component for selecting a single value from a range.

    The `Slider` components allows the user to select a single real number value
    by dragging a handle along a line. The value can be any number within a
    range you can specify.


    ## Attributes:

    `minimum`: The minimum value the slider can be set to.

    `maximum`: The maximum value the slider can be set to.

    `value`: The current value of the slider.

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

    `on_change`: A callback that is called when the value of the slider changes.


    ## Example:

    A minimal example of a `Slider` ranging from 0 to 100 will be shown:

    ```python
    rio.Slider(minimum=0, maximum=100)
    ```

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

    ```python
    class MyComponent(rio.Component):
        value: int = 0

        def build(self) -> rio.Component:
            return rio.Slider(
                value=self.bind().value,  # state binding
                minimum=0,
                maximum=100,
                step_size=1,
                on_change=lambda event: print(f"value: {event.value}"),
            )
    ```

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

    ```python
    class MyComponent(rio.Component):
        value: int = 0

        def build(self) -> rio.Component:
            return rio.Slider(
                value=self.value,
                minimum=0,
                maximum=100,
                step_size=1,
                on_change=lambda event: setattr(self, "value", event.value),
            )
    ```

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

    ```python
    class MyComponent(rio.Component):
        value: float = 0

        def on_change(self, event: rio.SliderChangeEvent):
            self.value = event.value
            print(f"value: {self.value}")

        def build(self) -> rio.Component:
            return rio.Slider(
                value=self.value,
                minimum=0,
                maximum=100,
                step_size=1,
                on_change=self.on_change,
            )
    ```
    """

    minimum: float
    maximum: float
    value: float
    step_size: float
    is_sensitive: bool
    on_change: rio.EventHandler[SliderChangeEvent]

    def __init__(
        self,
        *,
        minimum: float = 0,
        maximum: float = 1,
        step_size: float = 0,
        value: float | None = None,
        is_sensitive: bool = True,
        on_change: rio.EventHandler[SliderChangeEvent] = None,
        key: str | None = None,
        margin: float | None = None,
        margin_x: float | None = None,
        margin_y: float | None = None,
        margin_left: float | None = None,
        margin_top: float | None = None,
        margin_right: float | None = None,
        margin_bottom: float | None = None,
        width: float | Literal["grow"] = 1.3,
        height: float | Literal["grow"] = 1.3,
        align_x: float | None = None,
        align_y: float | None = None,
    ):
        super().__init__(
            key=key,
            margin=margin,
            margin_x=margin_x,
            margin_y=margin_y,
            margin_left=margin_left,
            margin_top=margin_top,
            margin_right=margin_right,
            margin_bottom=margin_bottom,
            width=width,
            height=height,
            align_x=align_x,
            align_y=align_y,
        )

        self.minimum = minimum
        self.maximum = maximum
        self.step_size = step_size
        self.value = value  # type: ignore  Possibly assigning None. Fixed in __post_init__ below
        self.is_sensitive = is_sensitive
        self.on_change = on_change

    def __post_init__(self) -> None:
        # Don't hammer potential state bindings
        minimum = self.minimum
        maximum = self.maximum
        step_size = self.step_size
        value = self.value

        initial_value = value

        if maximum <= minimum:
            raise ValueError(
                f"`maximum` must be greater than `minimum`. Got {maximum} <= {minimum}"
            )

        if step_size < 0:
            raise ValueError(
                f"`step_size` must be greater than or equal to 0. Got {step_size}"
            )

        if value is None:
            value = minimum + (maximum - minimum) / 2

        if step_size != 0:
            value = round(value / step_size) * step_size

        value = min(maximum, max(minimum, value))

        # Only assign the value if it has in fact changed, as this causes a
        # refresh. If the value is bound to the parent and the parent rebuilds
        # this creates an infinite loop.
        if value != initial_value:
            self.value = value

    # TODO: When `minimum` or `maximum` is changed, make sure the value is still within
    # the range

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

        if "value" in delta_state and not self.is_sensitive:
            raise AssertionError(
                f"Frontend tried to set `Slider.value` 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["value"]
        except KeyError:
            pass
        else:
            assert isinstance(new_value, (int, float)), new_value
            await self.call_event_handler(
                self.on_change,
                SliderChangeEvent(new_value),
            )


Slider._unique_id = "Slider-builtin"
