# -*- mode: python; coding: utf-8 -*-
#
# Pigment Python tools
#
# Copyright © 2006, 2007, 2008 Fluendo Embedded S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

import os, pgm, gobject
from pgm.timing import controller, modifier, keyframe
from pgm.utils import classinit


# Repeat behavior constants
FORWARD, REVERSE = 0, 1
# Repeat constant
INFINITE = -1
# Transformation constants
LINEAR, ACCELERATE, DECELERATE, SMOOTH = 5, 6, 7, 8
# Implicit mode
REPLACE, APPEND = 0, 1

# helper for implicitly animating pgm.Drawables
DRAWABLE_ATTRIBUTES = ("size",
                       "width",
                       "height",
                       "position",
                       "x",
                       "y",
                       "z",
                       "rx",
                       "ry",
                       "rz",
                       "s",
                       "fg_color",
                       "fg_r",
                       "fg_g",
                       "fg_b",
                       "fg_a",
                       "bg_color",
                       "bg_r",
                       "bg_g",
                       "bg_b",
                       "bg_a",
                       "opacity")


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 the object is animating proxied object's
                       variables; False by default
    @type passthrough: bool
    @ivar _animated_attributes: attributes of the proxied object that will be
                                implicitly animated
    @type _animated_attributes: tuple of strings
    @ivar _object:              object that is proxied and implicitly animated
    @type _object:              mutable object
    @ivar _current_controllers: controllers of the currently animated attributes
    @type _current_controllers: dict of L{pgm.timing.controller.Controller}
    @ivar _next_controllers:    controllers that will be used next time the
                                attributes are animated
    @type _next_controllers:    dict of L{pgm.timing.controller.Controller}

    """

    __metaclass__ = classinit.ClassInitMeta
    __classinit__ = classinit.build_properties

    def __init__(self, object, animated_attributes=DRAWABLE_ATTRIBUTES):
        """
        Initialize the controllers for the next implicit animations that will
        happen on object.

        @param object:              object to be proxied and implicitly
                                    animated
        @type object:               mutable object
        @param animated_attributes: attributes of the proxied object that will
                                    be implicitly animated
        @type animated_attributes:  tuple of strings
        """

        super(AnimatedObject, self).__init__()

        self._object = object

        self._current_controllers = {}
        self._next_controllers = {}
        self._animated_attributes = ()

        # create default controllers
        for attribute in animated_attributes:
            self.add_animated_attribute(attribute)

        self.mode = REPLACE
        self.passthrough = False

    def static_object__get(self):
        return self._object

    def setup_next_animations(self, attribute=None, duration=1000, resolution=17,
                              repeat_behavior=FORWARD, repeat_count=1,
                              end_behavior=controller.HOLD,
                              transformation=LINEAR, end_callback=None):
        """
        Setup the next implicit animations for L{attribute}.

        @param attribute:  attribute for which the animation is setup
        @type attribute:   string
        @param resolution: resolution of the implicit animation
        @type resolution:  float
        @param duration:   duration of the implicit animation
        @type duration:    int

        FIXME: documentation
        """
        if attribute != None:
            ctrl = controller.Controller(duration        = duration,
                                         resolution      = resolution,
                                         repeat_behavior = repeat_behavior,
                                         repeat_count    = repeat_count,
                                         end_behavior    = end_behavior,
                                         transformation  = transformation,
                                         modifiers       = [],
                                         end_callback    = end_callback)
            self._next_controllers[attribute] = ctrl
        else:
            for attribute in self._animated_attributes:
                ctrl = controller.Controller(duration        = duration,
                                             resolution      = resolution,
                                             repeat_behavior = repeat_behavior,
                                             repeat_count    = repeat_count,
                                             end_behavior    = end_behavior,
                                             transformation  = transformation,
                                             modifiers       = [],
                                             end_callback    = end_callback)
                self._next_controllers[attribute] = ctrl

    def update_animation_settings(self, attribute=None, duration=None,
                                  resolution=None, repeat_behavior=None,
                                  repeat_count=None, transformation=None,
                                  end_behavior=None, end_callback=-1):
        """
        Update the animation parameters for one animated attribute (if
        specified) or all of them, if L{attribute} is set to None.

        @keyword attribute:       the property for which we want to update
                                  the settings
        @type attribute:          string or None
        @keyword duration:        new duration of animation, in milliseconds
        @type duration:           int
        @keyword resolution:      new resolution of the animation
        @type resolution:         float
        @keyword repeat_behavior: new repeat behavior of the animation
        @type repeat_behavior:    int
        @keyword repeat_count:    new repeat count of the animation
        @type repeat_count:       int
        @keyword transformation:  new transformation to apply to the animation
        @type transformation:     int
        @keyword end_callback:    new callback to call at the end of animation
        @type end_callback:       callable
        """
        def update_controller(controller):
            if duration != None:
                controller.duration = duration
            if end_behavior != None:
                controller.end_behavior = end_behavior
            if resolution != None:
                controller.resolution = resolution
            if repeat_behavior != None:
                controller.repeat_behavior = repeat_behavior
            if repeat_count != None:
                controller.repeat_count = repeat_count
            if transformation != None:
                controller.transformation = transformation
            if end_callback != -1:
                controller.end_callback = end_callback

        if attribute != None:
            update_controller(self._current_controllers.get(attribute))
        else:
            for controller in self._current_controllers.values():
                update_controller(controller)

    def stop_animations(self):
        """
        Stop the animations for all the attributes.
        """
        for controller in self._current_controllers.values():
            controller.stop()
            controller.modifiers = []

    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
        """
        controller = self._current_controllers[attribute]
        controller.stop()
        controller.modifiers = []

    def add_animated_attribute(self, attribute):
        """
        Setup L{attribute} to be implicitly animated. Further assignments
        to this attribute will provoke animations.

        @param attribute:  attribute to setup for implicit animations
        @type attribute:   string
        """
        self._animated_attributes += (attribute,)
        ctrl = controller.Controller(resolution     = 17,
                                     end_behavior   = controller.HOLD,
                                     transformation = controller.LINEAR,
                                     modifiers      = [])
        self._current_controllers[attribute] = ctrl

    def _animate_attribute_new_animation(self, attribute, target_values):
        # get the current value of the attribute from the proxied object
        current_value = getattr(self._object, attribute)

        # retrieve the currrent controller for the attribute
        ctrl = self._current_controllers[attribute]
        if ctrl.started:
            ctrl.stop()

        # setup timeline and modifier for the new animation
        nb_target_values = len(target_values)
        time_step = 1.0/nb_target_values
        timeline = [keyframe.KeyFrame(target_values[i], time_step*(i+1)) for i in
                    xrange(nb_target_values)]
        timeline.insert(0, keyframe.KeyFrame(current_value, 0.0))

        # check if there is a new controller ready for the attribute
        if self._next_controllers.has_key(attribute):
            # use the new controller
            ctrl = self._next_controllers[attribute]
            self._current_controllers[attribute] = ctrl
            del self._next_controllers[attribute]

        if len(ctrl.modifiers) > 0:
            mod = ctrl.modifiers[0]
            mod.timeline = timeline
        else:
            mod = modifier.Modifier([self._object],
                                    attribute,
                                    timeline)
            ctrl.modifiers = [mod]

        # start the animation
        ctrl.start()

    def _animate_attribute_new_target(self, attribute, target_value):
        # get the current value of the attribute from the proxied object
        current_value = getattr(self._object, attribute)

        # retrieve the current controller for the attribute
        ctrl = self._current_controllers[attribute]
        if ctrl.started:
            # do not do anything if the new target is the same as the old target
            if target_value == ctrl.modifiers[0].timeline[-1].value:
                return
            else:
                # modify the timeline and restart the animation
                ctrl.stop()
                ctrl.modifiers[0].timeline[0].value = current_value
                ctrl.modifiers[0].timeline[-1].value = target_value
                ctrl.start()
        else:
            # The animation is always triggered even if the target value is the
            # same as the current value. It has not always been the case and
            # led to long debugging sessions...
            self._animate_attribute_new_animation(attribute, [target_value])

    def _animate_attribute_add_target(self, attribute, target_value):
        # get the current value of the attribute from the proxied object
        current_value = getattr(self._object, attribute)

        # retrieve the current controller for the attribute
        ctrl = self._current_controllers[attribute]
        if ctrl.started:
            # modify the timeline
            time = ctrl._compute_factor()
            #ctrl.stop()
            if time == 0.0:
                i = 1
            else:
                i = ctrl.modifiers[0].next_keyframe_index(time)

            targets = [key.value for key in ctrl.modifiers[0].timeline[i:]]
            targets.append(target_value)

            self._animate_attribute_new_animation(attribute, targets)
            """
            # setup timeline and modifier for the new animation
            nb_targets = len(targets)
            time_step = 1.0/nb_targets
            timeline = [keyframe.KeyFrame(targets[i], time_step*(i+1)) for i in
                        range(nb_targets)]
            timeline.insert(0, keyframe.KeyFrame(current_value, 0.0))
            ctrl.modifiers[0].timeline = timeline

            ctrl.start()
            """
        else:
            self._animate_attribute_new_animation(attribute, [target_value])

    def __setattr__(self, attribute, value):
        if attribute == "_object" or \
           attribute == "_current_controllers" or \
           attribute == "_next_controllers" or \
           attribute == "_animated_attributes" or \
           attribute == "mode" or \
           attribute == "passthrough":
            # if self._object is set, set it locally
            super(AnimatedObject, self).__setattr__(attribute, value)
        else:
            if not self.passthrough and attribute in self._animated_attributes:
                # if an animated attribute is to be set, animate it
                if not isinstance(value, list):
                    if self.mode == REPLACE:
                        self._animate_attribute_new_target(attribute, value)
                    elif self.mode == APPEND:
                        self._animate_attribute_add_target(attribute, value)
                else:
                    # FIXME: could also use add_target
                    self._animate_attribute_new_animation(attribute, value)
            else:
                # for any other attribute set, set it to the proxied object
                if isinstance(value, list):
                    value = value[-1]

                setattr(self._object, attribute, value)

    def __getattr__(self, attribute):
        # for any attribute accessed not found locally
        # retrieve it from the proxied object
        if attribute in self._animated_attributes:
            # if an animated attribute is to be retrieved, do not return the
            # real value but the target value
            ctrl = self._current_controllers[attribute]
            if ctrl.started:
                return ctrl.modifiers[0].timeline[-1].value
            else:
                return getattr(self._object, attribute)

        else:
            return getattr(self._object, attribute)

    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 != None:
            controller = self._current_controllers[attribute]
            return controller.started
        else:
            for controller in self._current_controllers.values():
                if controller.started:
                    return True

            return False
