
from math import sqrt, cos, sin, pi
from collections import ChainMap
from kivy.event import EventDispatcher
from kivy.clock import Clock
from kivy.compat import string_types, iterkeys
from kivy.weakproxy import WeakProxy


class Animation(EventDispatcher):
    '''Create an animation definition that can be used to animate a Widget.
    :Parameters:
        `duration` or `d`: float, defaults to 1.
            Duration of the animation, in seconds.
        `transition` or `t`: str or func
            Transition function for animate properties. It can be the name of a
            method from :class:`AnimationTransition`.
        `step` or `s`: float
            Step in milliseconds of the animation. Defaults to 0, which means
            the animation is updated for every frame.
            To update the animation less often, set the step value to a float.
            For example, if you want to animate at 30 FPS, use s=1/30.
    :Events:
        `on_start`: animation, widget
            Fired when the animation is started on a widget.
        `on_complete`: animation, widget
            Fired when the animation is completed or stopped on a widget.
        `on_progress`: animation, widget, progression
            Fired when the progression of the animation is changing.
    .. versionchanged:: 1.4.0
        Added s/step parameter.
    .. versionchanged:: 1.10.0
        The default value of the step parameter was changed from 1/60. to 0.
    '''

    _update_ev = None

    _instances = set()

    __events__ = ('on_start', 'on_progress', 'on_complete')


    def on_start(self, widget):
        pass

    def on_progress(self, widget, progress):
        pass

    def on_complete(self, widget):
        pass


    def __init__(self, **kw):
        super().__init__()
        # Initialize
        self._clock_installed = False
        self._duration = kw.pop('d', kw.pop('duration', 1.))
        self._transition = kw.pop('t', kw.pop('transition', 'linear'))
        self._step = kw.pop('s', kw.pop('step', 0))
        if isinstance(self._transition, string_types):
            self._transition = getattr(AnimationTransition, self._transition)
        self._animated_properties = kw
        self._widgets = {}

    @property
    def duration(self):
        '''Return the duration of the animation.
        '''
        return self._duration

    @property
    def transition(self):
        '''Return the transition of the animation.
        '''
        return self._transition

    @property
    def animated_properties(self):
        '''Return the properties used to animate.
        '''
        return self._animated_properties

    @staticmethod
    def stop_all(widget, *largs):
        '''Stop all animations that concern a specific widget / list of
        properties.
        Example::
            anim = Animation(x=50)
            anim.start(widget)
            # and later
            Animation.stop_all(widget, 'x')
        '''
        if len(largs):
            for animation in list(Animation._instances):
                for x in largs:
                    animation.stop_property(widget, x)
        else:
            for animation in set(Animation._instances):
                animation.stop(widget)

    @staticmethod
    def cancel_all(widget, *largs):
        '''Cancel all animations that concern a specific widget / list of
        properties. See :attr:`cancel`.
        Example::
            anim = Animation(x=50)
            anim.start(widget)
            # and later
            Animation.cancel_all(widget, 'x')
        .. versionadded:: 1.4.0
        .. versionchanged:: 2.1.0
            If the parameter ``widget`` == None, all animated widgets will be
            the target and cancelled. If ``largs`` is also given, animation of
            these properties will be canceled for all animated widgets.
        '''
        if widget == None:
            if largs:
                for animation in Animation._instances.copy():
                    for info in tuple(animation._widgets.values()):
                        widget = info['widget']
                        for x in largs:
                            animation.cancel_property(widget, x)
            else:
                for animation in Animation._instances:
                    animation._widgets.clear()
                    animation._clock_uninstall()
                Animation._instances.clear()
            return None
        if len(largs):
            for animation in list(Animation._instances):
                for x in largs:
                    animation.cancel_property(widget, x)
        else:
            for animation in set(Animation._instances):
                animation.cancel(widget)

    def start(self, widget):
        '''Start the animation on a widget.
        '''
        self.stop(widget)
        self._initialize(widget)
        self._register()
        self.dispatch('on_start', widget)

    def stop(self, widget):
        '''Stop the animation previously applied to a widget, triggering the
        `on_complete` event.'''
        props = self._widgets.pop(widget.uid, None)
        if props:
            self.dispatch('on_complete', widget)
        self.cancel(widget)

    def cancel(self, widget):
        '''Cancel the animation previously applied to a widget. Same
        effect as :attr:`stop`, except the `on_complete` event will
        *not* be triggered!
        .. versionadded:: 1.4.0
        '''
        self._widgets.pop(widget.uid, None)
        self._clock_uninstall()
        if not self._widgets:
            self._unregister()

    def stop_property(self, widget, prop):
        '''Even if an animation is running, remove a property. It will not be
        animated further. If it was the only/last property being animated,
        the animation will be stopped (see :attr:`stop`).
        '''
        props = self._widgets.get(widget.uid, None)
        if not props:
            return None
        props['properties'].pop(prop, None)

        # no more properties to animation ? kill the animation.
        if not props['properties']:
            self.stop(widget)

    def cancel_property(self, widget, prop):
        '''Even if an animation is running, remove a property. It will not be
        animated further. If it was the only/last property being animated,
        the animation will be canceled (see :attr:`cancel`)
        .. versionadded:: 1.4.0
        '''
        props = self._widgets.get(widget.uid, None)
        if not props:
            return None
        props['properties'].pop(prop, None)

        # no more properties to animation ? kill the animation.
        if not props['properties']:
            self.cancel(widget)

    def have_properties_to_animate(self, widget):
        '''Return True if a widget still has properties to animate.
        .. versionadded:: 1.8.0
        '''
        props = self._widgets.get(widget.uid, None)
        if props and props['properties']:
            return True

    #
    # Private
    #
    def _register(self):
        Animation._instances.add(self)

    def _unregister(self):
        if self in Animation._instances:
            Animation._instances.remove(self)

    def _initialize(self, widget):
        d = self._widgets[widget.uid] = {
            'widget': widget,
            'properties': {},
            'time': None}

        # get current values
        p = d['properties']
        for key, value in self._animated_properties.items():
            original_value = getattr(widget, key)
            if isinstance(original_value, (tuple, list)):
                original_value = original_value[:]
            elif isinstance(original_value, dict):
                original_value = original_value.copy()
            p[key] = (original_value, value)

        # install clock
        self._clock_install()

    def _clock_install(self):
        if self._clock_installed:
            return None
        
        self._update_ev = Clock.schedule_interval(self._update, self._step)
        self._clock_installed = True

    def _clock_uninstall(self):
        if self._widgets or not self._clock_installed:
            return None
        
        self._clock_installed = False
        if self._update_ev != None:
            self._update_ev.cancel()
            self._update_ev = None

    def _update(self, dt):
        widgets = self._widgets
        transition = self._transition
        calculate = self._calculate
        for uid in list(widgets.keys()):
            anim = widgets[uid]
            widget = anim['widget']

            if isinstance(widget, WeakProxy) and not len(dir(widget)):
                # empty proxy, widget is gone. ref: #2458
                self._widgets.pop(uid, None)
                self._clock_uninstall()
                
                if not self._widgets:
                    self._unregister()
                
                continue

            if anim['time'] == None:
                anim['time'] = 0.0
            else:
                anim['time'] += dt

            # calculate progression
            if self._duration:
                progress = min(1.0, anim['time'] / self._duration)
            else:
                progress = 1
            t = transition(progress)

            # apply progression on widget
            for key, values in anim['properties'].items():
                a, b = values
                value = calculate(a, b, t)
                setattr(widget, key, value)

            self.dispatch('on_progress', widget, progress)

            # time to stop ?
            if progress >= 1.0:
                self.stop(widget)

    def _calculate(self, a, b, t):
        _calculate = self._calculate
        
        if isinstance(a, list) or isinstance(a, tuple):
            
            if isinstance(a, list):
                tp = list
            else:
                tp = tuple

            return tp([_calculate(a[x], b[x], t) for x in range(len(a))])
        
        elif isinstance(a, dict):
            
            d = {}
            for x in iterkeys(a):
                if x not in b:
                    # User requested to animate only part of the dict.
                    # Copy the rest
                    d[x] = a[x]
                else:
                    d[x] = _calculate(a[x], b[x], t)
            return d
        else:
            return (a * (1.0 - t)) + (b * t)

    #
    # Default handlers
    #

    def __add__(self, animation):
        return Sequence(self, animation)

    def __and__(self, animation):
        return Parallel(self, animation)


