"""
Components/Spinner
==================

.. seealso::

    `Material Design spec, Menus <https://material.io/components/progress-indicators#circular-progress-indicators>`_

.. rubric:: Circular progress indicator in Google's Material Design.

Usage
-----

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp

    KV = '''
    MDScreen:

        MDSpinner:
            size_hint: None, None
            size: dp(46), dp(46)
            pos_hint: {'center_x': .5, 'center_y': .5}
            active: True if check.active else False

        MDCheckbox:
            id: check
            size_hint: None, None
            size: dp(48), dp(48)
            pos_hint: {'center_x': .5, 'center_y': .4}
            active: True
    '''


    class Test(MDApp):
        def build(self):
            return Builder.load_string(KV)


    Test().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/spinner.gif
    :align: center

Spinner palette
---------------

.. code-block:: kv

    MDSpinner:
        # The number of color values ​​can be any.
        palette:
            [0.28627450980392155, 0.8431372549019608, 0.596078431372549, 1], \
            [0.3568627450980392, 0.3215686274509804, 0.8666666666666667, 1], \
            [0.8862745098039215, 0.36470588235294116, 0.592156862745098, 1], \
            [0.8784313725490196, 0.9058823529411765, 0.40784313725490196, 1],

.. code-block:: python

    MDSpinner(
        size_hint=(None, None),
        size=(dp(46), dp(46)),
        pos_hint={'center_x': .5, 'center_y': .5},
        active=True,
        palette=[
            [0.28627450980392155, 0.8431372549019608, 0.596078431372549, 1],
            [0.3568627450980392, 0.3215686274509804, 0.8666666666666667, 1],
            [0.8862745098039215, 0.36470588235294116, 0.592156862745098, 1],
            [0.8784313725490196, 0.9058823529411765, 0.40784313725490196, 1],
        ]
    )

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/spinner-palette.gif
    :align: center

Determinate mode
----------------

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp

    KV = '''
    MDScreen:

        MDSpinner:
            size_hint: None, None
            size: dp(48), dp(48)
            pos_hint: {'center_x': .5, 'center_y': .5}
            determinate: True
    '''


    class Test(MDApp):
        def build(self):
            return Builder.load_string(KV)


    Test().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/spinner-determinate.gif
    :align: center
"""

__all__ = ("MDSpinner",)

import os
from typing import Union

from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
    BooleanProperty,
    ColorProperty,
    ListProperty,
    NumericProperty,
)
from kivy.uix.widget import Widget

from kivymd import uix_path
from kivymd.theming import ThemableBehavior

with open(
    os.path.join(uix_path, "spinner", "spinner.kv"), encoding="utf-8"
) as kv_file:
    Builder.load_string(kv_file.read())


