from datetime import datetime

from ipywidgets import jslink
import ipyvuetify as v
from deprecated.sphinx import deprecated
from traitlets import Unicode, observe, directional_link, Bool

from sepal_ui.sepalwidgets.sepalwidget import SepalWidget, TYPES

__all__ = ["Divider", "Alert", "StateBar"]


class Divider(v.Divider, SepalWidget):
    """
    A custom Divider with the ability to dynamically change color
    Whenever the type\_ trait is modified, the divider class will change accordingly

    Args:
        class\_ (str, optional): the initial color of the divider
        kwargs (optional): any parameter from a v.Divider. if set, 'class_' will be overwritten.
    """

    type_ = Unicode("").tag(sync=True)
    "str: Added type_ trait to specify the current color of the divider"

    def __init__(self, class_="", **kwargs):
        kwargs["class_"] = class_
        super().__init__(**kwargs)

    @observe("type_")
    def add_class_type(self, change):
        """
        Change the color of the divider according to the type\_
        It is binded to the type\_ traitlet but can also be called manually

        Args:
            change (dict): the only useful key is 'new' which is the new required color

        Return:
            self
        """

        type_ = change["new"]
        classes = self.class_.split(" ")
        existing = list(set(classes) & set(TYPES))
        if existing:
            classes[classes.index(existing[0])] = type_
            self.class_ = " ".join(classes)
        else:
            self.class_ += f" {type_}"

        return self