class CompoundAnimation(Animation):

    def stop_property(self, widget, prop):
        self.anim1.stop_property(widget, prop)
        self.anim2.stop_property(widget, prop)
        
        if not self.anim1.have_properties_to_animate(widget) and \
            not self.anim2.have_properties_to_animate(widget):
            self.stop(widget)

    def cancel(self, widget):
        self.anim1.cancel(widget)
        self.anim2.cancel(widget)
        super().cancel(widget)

    def cancel_property(self, widget, prop):
        '''Even if an animation is running, remove a property. It will not be
        animated further. If it was the only/last property being animated,
        the animation will be canceled (see :attr:`cancel`)
        This method overrides `:class:kivy.animation.Animation`'s
        version, to cancel it on all animations of the Sequence.
        .. versionadded:: 1.10.0
        '''
        self.anim1.cancel_property(widget, prop)
        self.anim2.cancel_property(widget, prop)
        if not self.anim1.have_properties_to_animate(widget) and \
            not self.anim2.have_properties_to_animate(widget):
            self.cancel(widget)

    def have_properties_to_animate(self, widget):
        return (self.anim1.have_properties_to_animate(widget) or
                self.anim2.have_properties_to_animate(widget))

    @property
    def animated_properties(self):
        return ChainMap({},
                        self.anim2.animated_properties,
                        self.anim1.animated_properties)

    @property
    def transition(self):
        # This property is impossible to implement
        raise AttributeError(
            "Can't lookup transition attribute of a CompoundAnimation")


