# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.

from elisa.plugins.pigment.animation.animation import Animation
from elisa.plugins.pigment.animation.animation import FORWARD, REVERSE, LINEAR, ACCELERATE, DECELERATE, SMOOTH, INFINITE
from elisa.plugins.pigment.animation.modifier import Modifier

# Implicit mode
REPLACE, APPEND = 0, 1

class AnimatedObject(object):
    """
    This is a proxy to any object that would like some of its attributes to be
    implicitly animated when set.

    @ivar passthrough: whether or not it is acting as a passthrough, that is
                       not animating proxied object's instance variables;
                       False by default
    @type passthrough: C{bool}
    """

    def __init__(self, obj, animated_attributes=None):
        """
        @param animated_attributes: backward compatibility only

        @param obj:   object to be proxied and implicitly animated
        @type obj:    object
        """
        self._set_local_attribute("object", obj)
        self._set_local_attribute("passthrough", False)
        self._set_local_attribute("_animations", {})
        self._set_local_attribute("_duration", 1000)
        self._set_local_attribute("_repeat", False)
        self._set_local_attribute("_repeat_behavior", FORWARD)
        self._set_local_attribute("_transformation", LINEAR)
        self._set_local_attribute("_end_callback", None)
        # _next_end_callback is an end callback that will be set to run at the
        # end of the next animation to be created.
        self._set_local_attribute("_next_end_callback", None)
        self._set_local_attribute("_end_time", 0)
        self._set_local_attribute("_end_time_animation", None)
        self._set_local_attribute("mode", None)

        # queue of (attribute, target_value) couples that need to be appended
        # after the animation is finished
        self._set_local_attribute("_appended_queue", [])

    def _set_local_attribute(self, attribute, value):
        object.__setattr__(self, attribute, value)

    def _end_animation_handler(self, animation):
        #NOTE: we consider that end_callback is to be called at the end of
        # the *current* animation, not at the end of all the animations that
        # could be appended. This might be wrong, but works well for our use
        # case.
        if self._end_callback != None:
            self._end_callback("dummy")
            self._end_callback = None
        if self._appended_queue != []:
            #FIXME: this won't work very predictably if some animation
            # parameters got changed since the time when (attribute,
            # target_value) got added.
            attribute, target_value = self._appended_queue.pop(0)
            self._animate_attribute_to(attribute, target_value)

        self._set_next_callback()

    def _set_next_callback(self):
        if self._next_end_callback is not None:
            self._end_callback = self._next_end_callback
            self._next_end_callback = None

    def _need_animation(self, animation, current_value, target_value):
        # Return True if an animation should be triggered, False if it's an
        # animation that would have no visual effect.

        if animation is not None and animation.started:
            # If there's an ongoing animation on this attribute, there's a
            # 0.0001% chance that it's gonna do the exact same thing that we
            # wanna do
            return True
        
        # There's no ongoing animation, it's all about whether our
        # animation would make us go where we already are, with no
        # intermediate point. Note that target value lists are handled by this
        # formula.
        return target_value != current_value



    def _animate_attribute_to(self, attribute, target_value):
        animation = self._animations.get(attribute, None)
        current_value = getattr(self.object, attribute)

        if not self._need_animation(animation, current_value, target_value):
            return
        
        if animation is not None:
            modifier = animation.modifiers[0]

            if self.mode == APPEND and animation.started:
                self._append_animate_attribute_to(attribute, target_value)
            else: # REPLACE
                if animation.started:
                    animation.stop()
                    # In this case, _set_next_callback() will be called in the
                    # _end_animation_handler() call triggered by
                    # animation.stop().  We cannot call it yet because
                    # _end_animation_handler() will be called *after* we
                    # return, because of a callFromThread().
                else:
                    self._set_next_callback()

                modifier.initial_value = current_value
                modifier.target_value = target_value
                animation.duration = self._duration
                animation.repeat = self._repeat
                animation.repeat_behavior = self._repeat_behavior
                animation.transformation = self._transformation
                animation.start()
        else:
            modifier = Modifier(self.object, attribute, current_value,
                                target_value)
            animation = Animation([modifier])
            animation.duration = self._duration
            animation.repeat = self._repeat
            animation.repeat_behavior = self._repeat_behavior
            animation.transformation = self._transformation
            self._animations[attribute] = animation
            self._set_next_callback()
            animation.start()

        # transfer the end_callback to the animation that will end last
        if self._end_time_animation != animation:
            # FIXME: access private variable
            end_time = animation.duration + animation._start_time
            if end_time > self._end_time:
                if self._end_time_animation != None:
                    self._end_time_animation.end_callback = None
                animation.end_callback = self._end_animation_handler
                self._end_time_animation = animation
    
    def _append_animate_attribute_to(self, attribute, target_value):
        """
        This is the implementation of the APPEND mode: it will trigger a new
        animation once this one is finished
        """
        self._appended_queue.append((attribute, target_value))

    def __setattr__(self, attribute, value):
        if self.__dict__.has_key(attribute):
            object.__setattr__(self, attribute, value)
        elif not self.passthrough:
            # animate the attribute to value
            self._animate_attribute_to(attribute, value)
        else:
            # set it directly to the proxied object
            setattr(self.object, attribute, value)

    def __getattr__(self, attribute):
        # for any attribute accessed not found locally
        # retrieve it from the proxied object

        animation = self._animations.get(attribute)
        if animation != None and animation.started:
            # if an animated attribute is to be retrieved, do not return the
            # current value but the target value
            return animation.modifiers[0].target_value
        else:
            # return the current value
            return getattr(self.object, attribute)


    # backward compatibility layer
    def setup_next_animations(self, duration=None, repeat_behavior=None,
                              repeat_count=None, transformation=None,
                              end_callback=-1):

        if duration != None:
            self._duration = duration
        if repeat_count != None:
            # repeat_count=-1 <=> INFINITE
            self._repeat = (repeat_count == INFINITE)
        if transformation != None:
            self._transformation = transformation
        if end_callback != -1:
            self._next_end_callback = end_callback
        if repeat_behavior is not None:
            self._repeat_behavior = repeat_behavior

    def update_animation_settings(self, duration=None, repeat_behavior=None,
                                  repeat_count=None, transformation=None,
                                  end_callback=-1):

        for animation in self._animations.itervalues():
            if duration != None:
                animation.duration = duration
            if repeat_count != None:
                # repeat_count=-1 <=> INFINITE
                animation.repeat = (repeat_count == -1)
            if transformation != None:
                animation.transformation = transformation
            if repeat_behavior is not None:
                animation.repeat_behavior = repeat_behavior

        if end_callback != -1:
            self._end_callback = end_callback

        self.setup_next_animations(duration, repeat_behavior, repeat_count,
                                   transformation)

    def stop_animations(self):
        for animation in self._animations.itervalues():
            animation.stop()

    def is_animated(self, attribute=None):
        """
        Returns True if L{attribute} is currently being animated, False
        otherwise.
        If no attribute is specified, return True if any of the attributes is
        currently animated.

        @param attribute: attribute to check for
        @type attribute:  string
        @rtype: boolean
        """
        if attribute is not None:
            animation = self._animations.get(attribute, None)
            if animation is not None:
                return animation.started
        else:
            for animation in self._animations.itervalues():
                if animation.started:
                    return True
        return False

    def stop_animation_for_attribute(self, attribute):
        """
        Stop the animation for a given attribute.

        @param attribute: attribute for which animations should be stopped
        @type attribute:  string
        """
        animation = self._animations.get(attribute, None)
        if animation is not None:
            animation.stop()