class Alert(v.Alert, SepalWidget):
    """
    A custom Alert widget. It is used as the output of all processes in the framework.
    In the voila interfaces, print statement will not be displayed.
    Instead use the sw.Alert method to provide information to the user.
    It's hidden by default.

    Args:
        type\_ (str, optional): The color of the Alert
        kwargs (optional): any parameter from a v.Alert. If set, 'type' will be overwritten.
    """

    def __init__(self, type_=None, **kwargs):

        # set default parameters
        kwargs["text"] = kwargs.pop("text", True)
        kwargs["type"] = type_ if (type_ in TYPES) else TYPES[0]
        kwargs["class_"] = kwargs.pop("class_", "mt-5")

        # call the constructor
        super().__init__(**kwargs)

        self.hide()

    def update_progress(self, progress, msg="Progress", bar_length=30):
        """
        Update the Alert message with a progress bar. This function will stay until we manage to use tqdm in the widgets

        Args:
            progress (float): the progress status in float [0, 1]
            msg (str, optionnal): The message to use before the progress bar
            bar_length (int, optionnal): the length of the progress bar in characters

        Return:
            self
        """

        # define the characters to use in the progress bar
        plain_char = "█"
        empty_char = " "

        # cast the progress to float
        progress = float(progress)
        if not (0 <= progress <= 1):
            raise ValueError(f"progress should be in [0, 1], {progress} given")

        # set the length parameter
        block = int(round(bar_length * progress))

        # construct the message content
        text = f"|{plain_char * block + empty_char * (bar_length - block)}|"

        # add the message to the output
        self.add_live_msg(
            v.Html(
                tag="span",
                children=[
                    v.Html(tag="span", children=[f"{msg}: "], class_="d-inline"),
                    v.Html(tag="pre", class_="info--text d-inline", children=[text]),
                    v.Html(
                        tag="span",
                        children=[f" {progress *100:.1f}%"],
                        class_="d-inline",
                    ),
                ],
            )
        )

        return self

    def add_msg(self, msg, type_="info"):
        """
        Add a message in the alert by replacing all the existing one.
        The color can also be changed dynamically

        Args:
            msg (str): the message to display
            type\_ (str, optional): the color to use in the widget

        Return:
            self
        """
        self.show()
        self.type = type_ if (type_ in TYPES) else TYPES[0]
        self.children = [v.Html(tag="p", children=[msg])]

        return self

    def add_live_msg(self, msg, type_="info"):
        """
        Add a message in the alert by replacing all the existing one.
        Also add the timestamp of the display.
        The color can also be changed dynamically

        Args:
            msg (str): the message to display
            type\_ (str, optional): the color to use in the widget

        Return:
            self
        """

        current_time = datetime.now().strftime("%Y/%m/%d, %H:%M:%S")

        self.show()
        self.type = type_ if (type_ in TYPES) else TYPES[0]

        self.children = [
            v.Html(tag="p", children=["[{}]".format(current_time)]),
            v.Html(tag="p", children=[msg]),
        ]

        return self

    def append_msg(self, msg, section=False, type_="info"):
        """
        Append a message in a new parragraph, with or without divider

        Args:
            msg (str): the message to display
            section (bool, optional): add a Divider before the added message

        Return:
            self
        """

        self.show()
        self.type = type_

        if len(self.children):
            current_children = self.children[:]
            if section:
                # As the list is mutable, and the trait is only triggered when
                # the children is changed, so have to create a copy.
                divider = Divider(class_="my-4", style="opacity: 0.22")

                # link Alert type with divider type
                directional_link((self, "type"), (divider, "type_"))
                current_children.extend([divider, v.Html(tag="p", children=[msg])])
                self.children = current_children

            else:
                current_children.append(v.Html(tag="p", children=[msg]))
                self.children = current_children
        else:
            self.add_msg(msg)

        return self

    def remove_last_msg(self):
        """
        Remove the last msg printed in the Alert widget

        Return:
            self
        """

        if len(self.children) > 1:
            current_children = self.children[:]
            self.children = current_children[:-1]
        else:
            self.reset()

        return self

    def reset(self):
        """
        Empty the messages and hide it

        Return:
            self
        """

        self.children = [""]
        self.hide()

        return self

    @deprecated(version="2.1.0", reason="use a Model object instead")
    def bind(self, widget, obj, attribute, msg=None, verbose=True, secret=False):
        """
        Bind the attribute to the widget and display it in the alert.
        The binded input need to have an active `v_model` trait.
        After the binding, whenever the `v_model` of the input is changed, the io attribute is changed accordingly.
        The value can also be displayed in the alert with a custom message with the following format = `[custom message] + [v_model]`

        Args:
            widget (v.XX): an ipyvuetify input element with an activated `v_model` trait
            obj (io): any io object
            attribute (str): the name of the attribute in io object
            msg (str, optionnal): the output message displayed before the variable
            verbose (bool, optional): wheter the variable should be displayed to the user
            secret (bool, optional): either if the variable is secret or not. If true only "*" will be shown in the output

        Return:
            self
        """
        if not msg:
            msg = "The selected variable is: "

        def _on_change(change, obj=obj, attribute=attribute, msg=msg):

            # if the key doesn't exist the getattr function will raise an AttributeError
            getattr(obj, attribute)

            # change the obj value
            setattr(obj, attribute, change["new"])

            # add the message if needed
            if secret:
                msg += "*" * len(str(change["new"]))
            else:
                msg += str(change["new"])

            if verbose:
                self.add_msg(msg)

            return

        widget.observe(_on_change, "v_model")

        return self

    def check_input(self, input_, msg=None):
        """
        Check if the inpupt value is initialized.
        If not return false and display an error message else return True

        Args:
            input\_ (any): the input to check
            msg (str, optionnal): the message to display if the input is not set

        Return:
            (bool): check if the value is initialized
        """
        if not msg:
            msg = "The value has not been initialized"
        init = True

        # check the collection type that are the only one supporting the len method
        try:
            init = False if len(input_) == 0 else init
        except Exception:
            init = False if input_ is None else init

        if init is False:
            self.add_msg(msg, "error")

        return init


class StateBar(v.SystemBar):

    """Widget to display quick messages on simple inline status bar

    Args:
       kwargs (optional): any parameter from a v.SystemBar. If set, 'children' will be overwritten.
    """

    msg = Unicode("").tag(sync=True)
    "str: the displayed message"

    loading = Bool(False).tag(sync=True)
    "bool: either or not the circular progress is spinning"

    progress = None
    "widget: The ProgressCircular widget that will be displayed in the statebar"

    def __init__(self, **kwargs):

        self.progress = v.ProgressCircular(
            indeterminate=self.loading,
            value=100,
            small=True,
            size=15,
            color="primary",
            class_="mr-2",
        )

        # set default parameter
        kwargs["children"] = [self.progress, self.msg]

        # call the constructor
        super().__init__(**kwargs)

        jslink((self, "loading"), (self.progress, "indeterminate"))

    @observe("loading")
    def _change_loading(self, change):
        """Change progress wheel state"""
        self.progress.indeterminate = self.loading

    @observe("msg")
    def _change_msg(self, change):
        """Change state bar message"""
        self.children = [self.progress, self.msg]

    def add_msg(self, msg, loading=False):
        """Change current status message"""
        self.msg = msg
        self.loading = loading

        return self
