"""
Components/RouletteScrollEffect
===============================

This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the
motion of a roulette, or a notched wheel (think Wheel of Fortune). It is
primarily designed for emulating the effect of the iOS and android date pickers.

Usage
-----

Here's an example of using :class:`RouletteScrollEffect` for a
:class:`kivy.uix.scrollview.ScrollView`::

.. code-block:: python

    from kivy.uix.gridlayout import GridLayout
    from kivy.uix.button import Button
    from kivy.uix.scrollview import ScrollView

    # Preparing a `GridLayout` inside a `ScrollView`.
    layout = GridLayout(cols=1, padding=10, size_hint=(None, None), width=500)
    layout.bind(minimum_height=layout.setter('height'))

    for i in range(30):
        btn = Button(text=str(i), size=(480, 40), size_hint=(None, None))
        layout.add_widget(btn)

    root = ScrollView(
        size_hint=(None, None),
        size=(500, 320),
        pos_hint={'center_x': .5, 'center_y': .5},
        do_scroll_x=False,
        )
    root.add_widget(layout)

    # Preparation complete. Now add the new scroll effect.
    root.effect_y = RouletteScrollEffect(anchor=20, interval=40)
    runTouchApp(root)

Here the :class:`ScrollView` scrolls through a series of buttons with height
40. We then attached a :class:`RouletteScrollEffect` with interval 40,
corresponding to the button heights. This allows the scrolling to stop at
the same offset no matter where it stops. The :attr:`RouletteScrollEffect.anchor`
adjusts this offset.

Customizations
--------------

Other settings that can be played with include:

:attr:`RouletteScrollEffect.pull_duration`,
:attr:`RouletteScrollEffect.coasting_alpha`,
:attr:`RouletteScrollEffect.pull_back_velocity`, and
:attr:`RouletteScrollEffect.terminal_velocity`.

See their module documentations for details.

:class:`RouletteScrollEffect` has one event ``on_coasted_to_stop`` that
is fired when the roulette stops, "making a selection". It can be listened to
for handling or cleaning up choice making.
"""

__all_ = ("RouletteScrollEffect",)

from math import ceil, exp, floor

from kivy.animation import Animation
from kivy.effects.scroll import ScrollEffect
from kivy.properties import AliasProperty, NumericProperty, ObjectProperty


class RouletteScrollEffect(ScrollEffect):
    """
    This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the
    motion of a roulette, or a notched wheel (think Wheel of Fortune). It is
    primarily designed for emulating the effect of the iOS and android date pickers.

    .. versionadded:: 0.104.2
    """

    __events__ = ("on_coasted_to_stop",)

    drag_threshold = NumericProperty(0)
    """
    Overrides :attr:`ScrollEffect.drag_threshold` to abolish drag threshold.
    
    .. note::
        If using this with a :class:`Roulette` or other :class:`Tickline`
        subclasses, what matters is :attr:`Tickline.drag_threshold`, which
        is passed to this attribute in the end.

    :attr:`drag_threshold` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    min = NumericProperty(-float("inf"))
    max = NumericProperty(float("inf"))

    interval = NumericProperty(50)
    """
    The interval of the values of the "roulette".

    :attr:`interval` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `50`.
    """

    anchor = NumericProperty(0)
    """
    One of the valid stopping values.

    :attr:`anchor` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    pull_duration = NumericProperty(0.2)
    """
    When movement slows around a stopping value, an animation is used
    to pull it toward the nearest value. :attr:`pull_duration` is the duration
    used for such an animation.

    :attr:`pull_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    coasting_alpha = NumericProperty(0.5)
    """
    When within :attr:`coasting_alpha` * :attr:`interval` of the
    next notch and velocity is below :attr:`terminal_velocity`, 
    coasting begins and will end on the next notch.

    :attr:`coasting_alpha` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.5`.
    """

    pull_back_velocity = NumericProperty("50sp")
    """
    The velocity below which the scroll value will be drawn to the 
    *nearest* notch instead of the *next* notch in the direction travelled.
    
    :attr:`pull_back_velocity` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `50sp`.
    """

    _anim = ObjectProperty(None)

    def get_term_vel(self):
        return (
            exp(self.friction)
            * self.interval
            * self.coasting_alpha
            / self.pull_duration
        )

    def set_term_vel(self, val):
        self.pull_duration = (
            exp(self.friction) * self.interval * self.coasting_alpha / val
        )

    terminal_velocity = AliasProperty(
        get_term_vel,
        set_term_vel,
        bind=["interval", "coasting_alpha", "pull_duration", "friction"],
        cache=True,
    )
    """
    If velocity falls between :attr:`pull_back_velocity` and 
    :attr:`terminal velocity` then the movement will start to coast
    to the next coming stopping value.
    
    :attr:`terminal_velocity` is computed from a set formula given
    :attr:`interval`, :attr:`coasting_alpha`, :attr:`pull_duration`,
    and :attr:`friction`. Setting :attr:`terminal_velocity` has the
    effect of setting :attr:`pull_duration`.
    """

    def start(self, val, t=None):
        if self._anim:
            self._anim.stop(self)
        return ScrollEffect.start(self, val, t=t)

    def on_notch(self, *args):
        return (self.scroll - self.anchor) % self.interval == 0

    def nearest_notch(self, *args):
        interval = float(self.interval)
        anchor = self.anchor
        n = round((self.scroll - anchor) / interval)
        return anchor + n * interval

    def next_notch(self, *args):
        interval = float(self.interval)
        anchor = self.anchor
        round_ = ceil if self.velocity > 0 else floor
        n = round_((self.scroll - anchor) / interval)
        return anchor + n * interval

    def near_notch(self, d=0.01):
        nearest = self.nearest_notch()
        if abs((nearest - self.scroll) / self.interval) % 1 < d:
            return nearest
        else:
            return None

    def near_next_notch(self, d=None):
        d = d or self.coasting_alpha
        next_ = self.next_notch()
        if abs((next_ - self.scroll) / self.interval) % 1 < d:
            return next_
        else:
            return None

    def update_velocity(self, dt):
        if self.is_manual:
            return
        velocity = self.velocity
        t_velocity = self.terminal_velocity
        next_ = self.near_next_notch()
        pull_back_velocity = self.pull_back_velocity
        if pull_back_velocity < abs(velocity) < t_velocity and next_:
            duration = abs((next_ - self.scroll) / self.velocity)
            anim = Animation(
                scroll=next_,
                duration=duration,
            )
            self._anim = anim
            anim.on_complete = self._coasted_to_stop
            anim.start(self)
            return
        if abs(velocity) < pull_back_velocity and not self.on_notch():
            anim = Animation(
                scroll=self.nearest_notch(),
                duration=self.pull_duration,
                t="in_out_circ",
            )
            self._anim = anim
            anim.on_complete = self._coasted_to_stop
            anim.start(self)
        else:
            self.velocity -= self.velocity * self.friction
            self.apply_distance(self.velocity * dt)
            self.trigger_velocity_update()

    def on_coasted_to_stop(self, *args):
        """
        This event fires when the roulette has stopped, `making a selection`.
        """

    def _coasted_to_stop(self, *args):
        self.velocity = 0
        self.dispatch("on_coasted_to_stop")
