import abc
from typing import Optional, Union, Dict
import omni.ui as ui
import omni.kit.app
import carb.events

from .style import UI_STYLE

# First delay time to start a long click (in seconds)
SPINNER_LONG_CLICK_DELAY = 0.6
# Long click interval (in seconds)
SPINNER_LONG_CLICK_INTERVAL = 0.2


class AbstractSpinner:
    """
    A Spinner widget includes a value field and a set of arrow (up/down or left/right) to change field value.
    Kwargs:
        model (ui.AbstractValueModel): Value model for value field. Default None. 
        step (Union[float, int]): Step value. Default 1.
        min (Union[float, int, None]): Min value. Default None means no min value.
        max (Union[float, int, None]): Max value. Default None means no max value.
        vertical (bool): Vertical spinner (with up/down arrows) if True. Otherwise horizontal spinner (with left/right arrows). Default True.
        width (ui.Length): Widget width. Default ui.Fraction(1).
    """

    def __init__(
        self,
        model: ui.AbstractValueModel = None,
        step: Union[float, int] = 1,
        min: Union[float, int, None] = None,
        max: Union[float, int, None] = None,
        vertical: bool = True,
        width: ui.Length = ui.Fraction(1),
        style: Dict = None,
        precision: int = 7
    ):
        self._model = model
        self._step = step
        self._width = width
        self._min = min
        self._max = max
        self._vertical = vertical
        self._precision = precision
        self._arrow_size = 6

        ui_style = UI_STYLE.copy()
        if style is not None:
            ui_style.update(style)

        self._build_ui(ui_style)

    def destroy(self) -> None:
        self._sub = None

    @abc.abstractclassmethod
    def _create_field_widget(self, model: Optional[ui.AbstractValueModel], **kwargs) -> ui.AbstractField:
        pass

    @property
    def enabled(self) -> bool:
        return self._container.enabled

    @enabled.setter
    def enabled(self, value: bool) -> None:
        self._container.enabled = value

    def _build_ui(self, style: Dict):
        self._container = ui.HStack(width=self._width, height=0, style=style)
        with self._container:
            if self._vertical:
                self._build_vertical_ui()
            else:
                self._build_horizontal_ui()

    def _build_vertical_ui(self):
        self._field = self._create_field_widget(self._model, style_type_name_override="Spinner.Field")
        ui.Spacer(width=5)
        with ui.ZStack(width=self._arrow_size):
            ui.Rectangle(style_type_name_override="Spinner.Arrow.Background")
            with ui.VStack(width=0, spacing=0):
                ui.Spacer()
                ui.Triangle(
                    width=self._arrow_size,
                    height=self._arrow_size,
                    alignment=ui.Alignment.CENTER_TOP,
                    style_type_name_override="Spinner.Arrow",
                    mouse_pressed_fn=(lambda x, y, key, m: self._begin_change(key, self._step)),
                    mouse_released_fn=(lambda *_: self._end_change()),
                )
                ui.Spacer(height=4)
                ui.Triangle(
                    width=self._arrow_size,
                    height=self._arrow_size,
                    alignment=ui.Alignment.CENTER_BOTTOM,
                    style_type_name_override="Spinner.Arrow",
                    mouse_pressed_fn=(lambda x, y, key, m: self._begin_change(key, -self._step)),
                    mouse_released_fn=(lambda *_: self._end_change()),
                )
                ui.Spacer()

    def _build_horizontal_ui(self):
        with ui.VStack(width=0):
            ui.Spacer()
            ui.Triangle(
                width=self._arrow_size,
                height=self._arrow_size,
                alignment=ui.Alignment.LEFT_CENTER,
                style_type_name_override="Spinner.Arrow",
                mouse_pressed_fn=(lambda x, y, key, m: self._begin_change(key, -self._step)),
                mouse_released_fn=(lambda *_: self._end_change()),
            )
            ui.Spacer()
        ui.Spacer(width=4)
        self._field = self._create_field_widget(self._model, style_type_name_override="Spinner.Field")
        ui.Spacer(width=4)
        with ui.VStack(width=0, spacing=0):
            ui.Spacer()
            ui.Triangle(
                width=self._arrow_size,
                height=self._arrow_size,
                alignment=ui.Alignment.RIGHT_CENTER,
                style_type_name_override="Spinner.Arrow",
                mouse_pressed_fn=(lambda x, y, key, m: self._begin_change(key, self._step)),
                mouse_released_fn=(lambda *_: self._end_change()),
            )
            ui.Spacer()

    def _begin_change(self, button: int, step: int):
        if not self._container.enabled:
            return
        if button != 0:
            return
        self._delta = step
        self._action_time = 0.0
        self._current_time = 0.0

        self._update_value()

        self._register_update()

    def _end_change(self):
        self._deregister_update()

    def _register_update(self):
        self._sub = (
            omni.kit.app.get_app()
            .get_update_event_stream()
            .create_subscription_to_pop(self._on_update, name="spinner update")
        )

    def _deregister_update(self):
        self._sub = None

    def _on_update(self, event: carb.events.IEvent):
        dt = event.payload["dt"]
        self._current_time += dt

        if self._action_time == 0.0:
            self._action_time = self._current_time + SPINNER_LONG_CLICK_DELAY
        elif self._current_time > self._action_time:
            self._update_value()
            self._action_time += SPINNER_LONG_CLICK_INTERVAL

    def _update_value(self):
        new_value = round(self._field.model.as_float + self._delta, self._precision)
        if self._min is not None:
            new_value = max(new_value, self._min)
        if self._max is not None:
            new_value = min(new_value, self._max)
        self._field.model.set_value(new_value)


class FloatSpinner(AbstractSpinner):
    def _create_field_widget(self, model: Optional[ui.AbstractValueModel], **kwargs) -> ui.AbstractField:
        return ui.FloatField(model, **kwargs)


class IntSpinner(AbstractSpinner):
    def _create_field_widget(self, model: Optional[ui.AbstractValueModel], **kwargs) -> ui.AbstractField:
        return ui.IntField(model, **kwargs)


class IntDragSpinner(AbstractSpinner):
    def _create_field_widget(self, model: Optional[ui.AbstractValueModel], **kwargs) -> ui.AbstractField:
        return ui.IntDrag(model, **kwargs)
