from __future__ import annotations

from typing import Any, Callable, Dict, List, Optional, Union

from supervisely.app import DataJson, StateJson
from supervisely.app.widgets import Widget

try:
    from typing import Literal
except ImportError:
    from typing_extensions import Literal


class Carousel(Widget):
    """Cycles through a series of images or text in limited space."""

    class Routes:
        """Callback route names used by the widget frontend to notify Python."""
        VALUE_CHANGED = "value_changed"

    class Item:
        """Single carousel item (name, label, is_link)."""

        def __init__(
            self,
            name: Optional[str] = "",
            label: Optional[str] = "",
            is_link: Optional[bool] = True,
        ):
            """
            :param name: Item name.
            :param label: Item label.
            :param is_link: Display as link.
            """
            self.name = name
            self.label = label
            self.is_link = is_link

        def to_json(self) -> Dict[str, Union[str, bool]]:
            """Returns dictionary with item data.

            Dictionary contains the following fields:
                - name: Name of the item.
                - label: Label of the item.
                - is_link: If True, the item will be displayed as a link.
            """
            return {
                "name": self.name,
                "label": self.label,
                "is_link": self.is_link,
            }

    def __init__(
        self,
        items: List[Carousel.Item],
        height: Optional[int] = 350,
        initial_index: Optional[int] = 0,
        trigger: Optional[Literal["hover", "click"]] = "click",
        autoplay: Optional[bool] = False,
        interval: Optional[int] = 3000,
        indicator_position: Optional[Literal["outside", "none"]] = "none",
        arrow: Optional[Literal["always", "hover", "never"]] = "hover",
        type: Optional[Literal["card"]] = None,
        widget_id: Optional[str] = None,
    ):
        """
        :param items: List of Carousel.Item.
        :param height: Height in pixels.
        :param initial_index: Initial visible item index.
        :param trigger: "hover" or "click".
        :param autoplay: Enable autoplay.
        :param interval: Autoplay interval (ms).
        :param indicator_position: "outside" or "none".
        :param arrow: "always", "hover", or "never".
        :param type: "card" or None.
        :param widget_id: Widget identifier.

        :Usage Example:

            .. code-block:: python

                from supervisely.app.widgets import Carousel
                items = [Carousel.Item(name="i1", label="Item 1"), Carousel.Item(name="i2", label="Item 2")]
                carousel = Carousel(items=items, height=350, trigger="click")
        """
        self._height = f"{height}px"
        self._items = items
        self._initial_index = initial_index
        self._trigger = trigger
        self._autoplay = autoplay
        self._interval = interval
        self._indicator_position = indicator_position if indicator_position is not None else "none"
        self._arrow = arrow
        self._type = type

        self._changes_handled = False
        self._clicked_value = None

        super().__init__(widget_id=widget_id, file_path=__file__)

    def _set_items(self):
        return [item.to_json() for item in self._items]

    def get_json_data(self) -> Dict[str, Any]:
        """Returns dictionary with widget data, which defines the appearance and behavior of the widget.

        Dictionary contains the following fields:
            - height: Height of the carousel in pixels.
            - items: List of items to be displayed in the carousel.
            - initialIndex: Index of the item to be displayed at initialization.
            - trigger: Trigger type to change the active item.
            - autoplay: If True, the carousel will be autoplayed.
            - interval: Time interval between each autoplay.
            - indicatorPosition: Position of the indicator.
            - arrow: Arrow display type.
            - type: Carousel type.
        """

        return {
            "height": self._height,
            "items": self._set_items(),
            "initialIndex": self._initial_index,
            "trigger": self._trigger,
            "autoplay": self._autoplay,
            "interval": self._interval,
            "indicatorPosition": self._indicator_position,
            "arrow": self._arrow,
            "type": self._type,
        }

    def get_json_state(self) -> Dict[str, Any]:
        """Returns dictionary with widget state.

        Dictionary contains the following fields:
            - clickedValue: Name of the item that was clicked.

        :returns: Dictionary with widget state.
        :rtype: Dict[str, Any]
        """
        return {"clickedValue": self._clicked_value}

    def get_active_item(self) -> int:
        """Returns index of the active item.

        :returns: Index of the active item.
        :rtype: int
        """
        return StateJson()[self.widget_id]["clickedValue"]

    def get_items(self) -> List[Carousel.Item]:
        """Returns list of items.

        :returns: List of items.
        :rtype: List[:class:`~supervisely.app.widgets.carousel.carousel.Carousel.Item`]
        """
        return DataJson()[self.widget_id]["items"]

    def set_items(self, value: List[Carousel.Item]) -> None:
        """Sets list of items to be displayed in the carousel.
        This method will overwrite the existing items, not append to it.
        To append items, use :meth:`add_items`.

        :param value: List of items to be displayed in the carousel.
        :type value: List[:class:`~supervisely.app.widgets.carousel.carousel.Carousel.Item`]
        :raises ValueError: If items are not of type :class:`~supervisely.app.widgets.carousel.carousel.Carousel.Item`.
        """
        if not all(isinstance(item, Carousel.Item) for item in value):
            raise ValueError("Items must be of type Carousel.Item")
        self._items = value
        DataJson()[self.widget_id]["items"] = self._set_items()
        DataJson().send_changes()

    def add_items(self, value: List[Carousel.Item]) -> None:
        """Appends list of items to the existing items.
        This method will not overwrite the existing items, but append to it.
        To overwrite items, use :meth:`set_items`.

        :param value: List of items to be displayed in the carousel.
        :type value: List[:class:`~supervisely.app.widgets.carousel.carousel.Carousel.Item`]
        """
        self._items.extend(value)
        DataJson()[self.widget_id]["items"] = self._set_items()
        DataJson().send_changes()

    def set_height(self, value: int) -> None:
        """Sets height of the carousel.

        :param value: Height of the carousel in pixels.
        :type value: int
        :raises ValueError: If height value is not an integer.
        """
        if type(value) is not int:
            raise ValueError("Height value must be an integer")
        self._height = f"{value}px"
        DataJson()[self.widget_id]["height"] = self._height
        DataJson().send_changes()

    def get_initial_index(self) -> int:
        """Returns index of the item to be displayed at initialization.

        :returns: Index of the item to be displayed at initialization.
        :rtype: int
        """
        return DataJson()[self.widget_id]["initialIndex"]

    def set_initial_index(self, value: int) -> None:
        """Sets index of the item to be displayed at initialization.

        :param value: Index of the item to be displayed at initialization.
        :type value: int
        :raises ValueError: If index value exceeds the size of the carousel items.
        """
        if value >= len(self._items):
            raise ValueError("Index of the value being set exceeds the size of the carousel items.")
        self._initial_index = value
        DataJson()[self.widget_id]["initialIndex"] = self._initial_index
        DataJson().send_changes()

    def value_changed(self, func: Callable[[str], Any]) -> Callable[[], None]:
        """Decorator for the function to be called when the value of the carousel is changed.

        :param func: Function to be called when the value of the carousel is changed.
        :type func: Callable[[str], Any]
        :returns: Decorated function.
        :rtype: Callable[[], None]
        """
        route_path = self.get_route_path(Carousel.Routes.VALUE_CHANGED)
        server = self._sly_app.get_server()
        self._changes_handled = True

        @server.post(route_path)
        def _click():
            curr_idx = self.get_active_item()
            curr_name = self._items[curr_idx].name
            func(curr_name)

        return _click
