/*
 * Copyright 2009 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of either or both of the following licenses:
 *
 * 1) the GNU Lesser General Public License version 3, as published by the
 * Free Software Foundation; and/or
 * 2) the GNU Lesser General Public License version 2.1, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranties of
 * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the applicable version of the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of both the GNU Lesser General Public
 * License version 3 and version 2.1 along with this program.  If not, see
 * <http://www.gnu.org/licenses/>
 *
 * Authored by: Neil Jagdish Patel <neil.patel@canonical.com>
 *
 */
/** 
 * SECTION:ctk-effect
 * @short_description: The base class for all effects
 *
 * #CtkEffect is the base class that all effects need to derive from. 
 */

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include "ctk-effect.h"

#include <memory.h>
#include <gobject/gvaluecollector.h>

G_DEFINE_ABSTRACT_TYPE (CtkEffect, ctk_effect, G_TYPE_INITIALLY_UNOWNED);

enum
{
  PROP_0,

  PROP_ACTOR,
  PROP_MARGIN,
  PROP_OPACITY,
  PROP_STRENGTH
};

#define GET_PRIVATE(o) \
  (G_TYPE_INSTANCE_GET_PRIVATE ((o), CTK_TYPE_EFFECT, CtkEffectPrivate))

struct _CtkEffectPrivate
{
  gint margin;
  gfloat opacity;

  /* Specific to drop-shadow, blur, and glow effects */
  gint strength;      /* number of passes */
  gboolean recache_effect; /* this flags forces an effect to be recached */ 
};

/* Globals */
static GQuark effect_animation_quark = 0;

/* Forwards */

/*
 * GObject stuff
 */