class Sequence(CompoundAnimation):

    def on_anim1_complete(self, instance, widget):
        if widget.uid not in self._widgets:
            return None
        
        self.anim2.start(widget)

    def on_anim1_progress(self, instance, widget, progress):
        self.dispatch('on_progress', widget, progress / 2.)

    def on_anim2_complete(self, instance, widget):
        '''Repeating logic used with boolean variable "repeat".
        .. versionadded:: 1.7.1
        '''
        if widget.uid not in self._widgets:
            return None
        
        if self.repeat:
            self.anim1.start(widget)
        else:
            self.dispatch('on_complete', widget)
            self.cancel(widget)

    def on_anim2_progress(self, instance, widget, progress):
        self.dispatch('on_progress', widget, 0.5 + progress / 2.0)

    def __init__(self, anim1, anim2):
        super().__init__()

        #: Repeat the sequence. See 'Repeating animation' in the header
        #: documentation.
        self.repeat = False

        self.anim1 = anim1
        self.anim2 = anim2

        self.anim1.bind(on_complete=self.on_anim1_complete,
                        on_progress=self.on_anim1_progress)
        
        self.anim2.bind(on_complete=self.on_anim2_complete,
                        on_progress=self.on_anim2_progress)

    @property
    def duration(self):
        return self.anim1.duration + self.anim2.duration

    def stop(self, widget):
        props = self._widgets.pop(widget.uid, None)
        self.anim1.stop(widget)
        self.anim2.stop(widget)
        
        if props:
            self.dispatch('on_complete', widget)
        
        super().cancel(widget)

    def start(self, widget):
        self.stop(widget)
        self._widgets[widget.uid] = True
        self._register()
        self.dispatch('on_start', widget)
        self.anim1.start(widget)


class Parallel(CompoundAnimation):

    def on_anim_complete(self, instance, widget):
        self._widgets[widget.uid]['complete'] += 1
        
        if self._widgets[widget.uid]['complete'] == 2:
            self.stop(widget)
    
    def on_anim_progress(self, instance, widget, progress):
        self.dispatch('on_progress', widget, progress)

    def __init__(self, anim1, anim2):
        super().__init__()
        self.anim1 = anim1
        self.anim2 = anim2

        self.anim1.bind(on_complete=self.on_anim_complete)
        self.anim2.bind(on_complete=self.on_anim_complete)

        if self.anim1.duration > self.anim2.duration:
            self.anim1.bind(on_progress=self.on_anim_progress)
        else:
            self.anim2.bind(on_progress=self.on_anim_progress)

    @property
    def duration(self):
        return max(self.anim1.duration, self.anim2.duration)

    def stop(self, widget):
        self.anim1.stop(widget)
        self.anim2.stop(widget)
        
        if self._widgets.pop(widget.uid, None):
            self.dispatch('on_complete', widget)
        
        super().cancel(widget)

    def start(self, widget):
        self.stop(widget)
        self.anim1.start(widget)
        self.anim2.start(widget)
        self._widgets[widget.uid] = {'complete': 0}
        self._register()
        self.dispatch('on_start', widget)



