from abc import abstractmethod

from ipywidgets import widgets, Layout, ValueWidget, link, HBox
from ipywidgets.widgets.widget_description import DescriptionWidget


class WindowController(HBox, ValueWidget, DescriptionWidget):
    def __init__(
        self,
        vmin,
        vmax,
        start_value=(None, None),
        description="window (s)",
        orientation="horizontal",
        **kwargs
    ):

        if orientation not in ("horizontal", "vertical"):
            ValueError("Unrecognized orientation: {}".format(orientation))

        super().__init__()
        self.vmin = vmin
        self.vmax = vmax
        self.start_value = start_value
        self.description = description
        self.orientation = orientation

        if self.orientation == "horizontal":
            self.to_start_button = widgets.Button(
                description="◀◀", layout=Layout(width="65px")
            )
            self.backwards_button = widgets.Button(
                description="◀", layout=Layout(width="40px")
            )
            self.forward_button = widgets.Button(
                description="▶", layout=Layout(width="40px")
            )
            self.to_end_button = widgets.Button(
                description="▶▶", layout=Layout(width="65px")
            )
        else:  # vertical
            self.to_end_button = widgets.Button(
                description="▲▲", layout=Layout(width="50px")
            )
            self.forward_button = widgets.Button(
                description="▲", layout=Layout(width="50px")
            )
            self.backwards_button = widgets.Button(
                description="▼", layout=Layout(width="50px")
            )
            self.to_start_button = widgets.Button(
                description="▼▼", layout=Layout(width="50px")
            )

        self.to_start_button.on_click(self.move_start)
        self.backwards_button.on_click(self.move_down)
        self.forward_button.on_click(self.move_up)
        self.to_end_button.on_click(self.move_end)

    # @abstractmethod
    # @property
    # def value(self):
    #    """Must be window in seconds"""
    #    pass

    @abstractmethod
    def get_children(self):
        pass

    @abstractmethod
    def move_up(self, change):
        pass

    @abstractmethod
    def move_down(self, change):
        pass

    @abstractmethod
    def move_start(self, change):
        pass

    @abstractmethod
    def move_end(self, change):
        pass


class RangeController(WindowController):
    def __init__(
        self,
        vmin,
        vmax,
        start_value=None,
        dtype="float",
        description="time window (s)",
        orientation="horizontal",
        **kwargs
    ):
        super().__init__(vmin, vmax, start_value, description, orientation, **kwargs)

        self.dtype = dtype
        self.slider = self.make_range_slider(description=description, **kwargs)

        link((self.slider, "value"), (self, "value"))
        link((self.slider, "description"), (self, "description"))

        self.children = self.get_children()

    def make_range_slider(self, **kwargs):
        """

        Parameters
        ----------
        kwargs: passed into RangeSlider constructor

        Returns
        -------

        """

        slider_kwargs = dict(
            value=self.start_value,
            min=self.vmin,
            max=self.vmax,
            continuous_update=False,
            readout=True,
            style={"description_width": "initial"},
            orientation=self.orientation,
        )

        if self.dtype == "float":
            slider_kwargs.update(
                readout_format=".1f",
                step=0.1,
                description="time window (s)",
                layout=Layout(width="100%"),
            )
            slider_kwargs.update(kwargs)
            return widgets.FloatRangeSlider(**slider_kwargs)
        elif self.dtype == "int":
            slider_kwargs.update(
                description="unit window", layout=Layout(height="100%")
            )
            slider_kwargs.update(kwargs)
            return widgets.IntRangeSlider(**slider_kwargs)
        else:
            raise ValueError("Unrecognized dtype: {}".format(self.dtype))

    def move_up(self, change):
        value_range = self.value[1] - self.value[0]
        if self.value[1] + value_range < self.vmax:
            self.value = (self.value[0] + value_range, self.value[1] + value_range)
        else:
            self.move_end(change)

    def move_down(self, change):
        value_range = self.value[1] - self.value[0]
        if self.value[0] - value_range > self.vmin:
            self.value = (self.value[0] - value_range, self.value[1] - value_range)
        else:
            self.move_start(change)

    def move_start(self, change):
        value_range = self.value[1] - self.value[0]
        self.value = (self.vmin, self.vmin + value_range)

    def move_end(self, change):
        value_range = self.value[1] - self.value[0]
        self.value = (self.vmax - value_range, self.vmax)

    def get_children(self):

        if self.orientation == "horizontal":
            return [
                self.slider,
                self.to_start_button,
                self.forward_button,
                self.backwards_button,
                self.to_end_button,
            ]
        else:
            return [
                widgets.VBox(
                    [
                        self.slider,
                        self.to_end_button,
                        self.forward_button,
                        self.backwards_button,
                        self.to_start_button,
                    ],
                    layout=widgets.Layout(
                        display="flex", flex_flow="column", align_items="center"
                    ),
                )
            ]