class MDSpinner(ThemableBehavior, Widget):
    """
    :class:`MDSpinner` is an implementation of the circular progress
    indicator in `Google's Material Design`.

    For more information, see in the
    :class:`~kivymd.theming.ThemableBehavior` and
    :class:`~kivy.uix.widget.Widget` classes documentation.

    It can be used either as an indeterminate indicator that loops while
    the user waits for something to happen, or as a determinate indicator.

    Set :attr:`determinate` to **True** to activate determinate mode, and
    :attr:`determinate_time` to set the duration of the animation.

    :Events:
        `on_determinate_complete`
            The event is called at the end of the spinner loop in the
            `determinate = True` mode.
    """

    determinate = BooleanProperty(False)
    """
    Determinate value.

    :attr:`determinate` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    determinate_time = NumericProperty(2)
    """
    Determinate time value.

    :attr:`determinate_time` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `2`.
    """

    line_width = NumericProperty(dp(2.25))
    """
    Progress line width of spinner.

    :attr:`line_width` is a :class:`~kivy.properties.NumericProperty`
    and defaults to `dp(2.25)`.
    """

    active = BooleanProperty(True)
    """
    Use :attr:`active` to start or stop the spinner.

    :attr:`active` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    color = ColorProperty(None, allownone=True)
    """
    Spinner color in (r, g, b, a) or string format.

    :attr:`color` is a :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    palette = ListProperty()
    """
    A set of colors. Changes with each completed spinner cycle.

    :attr:`palette` is a :class:`~kivy.properties.ListProperty`
    and defaults to `[]`.
    """

    _alpha = NumericProperty(0)
    _rotation_angle = NumericProperty(360)
    _angle_start = NumericProperty(0)
    _angle_end = NumericProperty(0)
    _palette = []

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if not self.color:
            self.color = self.theme_cls.primary_color
        if self.color == self.theme_cls.primary_color:
            self.theme_cls.bind(primary_color=self._update_color)

        self._alpha_anim_in = Animation(_alpha=1, duration=0.8, t="out_quad")
        self._alpha_anim_out = Animation(_alpha=0, duration=0.3, t="out_quad")
        self._alpha_anim_out.bind(
            on_complete=self._reset,
            on_progress=self._on_determinate_progress,
        )
        self.register_event_type("on_determinate_complete")
        Clock.schedule_once(self.check_determinate)

    def on__rotation_angle(self, *args):
        if self._rotation_angle == 0:
            self._rotation_angle = 360
            if not self.determinate:
                _rot_anim = Animation(_rotation_angle=0, duration=2)
                _rot_anim.start(self)
        elif self._rotation_angle == 360:
            if self._palette:
                try:
                    Animation(color=next(self._palette), duration=2).start(self)
                except StopIteration:
                    self._palette = iter(self.palette)
                    Animation(color=next(self._palette), duration=2).start(self)

    def on_palette(self, instance_spinner, palette_list: list) -> None:
        self._palette = iter(palette_list)

    def on_active(self, instance_spinner, active_value: bool) -> None:
        self._reset()
        if self.active:
            self.check_determinate()

    def on_determinate_complete(self, *args):
        """
        The event is called at the end of the spinner loop in the
        `determinate = True` mode.
        """

    def check_determinate(self, interval: Union[float, int] = 0) -> None:
        if self.active:
            if self.determinate:
                self._start_determinate()
            else:
                self._start_loop()

    def _update_color(self, *args):
        self.color = self.theme_cls.primary_color

    def _start_determinate(self, *args):
        self._alpha_anim_in.start(self)
        Animation(
            _rotation_angle=0,
            duration=self.determinate_time * 0.7,
            t="out_quad",
        ).start(self)

        _angle_start_anim = Animation(
            _angle_end=360, duration=self.determinate_time, t="in_out_quad"
        )
        _angle_start_anim.bind(
            on_complete=lambda *x: self._alpha_anim_out.start(self)
        )

        _angle_start_anim.start(self)

    def _start_loop(self, *args):
        if self._alpha == 0:
            _rot_anim = Animation(_rotation_angle=0, duration=2, t="linear")
            _rot_anim.start(self)

        self._alpha = 1
        self._alpha_anim_in.start(self)
        _angle_start_anim = Animation(
            _angle_end=self._angle_end + 270, duration=0.6, t="in_out_cubic"
        )
        _angle_start_anim.bind(on_complete=self._anim_back)
        _angle_start_anim.start(self)

    def _anim_back(self, *args):
        _angle_back_anim = Animation(
            _angle_start=self._angle_end - 8, duration=0.6, t="in_out_cubic"
        )
        _angle_back_anim.bind(on_complete=self._start_loop)

        _angle_back_anim.start(self)

    def _reset(self, *args):
        Animation.cancel_all(
            self,
            "_angle_start",
            "_rotation_angle",
            "_angle_end",
            "_alpha",
            "color",
        )
        self._angle_start = 0
        self._angle_end = 0
        self._rotation_angle = 360
        self._alpha = 0

    def _on_determinate_progress(
        self, instance_animation, instance_spinner, value
    ):
        if value == 1:
            self.dispatch("on_determinate_complete")