class AnimationTransition():
    '''Collection of animation functions to be used with the Animation object.
    Easing Functions ported to Kivy from the Clutter Project
    https://developer.gnome.org/clutter/stable/ClutterAlpha.html
    The `progress` parameter in each animation function is in the range 0-1.
    '''

    @staticmethod
    def linear(progress):
        '''.. image:: images/anim_linear.png'''
        return progress

    @staticmethod
    def in_quad(progress):
        '''.. image:: images/anim_in_quad.png
        '''
        return (progress * progress)

    @staticmethod
    def out_quad(progress):
        '''.. image:: images/anim_out_quad.png
        '''
        return ( -1.0 * progress * (progress - 2.0) )

    @staticmethod
    def in_out_quad(progress):
        '''.. image:: images/anim_in_out_quad.png
        '''
        p = (progress * 2)
        if p < 1:
            return 0.5 * p * p
        p -= 1.0
        return ( -0.5 * (p * (p - 2.0) - 1.0) )

    @staticmethod
    def in_cubic(progress):
        '''.. image:: images/anim_in_cubic.png
        '''
        return (progress * progress * progress)

    @staticmethod
    def out_cubic(progress):
        '''.. image:: images/anim_out_cubic.png
        '''
        p = progress - 1.0
        return (p * p * p + 1.0)

    @staticmethod
    def in_out_cubic(progress):
        '''.. image:: images/anim_in_out_cubic.png
        '''
        p = progress * 2
        if p < 1:
            return (0.5 * p * p * p)
        p -= 2
        return ( 0.5 * (p * p * p + 2.0) )

    @staticmethod
    def in_quart(progress):
        '''.. image:: images/anim_in_quart.png
        '''
        return (progress * progress * progress * progress)

    @staticmethod
    def out_quart(progress):
        '''.. image:: images/anim_out_quart.png
        '''
        p = progress - 1.0
        return ( -1.0 * (p * p * p * p - 1.0) )

    @staticmethod
    def in_out_quart(progress):
        '''.. image:: images/anim_in_out_quart.png
        '''
        p = progress * 2
        if p < 1:
            return (0.5 * p * p * p * p)
        p -= 2
        return ( -0.5 * (p * p * p * p - 2.0) )

    @staticmethod
    def in_quint(progress):
        '''.. image:: images/anim_in_quint.png
        '''
        return (progress * progress * progress * progress * progress)

    @staticmethod
    def out_quint(progress):
        '''.. image:: images/anim_out_quint.png
        '''
        p = progress - 1.0
        return (p * p * p * p * p + 1.0)

    @staticmethod
    def in_out_quint(progress):
        '''.. image:: images/anim_in_out_quint.png
        '''
        p = progress * 2
        if p < 1:
            return (0.5 * p * p * p * p * p)
        p -= 2.0
        return ( 0.5 * (p * p * p * p * p + 2.0) )

    @staticmethod
    def in_sine(progress):
        '''.. image:: images/anim_in_sine.png
        '''
        return (-1.0 * cos(progress * (pi / 2.0)) + 1.0)

    @staticmethod
    def out_sine(progress):
        '''.. image:: images/anim_out_sine.png
        '''
        return sin(progress * (pi / 2.0))

    @staticmethod
    def in_out_sine(progress):
        '''.. image:: images/anim_in_out_sine.png
        '''
        return ( -0.5 * (cos(pi * progress) - 1.0) )

    @staticmethod
    def in_expo(progress):
        '''.. image:: images/anim_in_expo.png
        '''
        if progress == 0:
            return 0.0
        return pow(2, 10 * (progress - 1.0))

    @staticmethod
    def out_expo(progress):
        '''.. image:: images/anim_out_expo.png
        '''
        if progress == 1.0:
            return 1.0
        return -pow(2, -10 * progress) + 1.0

    @staticmethod
    def in_out_expo(progress):
        '''.. image:: images/anim_in_out_expo.png
        '''
        if progress == 0:
            return 0.0
        
        if progress == 1.0:
            return 1.0
        
        p = progress * 2
        if p < 1:
            return ( 0.5 * pow(2, 10 * (p - 1.0)) )
        
        p -= 1.0
        return ( 0.5 * (-pow(2, -10 * p) + 2.0) )

    @staticmethod
    def in_circ(progress):
        '''.. image:: images/anim_in_circ.png
        '''
        return ( -1.0 * (sqrt(1.0 - progress * progress) - 1.0) )

    @staticmethod
    def out_circ(progress):
        '''.. image:: images/anim_out_circ.png
        '''
        p = progress - 1.0
        return sqrt(1.0 - p * p)

    @staticmethod
    def in_out_circ(progress):
        '''.. image:: images/anim_in_out_circ.png
        '''
        p = progress * 2
        if p < 1:
            return ( -0.5 * (sqrt(1.0 - p * p) - 1.0) )
        
        p -= 2.0
        return ( 0.5 * (sqrt(1.0 - p * p) + 1.0) )

    @staticmethod
    def in_elastic(progress):
        '''.. image:: images/anim_in_elastic.png
        '''
        p = .3
        s = p / 4.0
        q = progress
        if q == 1:
            return 1.0
        q -= 1.0
        return -(pow(2, 10 * q) * sin((q - s) * (2 * pi) / p))

    @staticmethod
    def out_elastic(progress):
        '''.. image:: images/anim_out_elastic.png
        '''
        p = 0.3
        s = p / 4.0
        q = progress
        if q == 1:
            return 1.0
        return (pow(2, -10 * q) * sin((q - s) * (2 * pi) / p) + 1.0)

    @staticmethod
    def in_out_elastic(progress):
        '''.. image:: images/anim_in_out_elastic.png
        '''
        p = .3 * 1.5
        s = p / 4.0
        q = progress * 2
        if q == 2:
            return 1.0
        if q < 1:
            q -= 1.0
            return ( -0.5 * (pow(2, 10 * q) * sin((q - s) * (2.0 * pi) / p)) )
        else:
            q -= 1.0
            return (pow(2, -10 * q) * sin((q - s) * (2.0 * pi) / p) * 0.5 + 1.0)

    @staticmethod
    def in_back(progress):
        '''.. image:: images/anim_in_back.png
        '''
        return ( progress * progress * ((1.70158 + 1.0) * progress - 1.70158) )

    @staticmethod
    def out_back(progress):
        '''.. image:: images/anim_out_back.png
        '''
        p = progress - 1.0
        return (p * p * ((1.70158 + 1) * p + 1.70158) + 1.0)

    @staticmethod
    def in_out_back(progress):
        '''.. image:: images/anim_in_out_back.png
        '''
        p = progress * 2.
        s = 1.70158 * 1.525
        if p < 1:
            return ( 0.5 * (p * p * ((s + 1.0) * p - s)) )
        
        p -= 2.0
        return ( 0.5 * (p * p * ((s + 1.0) * p + s) + 2.0) )

    @staticmethod
    def _out_bounce_internal(t, d):
        p = t / d

        if p < (1.0 / 2.75):
            return (7.5625 * p * p)
        
        elif p < (2.0 / 2.75):
            p -= (1.5 / 2.75)
            return (7.5625 * p * p + 0.75)
        
        elif p < (2.5 / 2.75):
            p -= (2.25 / 2.75)
            return (7.5625 * p * p + 0.9375)
        
        else:
            p -= (2.625 / 2.75)
            return (7.5625 * p * p + 0.984375)

    @staticmethod
    def _in_bounce_internal(t, d):
        return ( 1.0 - AnimationTransition._out_bounce_internal(d - t, d) )

    @staticmethod
    def in_bounce(progress):
        '''.. image:: images/anim_in_bounce.png
        '''
        return AnimationTransition._in_bounce_internal(progress, 1.0)

    @staticmethod
    def out_bounce(progress):
        '''.. image:: images/anim_out_bounce.png
        '''
        return AnimationTransition._out_bounce_internal(progress, 1.0)

    @staticmethod
    def in_out_bounce(progress):
        '''.. image:: images/anim_in_out_bounce.png
        '''
        p = progress * 2.0
        if p < 1.0:
            return ( AnimationTransition._in_bounce_internal(p, 1.0) * 0.5 )
        
        return ( AnimationTransition._out_bounce_internal(p - 1.0, 1.0) * 0.5 + 0.5 )