class StartAndDurationController(WindowController):
    """
    Can be used in place of the RangeController.
    """

    DEFAULT_DURATION = 5

    def __init__(
        self, tmax, tmin=0, start_value=None, description="start (s)", **kwargs
    ):
        """

        Parameters
        ----------
        tmax: float
            in seconds
        tmin: float
            in seconds
        start_value: (float, float)
            start and stop in seconds
        description: str
        kwargs: dict
        """

        if tmin > tmax:
            raise ValueError(
                "tmax and tmin were probably entered in the wrong order. tmax should be first"
            )

        super().__init__(tmin, tmax, start_value, description, **kwargs)

        self.slider = widgets.FloatSlider(
            value=start_value,
            min=tmin,
            max=tmax,
            step=0.01,
            description=description,
            continuous_update=False,
            orientation="horizontal",
            readout=True,
            readout_format=".2f",
            style={"description_width": "initial"},
            layout=Layout(width="100%", min_width="250px"),
        )

        link((self.slider, "description"), (self, "description"))

        if start_value is None:
            duration = min(self.DEFAULT_DURATION, tmax - tmin)
        else:
            duration = start_value[1] - start_value[0]

        self.duration = widgets.BoundedFloatText(
            value=duration,
            min=0,
            max=tmax - tmin,
            step=0.1,
            description="duration (s):",
            style={"description_width": "initial"},
            layout=Layout(max_width="200px"),
        )

        self.value = (self.slider.value, self.slider.value + self.duration.value)

        self.slider.observe(self.monitor_slider)
        self.duration.observe(self.monitor_duration)

        self.children = self.get_children()

    def monitor_slider(self, change):
        if "new" in change:
            if isinstance(change["new"], dict):
                if "value" in change["new"]:
                    value = change["new"]["value"]
                else:
                    return
            else:
                value = change["new"]
        if self.slider.value + self.duration.value > self.vmax:
            self.slider.value = self.vmax - self.duration.value
        else:
            self.value = (value, value + self.duration.value)

    def monitor_duration(self, change):
        if "new" in change:
            if isinstance(change["new"], dict):
                if "value" in change["new"]:
                    value = change["new"]["value"]
                    if self.slider.value + value > self.vmax:
                        self.slider.value = self.vmax - value
                    self.value = (self.slider.value, self.slider.value + value)

    def move_up(self, change):
        if self.slider.value + 2 * self.duration.value < self.vmax:
            self.slider.value += self.duration.value
        else:
            self.move_end(change)

    def move_down(self, change):
        if self.slider.value - self.duration.value > self.vmin:
            self.slider.value -= self.duration.value
        else:
            self.move_start(change)

    def move_start(self, change):
        self.slider.value = self.vmin

    def move_end(self, change):
        self.slider.value = self.vmax - self.duration.value

    def get_children(self):

        if self.orientation == "horizontal":
            return [
                self.slider,
                self.duration,
                # self.to_start_button,
                self.backwards_button,
                self.forward_button,
                # self.to_end_button
            ]
        else:
            return [
                widgets.VBox(
                    [
                        self.slider,
                        self.duration,
                        # self.to_end_button,
                        self.forward_button,
                        self.backwards_button,
                        # self.to_start_button,
                    ],
                    layout=widgets.Layout(
                        display="flex", flex_flow="column", align_items="center"
                    ),
                )
            ]