static void
ctk_effect_set_property (GObject      *object,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  switch (prop_id)
    {
    case PROP_ACTOR:
      CTK_EFFECT (object)->_actor = g_value_get_pointer (value);
      break;

    case PROP_MARGIN:
      ctk_effect_set_margin (CTK_EFFECT (object), g_value_get_int (value));
      break;

    case PROP_OPACITY:
      ctk_effect_set_opacity (CTK_EFFECT (object), g_value_get_float (value));
      break;

    case PROP_STRENGTH:
      ctk_effect_set_strength (CTK_EFFECT (object), g_value_get_int (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ctk_effect_get_property (GObject    *object,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  switch (prop_id)
    {
    case PROP_ACTOR:
      g_value_set_pointer (value, CTK_EFFECT (object)->_actor);
      break;

    case PROP_MARGIN:
      g_value_set_int (value, ctk_effect_get_margin (CTK_EFFECT (object)));
      break;

    case PROP_OPACITY:
      g_value_set_float (value, ctk_effect_get_opacity (CTK_EFFECT (object)));
      break;

    case PROP_STRENGTH:
      g_value_set_int (value, ctk_effect_get_strength (CTK_EFFECT (object)));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ctk_effect_class_init (CtkEffectClass *klass)
{
  GObjectClass *obj_class = G_OBJECT_CLASS (klass);
  GParamSpec   *pspec;

  obj_class->set_property = ctk_effect_set_property;
  obj_class->get_property = ctk_effect_get_property;

  pspec = g_param_spec_pointer ("actor", "Actor",
                                "The ClutterActor this effect is bound to",
                                G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
  g_object_class_install_property (obj_class, PROP_ACTOR, pspec);

  pspec = g_param_spec_int ("margin", "Margin",
                            "Effect Margin Area",
                            CTK_EFFECT_MIN_MARGIN,
                            CTK_EFFECT_MAX_MARGIN,
                            CTK_EFFECT_DEFAULT_MARGIN,
                            G_PARAM_READWRITE);
  g_object_class_install_property (obj_class, PROP_MARGIN, pspec);

  pspec = g_param_spec_float ("opacity", "Opacity",
                            "Effect Opacity",
                            CTK_EFFECT_MIN_OPACITY,
                            CTK_EFFECT_MAX_OPACITY,
                            CTK_EFFECT_DEFAULT_OPACITY,
                            G_PARAM_READWRITE);
  g_object_class_install_property (obj_class, PROP_OPACITY, pspec);

  pspec = g_param_spec_int ("strength", "Strength",
                            "Strength of the blur in the effect",
                            CTK_EFFECT_MIN_STRENGTH,
                            CTK_EFFECT_MAX_STRENGTH,
                            CTK_EFFECT_DEFAULT_STRENGTH,
                            G_PARAM_READWRITE);
  g_object_class_install_property (obj_class, PROP_STRENGTH, pspec);

  g_type_class_add_private (obj_class, sizeof (CtkEffectPrivate));
}


static void
ctk_effect_init (CtkEffect *self)
{
  CtkEffectPrivate *priv;
  priv = GET_PRIVATE (self);

  if (effect_animation_quark == 0)
    {
      /* Using the same quark as clutter_actor_animate allows us to have the
       * nice effect of the animation destroying itself automatically after
       * the completed signal
       */
      effect_animation_quark =
        g_quark_from_static_string ("clutter-actor-animation");
    }

  priv->margin = CTK_EFFECT_MIN_MARGIN;
  priv->opacity = CTK_EFFECT_DEFAULT_OPACITY;
  priv->strength = CTK_EFFECT_DEFAULT_STRENGTH;
  priv->recache_effect = TRUE; /* will cause the caching of the effect the first time */
}

/*
 * Private Methods
 */

/*
 * COPIED AND MODIFIED (SLIGHTLY) FROM CLUTTER's clutter_actor_animate
 * (clutter-animation.c)
 * 
 * (C) Intel Corporation.
 *
 */
static ClutterAnimation *
animation_for_effect (CtkEffect *self)
{
  ClutterAnimation *animation;
  GObject          *object = G_OBJECT (self);

  animation = g_object_get_qdata (object, effect_animation_quark);
  if (animation == NULL)
    {
      animation = clutter_animation_new ();
      clutter_animation_set_object (animation, object);
      g_object_set_qdata (object, effect_animation_quark, animation);
    }
  else
    {
      ;
    }

  return animation;
}

static void
animation_setup_property (GObject          *object,
                                  ClutterAnimation *animation,
                                  const gchar      *property_name,
                                  const GValue     *value,
                                  GParamSpec       *pspec,
                                  gboolean          is_fixed)
{
  GValue real_value = { 0, };

  if (pspec->flags & G_PARAM_CONSTRUCT_ONLY)
    {
      g_warning ("Cannot bind property '%s': the property is "
                 "construct-only",
                 property_name);
      return;
    }

  if (!(pspec->flags & G_PARAM_WRITABLE))
    {
      g_warning ("Cannot bind property '%s': the property is "
                 "not writable",
                 property_name);
      return;
    }

  /* initialize the real value that will be used to store the
   * final state of the animation
   */
  g_value_init (&real_value, G_PARAM_SPEC_VALUE_TYPE (pspec));

  /* if it's not the same type of the GParamSpec value, try to
   * convert it using the GValue transformation API, otherwise
   * just copy it
   */
  if (!g_type_is_a (G_VALUE_TYPE (value), G_VALUE_TYPE (&real_value)))
    {
      if (!g_value_type_compatible (G_VALUE_TYPE (value),
                                    G_VALUE_TYPE (&real_value)) &&
          !g_value_type_compatible (G_VALUE_TYPE (&real_value),
                                    G_VALUE_TYPE (value)))
        {
          g_warning ("%s: Unable to convert from %s to %s for "
                     "the property '%s' of object %s",
                     G_STRLOC,
                     g_type_name (G_VALUE_TYPE (value)),
                     g_type_name (G_VALUE_TYPE (&real_value)),
                     property_name,
                     G_OBJECT_TYPE_NAME (object));
          g_value_unset (&real_value);
          return;
        }

      if (!g_value_transform (value, &real_value))
        {
          g_warning ("%s: Unable to transform from %s to %s",
                     G_STRLOC,
                     g_type_name (G_VALUE_TYPE (value)),
                     g_type_name (G_VALUE_TYPE (&real_value)));
          g_value_unset (&real_value);
          return;
        }
    }
  else
    g_value_copy (value, &real_value);

  /* create an interval and bind it to the property, in case
   * it's not a fixed property, otherwise just set it
   */
  if (G_LIKELY (!is_fixed))
    {
      ClutterInterval *interval;
      GValue cur_value = { 0, };

      g_value_init (&cur_value, G_PARAM_SPEC_VALUE_TYPE (pspec));
      g_object_get_property (object, property_name, &cur_value);

      interval =
        clutter_interval_new_with_values (G_PARAM_SPEC_VALUE_TYPE (pspec),
                                          &cur_value,
                                          &real_value);

      if (!clutter_animation_has_property (animation, pspec->name))
        clutter_animation_bind_interval (animation,
                                         property_name,
                                         interval);
       /* clutter_animation_bind_property_internal (animation,
                                                  pspec,
                                                  interval);*/
      else
        clutter_animation_update_interval (animation,
                                           property_name,
                                           interval);
        /*clutter_animation_update_property_internal (animation,
                                                    pspec,
                                                    interval);*/

      g_value_unset (&cur_value);
    }
  else
    g_object_set_property (object, property_name, &real_value);

  g_value_unset (&real_value);
}

static const struct
{
  const gchar   *name;
  GConnectFlags  flags;
} signal_prefixes[] = 
  {
    { "::", 0 },
    { "-swapped::", G_CONNECT_SWAPPED },
    { "-after::", G_CONNECT_AFTER },
    { "-swapped-after::", G_CONNECT_SWAPPED | G_CONNECT_AFTER }
  };

static gboolean
property_has_signal_prefix (const gchar   *property_name, 
                            GConnectFlags *flags,
                            gint          *offset)
{
  int i;

  if (!g_str_has_prefix (property_name, "signal"))
    return FALSE;

  for (i = 0; G_N_ELEMENTS (signal_prefixes); i++)
    {
      if (g_str_has_prefix (property_name + 6, signal_prefixes[i].name))
        {
          *offset = strlen (signal_prefixes[i].name) + 6;
          *flags = signal_prefixes[i].flags;
          return TRUE;
        }
    }

  return FALSE;
}

static void
animation_setup_valist (GObject          *object,
                        ClutterAnimation *animation,
                        const gchar      *first_property_name,
                        va_list           var_args)
{
  GObjectClass *klass;
  const gchar  *property_name;

  klass = G_OBJECT_GET_CLASS (object);

  property_name = first_property_name;
  while (property_name != NULL)
    {
      GParamSpec   *pspec;
      GValue        final = { 0, };
      gchar        *error = NULL;
      gboolean      is_fixed = FALSE;
      GConnectFlags flags;
      gint          offset;

      if (property_has_signal_prefix (property_name, &flags, &offset))
        {
          const gchar *signal_name = property_name + offset;
          GCallback callback = va_arg (var_args, GCallback);
          gpointer  userdata = va_arg (var_args, gpointer);

          g_signal_connect_data (animation, signal_name,
                                 callback, userdata,
                                 NULL, flags);
        }
      else
        {
          if (g_str_has_prefix (property_name, "fixed::"))
            {
              property_name += 7; /* strlen("fixed::") */
              is_fixed = TRUE;
            }

          pspec = g_object_class_find_property (klass, property_name);
          if (!pspec)
            {
              g_warning ("Cannot bind property '%s': objects of type '%s' do "
                         "not have this property",
                         property_name,
                         g_type_name (G_OBJECT_TYPE (object)));
              break;
            }

          g_value_init (&final, G_PARAM_SPEC_VALUE_TYPE (pspec));
          G_VALUE_COLLECT (&final, var_args, 0, &error);
          if (error)
            {
              g_warning ("%s: %s", G_STRLOC, error);
              g_free (error);
              break;
            }

          animation_setup_property (object,
                                    animation,
                                    property_name,
                                    &final,
                                    pspec,
                                    is_fixed);
        }
      property_name = va_arg (var_args, gchar *);
    }
}

static void
animation_start (ClutterAnimation *animation)
{
  ClutterTimeline *timeline;

  timeline = clutter_animation_get_timeline (animation);
  if (G_UNLIKELY (timeline != NULL))
    clutter_timeline_start (timeline);
  else
    g_warning (G_STRLOC ": no timeline found, unable to start animation");
}

/*
 * END OF CLUTTER ANIMATION CODE
 */


/*
 * Public Methods
 */
/**
 * ctk_effect_paint:
 * @self: a #CtkEffect
 * @func: a #CtkEffectPaintFunc
 *
 * Will cause the @self to paint itself. Should only be called during a
 * paint cycle. @func will be used to paint the actor that the effect is 
 * attached to directly, without going through #clutter_actor_paint, which
 * does some additional work which conflicts with the effects.
 **/
void
ctk_effect_paint (CtkEffect          *self,
                  CtkEffectPaintFunc  func,
                  gboolean            is_last_effect)
{
  CtkEffectClass *klass;

  g_return_if_fail (CTK_IS_EFFECT (self));
  klass = CTK_EFFECT_GET_CLASS (self);

  if (klass->paint)
    klass->paint (self, func, is_last_effect);
}

/**
 * ctk_effect_set_actor:
 * @self: a #CtkEffect
 * @actor: (allow-none): a #CtkActor, or %NULL
 *
 * This is a conveniece function for #CtkActor. If you want to bind an effect
 * to an actor, you should call #ctk_actor_add_effect instead.
 **/
void
ctk_effect_set_actor (CtkEffect *self, ClutterActor *actor)
{
  g_return_if_fail (CTK_IS_EFFECT (self));

  self->_actor = actor;
}

/**
 * ctk_effect_get_actor:
 * @self: a #CtkEffect
 *
 * Returns: the #ClutterActor that this effect is bound to, or %NULL
 **/
ClutterActor *
ctk_effect_get_actor (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), NULL);

  return self->_actor;
}

/**
 * ctk_effect_set_margin_area:
 * @self: a #CtkEffect
 *
 * The margin area of the effect. This icreases the area used to apply an effect on an actor
 * and can be used to make sure the effect is fully visible (ie not clipped).
 **/
void
ctk_effect_set_margin (CtkEffect *self,
                            gint margin)
{
  g_return_if_fail (CTK_IS_EFFECT (self));
  GET_PRIVATE(self)->margin = margin;
}

/**
 * ctk_effect_get_margin_area:
 * @self: a #CtkEffect
 *
 * Returns: The margin area of the effect. The area that extend beyond the actor surface and 
 * where the effect can be visible.
 **/
gint
ctk_effect_get_margin (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), 0);
  return GET_PRIVATE(self)->margin;
}

/**
 * ctk_effect_set_opacity:
 * @self: a #CtkEffect
 *
 * The opacity of the effect.
 **/
void
ctk_effect_set_opacity (CtkEffect *self,
                        gfloat opacity)
{
  ClutterActor *actor;
  g_return_if_fail (CTK_IS_EFFECT (self));

  if (opacity > CTK_EFFECT_MAX_OPACITY ||
      opacity < CTK_EFFECT_MIN_OPACITY)
    {
      g_warning ("%s() at line %s: Out of bound parameter", G_STRFUNC, G_STRLOC);
    }

  opacity = CLAMP (opacity, CTK_EFFECT_MIN_OPACITY, CTK_EFFECT_MAX_OPACITY);  
  GET_PRIVATE(self)->opacity = opacity;
  g_object_notify (G_OBJECT (self), "opacity");

  actor = ctk_effect_get_actor (self);
  if (CLUTTER_IS_ACTOR (actor) && CLUTTER_ACTOR_IS_VISIBLE (actor))
    clutter_actor_queue_redraw (actor);
}

/**
 * ctk_effect_get_opacity:
 * @self: a #CtkEffect
 *
 * Returns: The opacity.
 **/
gfloat
ctk_effect_get_opacity (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), 0);
  return GET_PRIVATE(self)->opacity;
}

/**
 * ctk_effect_set_strength:
 * @self: a #CtkEffect
 *
 * The strength of the effect. This value is used as an indicator for some effects, in order to increase 
 * the strength of the effect.
 **/
void
ctk_effect_set_strength (CtkEffect *self,
                         gint strength)
{
  ClutterActor *actor;
  g_return_if_fail (CTK_IS_EFFECT (self));

  if (strength > CTK_EFFECT_MAX_STRENGTH ||
      strength < CTK_EFFECT_MIN_STRENGTH)
    {
      g_warning ("%s() at line %s: Out of bound parameter", G_STRFUNC, G_STRLOC);
    }

  strength = CLAMP (strength, CTK_EFFECT_MIN_STRENGTH, CTK_EFFECT_MAX_STRENGTH);  
  GET_PRIVATE(self)->strength = strength;
  g_object_notify (G_OBJECT (self), "strength");

  actor = ctk_effect_get_actor (self);
  if (CLUTTER_IS_ACTOR (actor) && CLUTTER_ACTOR_IS_VISIBLE (actor))
    clutter_actor_queue_redraw (actor);
}

/**
 * ctk_effect_get_strength:
 * @self: a #CtkEffect
 *
 * Returns: The strength of the effect.
 **/
gint
ctk_effect_get_strength (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), 0);
  return GET_PRIVATE(self)->strength;
}

