"""
Effects/StiffScrollEffect
=========================

An Effect to be used with ScrollView to prevent scrolling beyond
the bounds, but politely.

A ScrollView constructed with StiffScrollEffect,
eg. ScrollView(effect_cls=StiffScrollEffect), will get harder to
scroll as you get nearer to its edges. You can scroll all the way to
the edge if you want to, but it will take more finger-movement than
usual.

Unlike DampedScrollEffect, it is impossible to overscroll with
StiffScrollEffect. That means you cannot push the contents of the
ScrollView far enough to see what's beneath them. This is appropriate
if the ScrollView contains, eg., a background image, like a desktop
wallpaper. Overscrolling may give the impression that there is some
reason to overscroll, even if just to take a peek beneath, and that
impression may be misleading.

StiffScrollEffect was written by Zachary Spector. His other stuff is at:
https://github.com/LogicalDash/
He can be reached, and possibly hired, at:
zacharyspector@gmail.com

"""

from time import time

from kivy.animation import AnimationTransition
from kivy.effects.kinetic import KineticEffect
from kivy.properties import NumericProperty, ObjectProperty
from kivy.uix.widget import Widget


class StiffScrollEffect(KineticEffect):
    drag_threshold = NumericProperty("20sp")
    """Minimum distance to travel before the movement is considered as a
    drag.

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

    min = NumericProperty(0)
    """Minimum boundary to stop the scrolling at.

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

    max = NumericProperty(0)
    """Maximum boundary to stop the scrolling at.

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

    max_friction = NumericProperty(1)
    """How hard should it be to scroll, at the worst?

    :attr:`max_friction` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `1`.
    """

    body = NumericProperty(0.7)
    """Proportion of the range in which you can scroll unimpeded.

    :attr:`body` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.7`.
    """

    scroll = NumericProperty(0.0)
    """Computed value for scrolling

    :attr:`scroll` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.0`.
    """

    transition_min = ObjectProperty(AnimationTransition.in_cubic)
    """The AnimationTransition function to use when adjusting the friction
    near the minimum end of the effect.

    :attr:`transition_min` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to :class:`kivy.animation.AnimationTransition`.
    """

    transition_max = ObjectProperty(AnimationTransition.in_cubic)
    """The AnimationTransition function to use when adjusting the friction
    near the maximum end of the effect.

    :attr:`transition_max` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to :class:`kivy.animation.AnimationTransition`.
    """

    target_widget = ObjectProperty(None, allownone=True, baseclass=Widget)
    """The widget to apply the effect to.

    :attr:`target_widget` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to ``None``.
    """

    displacement = NumericProperty(0)
    """The absolute distance moved in either direction.

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

    def __init__(self, **kwargs):
        """Set ``self.base_friction`` to the value of ``self.friction`` just
        after instantiation, so that I can reset to that value later.
        """

        super().__init__(**kwargs)
        self.base_friction = self.friction

    def update_velocity(self, dt):
        """Before actually updating my velocity, meddle with ``self.friction``
        to make it appropriate to where I'm at, currently.
        """

        hard_min = self.min
        hard_max = self.max
        if hard_min > hard_max:
            hard_min, hard_max = hard_max, hard_min

        margin = (1.0 - self.body) * (hard_max - hard_min)
        soft_min = hard_min + margin
        soft_max = hard_max - margin

        if self.value < soft_min:
            try:
                prop = (soft_min - self.value) / (soft_min - hard_min)
                self.friction = self.base_friction + abs(
                    self.max_friction - self.base_friction
                ) * self.transition_min(prop)
            except ZeroDivisionError:
                pass
        elif self.value > soft_max:
            try:
                # normalize how far past soft_max I've gone as a
                # proportion of the distance between soft_max and hard_max
                prop = (self.value - soft_max) / (hard_max - soft_max)
                self.friction = self.base_friction + abs(
                    self.max_friction - self.base_friction
                ) * self.transition_min(prop)
            except ZeroDivisionError:
                pass
        else:
            self.friction = self.base_friction

        return super().update_velocity(dt)

    def on_value(self, *args):
        """Prevent moving beyond my bounds, and update ``self.scroll``"""

        if self.value > self.min:
            self.velocity = 0
            self.scroll = self.min
        elif self.value < self.max:
            self.velocity = 0
            self.scroll = self.max
        else:
            self.scroll = self.value

    def start(self, val, t=None):
        """Start movement with ``self.friction`` = ``self.base_friction``"""

        self.is_manual = True
        t = t or time()
        self.velocity = self.displacement = 0
        self.friction = self.base_friction
        self.history = [(t, val)]

    def update(self, val, t=None):
        """Reduce the impact of whatever change has been made to me, in
        proportion with my current friction.
        """

        t = t or time()
        hard_min = self.min
        hard_max = self.max
        if hard_min > hard_max:
            hard_min, hard_max = hard_max, hard_min

        gamut = hard_max - hard_min
        margin = (1.0 - self.body) * gamut
        soft_min = hard_min + margin
        soft_max = hard_max - margin
        distance = val - self.history[-1][1]
        reach = distance + self.value

        if (distance < 0 and reach < soft_min) or (
            distance > 0 and soft_max < reach
        ):
            distance -= distance * self.friction
        self.apply_distance(distance)
        self.history.append((t, val))

        if len(self.history) > self.max_history:
            self.history.pop(0)
        self.displacement += abs(distance)
        self.trigger_velocity_update()

    def stop(self, val, t=None):
        """Work out whether I've been flung."""

        self.is_manual = False
        self.displacement += abs(val - self.history[-1][1])
        if self.displacement <= self.drag_threshold:
            self.velocity = 0

        return super().stop(val, t)