'''
Kinetic effect
==============

.. versionadded:: 1.7.0

The :class:`KineticEffect` is the base class that is used to compute the
velocity out of a movement. When the movement is finished, the effect will
compute the position of the movement according to the velocity, and reduce the
velocity with a friction. The movement stop until the velocity is 0.

Conceptually, the usage could be::

    >>> effect = KineticEffect()
    >>> effect.start(10)
    >>> effect.update(15)
    >>> effect.update(30)
    >>> effect.stop(48)

Over the time, you will start a movement of a value, update it, and stop the
movement. At this time, you'll get the movement value into
:data:`KineticEffect.value`. On the example i've typed manually, the computed
velocity will be::

    >>> effect.velocity
    3.1619100231163046

After multiple clock interaction, the velocity will decrease according to
:data:`KineticEffect.friction`. The computed value will be stored in
:data:`KineticEffect.value`. The output of this `value` could be::

    46.30038145219605
    54.58302451968686
    61.9229016256196
    # ...

'''

__all__ = ('KineticEffect', )


from time import time
from kivy.event import EventDispatcher
from kivy.properties import NumericProperty, BooleanProperty
from kivy.clock import Clock


class KineticEffect(EventDispatcher):
    '''Kinetic effect class. See module documentation for more information.
    '''

    velocity = NumericProperty(0)
    '''Velocity of the movement.

    :data:`velocity` is a :class:`~kivy.properties.NumericProperty`, default to
    0
    '''

    friction = NumericProperty(0.05)
    '''Friction to apply on the velocity

    :data:`velocity` is a :class:`~kivy.properties.NumericProperty`, default to
    0.05
    '''

    value = NumericProperty(0)
    '''Value (during the movement and computed) of the effect.

    :data:`velocity` is a :class:`~kivy.properties.NumericProperty`, default to
    0
    '''

    is_manual = BooleanProperty(False)
    '''Indicate if a movement is in progress (True) or not (False).

    :data:`velocity` is a :class:`~kivy.properties.BooleanProperty`, default to
    False
    '''

    max_history = NumericProperty(5)
    '''Save up to `max_history` movement value into the history. This is used
    for correctly calculating the velocity according to the movement.

    :data:`max_history` is a :class:`~kivy.properties.NumericProperty`, default
    to 5.
    '''

    def __init__(self, **kwargs):
        self.history = []
        self.trigger_velocity_update = Clock.create_trigger(self.update_velocity, 0)
        super(KineticEffect, self).__init__(**kwargs)

    def apply_distance(self, distance):
        if abs(distance) < 0.1:
            self.velocity = 0
        self.value += distance

    def start(self, val, t=None):
        '''Start the movement.

        :Parameters:
            `val`: float or int
                Value of the movement
            `t`: float, default to None
                Time when the movement happen. If no time is set, it will use
                time.time()
        '''
        self.is_manual = True
        t = t or time()
        self.velocity = 0
        self.history = [(t, val)]

    def update(self, val, t=None):
        '''Update the movement.

        See :meth:`start` for the arguments.
        '''
        t = t or time()
        distance = val - self.history[-1][1]
        self.apply_distance(distance)
        self.history.append((t, val))
        if len(self.history) > self.max_history:
            self.history.pop(0)

    def stop(self, val, t=None):
        '''Stop the movement.

        See :meth:`start` for the arguments.
        '''
        self.is_manual = False
        t = t or time()
        distance = val - self.history[-1][1]
        self.apply_distance(distance)
        newest_sample = (t, val)
        old_sample = self.history[0]
        for sample in self.history:
            if (newest_sample[0] - sample[0]) < 10. / 60.:
                break
            old_sample = sample
        distance = newest_sample[1] - old_sample[1]
        duration = abs(newest_sample[0] - old_sample[0])
        self.velocity = (distance / max(duration, 0.0001))
        self.trigger_velocity_update()

    def cancel(self):
        '''Cancel a movement. This can be used in case of :meth:`stop` cannot be
        called. It will reset :data:`is_manual` to False, and compute the
        movement if the velocity is > 0.
        '''
        self.is_manual = False
        self.trigger_velocity_update()

    def update_velocity(self, dt):
        '''(internal) Update the velocity according to a frametime and the
        friction.
        '''
        if abs(self.velocity) <= 0.5:
            self.velocity = 0
            return

        self.velocity -= self.velocity * self.friction
        self.apply_distance(self.velocity * dt)
        self.trigger_velocity_update()