/**
 * ctk_effect_set_invalidate_effect_cache:
 * @self: a #CtkEffect
 *
 * Invalidate an effect cache. Cause the effect to be recached next time it is being rendered.
 **/
void
ctk_effect_set_invalidate_effect_cache (CtkEffect *self, gboolean dirty)
{
  ClutterActor *actor;
  g_return_if_fail (CTK_IS_EFFECT (self));

  GET_PRIVATE(self)->recache_effect = dirty;

  actor = ctk_effect_get_actor (self);
  if (CLUTTER_IS_ACTOR (actor) && CLUTTER_ACTOR_IS_VISIBLE (actor))
    clutter_actor_queue_redraw (actor);
}

/**
 * ctk_effect_is_effect_cache_dirty:
 * @self: a #CtkEffect
 *
 * Returns: True if the effect cache is dirty. This function is called in the paint cycle of the actor.
 **/
gboolean
ctk_effect_is_effect_cache_dirty (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), FALSE);

  return GET_PRIVATE(self)->recache_effect;
}

/**
 * ctk_effect_animate:
 * @self: a #CtkEffect
 * @mode: an animation mode logical id
 * @duration: duration of the animation, in milliseconds
 * @first_property_name: the name of a property
 * @VarArgs: a %NULL terminated list of property name and property values
 *
 * Animates the give list of properties of @self between the current value for
 * each property and a new final value. The animation has a definite duration
 * and a speed given by the @mode. See #clutter_actor_animate for more details
 * on signal connection.
 *
 * Return value: (transfer none): a #ClutterAnimation object. The object is
 * owned by the #CtkEffect  and should not be unrefferenced with 
 * g_object_unref ()
 **/
ClutterAnimation *
ctk_effect_animate (CtkEffect    *self,
                    gulong        mode,
                    guint         duration,
                    const gchar  *first_property_name,
                    ...)
{
  ClutterAnimation *animation;
  va_list args;

  g_return_val_if_fail (CTK_IS_EFFECT (self), NULL);
  g_return_val_if_fail (mode != CLUTTER_CUSTOM_MODE, NULL);
  g_return_val_if_fail (duration > 0, NULL);
  g_return_val_if_fail (first_property_name != NULL, NULL);

  animation = animation_for_effect (self);
  clutter_animation_set_mode (animation, mode);
  clutter_animation_set_duration (animation, duration);

  va_start (args, first_property_name);
  animation_setup_valist (G_OBJECT (self),
                          animation,
                          first_property_name,
                          args);
  va_end (args);

  animation_start (animation);
  
  return animation;
}

/**
 * ctk_effect_get_animation:
 * @self: a #CtkEffect
 *
 * Returns: a #ClutterAnimation for the effect, if #ctk_effect_animate has
 * been called on @self.
 **/
ClutterAnimation *
ctk_effect_get_animation (CtkEffect *self)
{
  g_return_val_if_fail (CTK_IS_EFFECT (self), NULL);

  return g_object_get_qdata (G_OBJECT (self), effect_animation_quark);
}
