/*
 * Clutter.
 *
 * An OpenGL based 'interactive canvas' library.
 *
 * Copyright (C) 2010  Intel Corporation.
 * Copyright (C) 2011  Robert Bosch Car Multimedia GmbH.
 * Copyright (C) 2012, 2014  Collabora Ltd.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Author:
 *   Emanuele Aina <emanuele.aina@collabora.com>
 *
 * Based on ClutterDragAction, ClutterSwipeAction, and MxKineticScrollView,
 * written by:
 *   Emmanuele Bassi <ebassi@linux.intel.com>
 *   Tomeu Vizoso <tomeu.vizoso@collabora.co.uk>
 *   Chris Lord <chris@linux.intel.com>
 */

/**
 * SECTION:clutter-pan-action
 * @Title: ClutterPanAction
 * @Short_Description: Action for pan gestures
 *
 * #ClutterPanAction is a sub-class of #ClutterGestureAction that implements
 * the logic for recognizing pan gestures.
 *
 * The simplest usage of #ClutterPanAction consists in adding it to
 * a #ClutterActor with a child and setting it as reactive; for instance,
 * the following code:
 *
 * |[
 *   clutter_actor_add_action (actor, clutter_pan_action_new ());
 *   clutter_actor_set_reactive (actor, TRUE);
 * ]|
 *
 * will automatically result in the actor children to be moved
 * when dragging.
 *
 * Since: 1.12
 */

#include "clutter-build-config.h"

#include "clutter-pan-action.h"

#include "clutter-debug.h"
#include "clutter-enum-types.h"
#include "clutter-gesture-action-private.h"
#include "clutter-marshal.h"
#include "clutter-private.h"
#include "clutter-timeline.h"
#include <math.h>

#define FLOAT_EPSILON   (1e-15)

static const gfloat min_velocity = 0.1f; // measured in px/ms
static const gfloat reference_fps = 60.0f; // the fps assumed for the deceleration rate
static const gfloat default_deceleration_rate = 0.95f;
static const gfloat default_acceleration_factor = 1.0f;

typedef enum
{
  PAN_STATE_INACTIVE,
  PAN_STATE_PANNING,
  PAN_STATE_INTERPOLATING
} PanState;

typedef enum
{
  SCROLL_PINNED_UNKNOWN,
  SCROLL_PINNED_NONE,
  SCROLL_PINNED_HORIZONTAL,
  SCROLL_PINNED_VERTICAL
} PinState;

struct _ClutterPanActionPrivate
{
  ClutterPanAxis pan_axis;

  PanState state;

  /* Variables for storing acceleration information */
  ClutterTimeline *deceleration_timeline;
  gfloat target_x;
  gfloat target_y;
  gfloat dx;
  gfloat dy;
  gdouble deceleration_rate;
  gdouble acceleration_factor;

  /* Inertial motion tracking */
  gfloat interpolated_x;
  gfloat interpolated_y;
  gfloat release_x;
  gfloat release_y;

  guint should_interpolate : 1;

  PinState pin_state;
};

enum
{
  PROP_0,

  PROP_PAN_AXIS,
  PROP_INTERPOLATE,
  PROP_DECELERATION,
  PROP_ACCELERATION_FACTOR,

  PROP_LAST
};

static GParamSpec *pan_props[PROP_LAST] = { NULL, };

enum
{
  PAN,
  PAN_STOPPED,

  LAST_SIGNAL
};

static guint pan_signals[LAST_SIGNAL] = { 0, };

G_DEFINE_TYPE_WITH_PRIVATE (ClutterPanAction, clutter_pan_action,
                            CLUTTER_TYPE_GESTURE_ACTION)

static void
emit_pan (ClutterPanAction *self,
          ClutterActor     *actor,
          gboolean          is_interpolated)
{
  ClutterPanActionPrivate *priv = self->priv;
  gboolean retval;

  if (priv->pin_state == SCROLL_PINNED_UNKNOWN)
    {
      priv->pin_state = SCROLL_PINNED_NONE;
      if (priv->pan_axis == CLUTTER_PAN_AXIS_AUTO)
        {
          gfloat delta_x;
          gfloat delta_y;
          gfloat scroll_threshold = G_PI_4/2;
          gfloat drag_angle;

          clutter_gesture_action_get_motion_delta (CLUTTER_GESTURE_ACTION (self),
                                                   0,
                                                   &delta_x,
                                                   &delta_y);

          if (delta_x != 0.0f)
            drag_angle = atanf (delta_y / delta_x);
          else
            drag_angle = G_PI_2;

          if ((drag_angle > -scroll_threshold) &&
              (drag_angle < scroll_threshold))
            priv->pin_state = SCROLL_PINNED_HORIZONTAL;
          else if ((drag_angle > (G_PI_2 - scroll_threshold)) ||
                    (drag_angle < -(G_PI_2 - scroll_threshold)))
            priv->pin_state = SCROLL_PINNED_VERTICAL;
        }
    }

  g_signal_emit (self, pan_signals[PAN], 0, actor, is_interpolated, &retval);
}

static void
emit_pan_stopped (ClutterPanAction *self,
                  ClutterActor     *actor)
{
  ClutterPanActionPrivate *priv = self->priv;

  g_signal_emit (self, pan_signals[PAN_STOPPED], 0, actor);
  priv->state = PAN_STATE_INACTIVE;
}

static void
on_deceleration_stopped (ClutterTimeline           *timeline,
                         gboolean                   is_finished,
                         ClutterPanAction *self)
{
  ClutterPanActionPrivate *priv = self->priv;
  ClutterActor *actor;

  g_object_unref (timeline);
  priv->deceleration_timeline = NULL;

  actor = clutter_actor_meta_get_actor (CLUTTER_ACTOR_META (self));
  emit_pan_stopped (self, actor);
}

static void
on_deceleration_new_frame (ClutterTimeline     *timeline,
                           gint                 elapsed_time,
                           ClutterPanAction    *self)
{
  ClutterPanActionPrivate *priv = self->priv;
  ClutterActor *actor;
  gdouble progress;
  gfloat interpolated_x, interpolated_y;

  progress = clutter_timeline_get_progress (timeline);

  interpolated_x = priv->target_x * progress;
  interpolated_y = priv->target_y * progress;
  priv->dx = interpolated_x - priv->interpolated_x;
  priv->dy = interpolated_y - priv->interpolated_y;
  priv->interpolated_x = interpolated_x;
  priv->interpolated_y = interpolated_y;

  actor = clutter_actor_meta_get_actor (CLUTTER_ACTOR_META (self));
  emit_pan (self, actor, TRUE);
}

static gboolean
gesture_prepare (ClutterGestureAction  *gesture,
                 ClutterActor          *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gesture);
  ClutterPanActionPrivate *priv = self->priv;

  if (priv->state == PAN_STATE_INTERPOLATING && priv->deceleration_timeline)
    clutter_timeline_stop (priv->deceleration_timeline);

  return TRUE;
}

static gboolean
gesture_begin (ClutterGestureAction  *gesture,
               ClutterActor          *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gesture);
  ClutterPanActionPrivate *priv = self->priv;

  priv->pin_state = SCROLL_PINNED_UNKNOWN;
  priv->state = PAN_STATE_PANNING;
  priv->interpolated_x = priv->interpolated_y = 0.0f;
  priv->dx = priv->dy = 0.0f;

  return TRUE;
}

static gboolean
gesture_progress (ClutterGestureAction *gesture,
                  ClutterActor         *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gesture);

  emit_pan (self, actor, FALSE);

  return TRUE;
}

static void
gesture_cancel (ClutterGestureAction *gesture,
                ClutterActor         *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gesture);
  ClutterPanActionPrivate *priv = self->priv;

  priv->state = PAN_STATE_INACTIVE;
}

static void
gesture_end (ClutterGestureAction *gesture,
             ClutterActor         *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gesture);
  ClutterPanActionPrivate *priv = self->priv;
  gfloat velocity, velocity_x, velocity_y;
  gfloat delta_x, delta_y;
  gfloat tau;
  gint duration;

  clutter_gesture_action_get_release_coords (CLUTTER_GESTURE_ACTION (self),
                                             0,
                                             &priv->release_x,
                                             &priv->release_y);

  if (!priv->should_interpolate)
    {
      priv->state = PAN_STATE_INACTIVE;
      return;
    }

  priv->state = PAN_STATE_INTERPOLATING;

  clutter_gesture_action_get_motion_delta (gesture, 0, &delta_x, &delta_y);
  velocity = clutter_gesture_action_get_velocity (gesture, 0,
                                                  &velocity_x,
                                                  &velocity_y);

  /* Exponential timing constant v(t) = v(0) * exp(-t/tau)
   * tau = 1000ms / (frame_per_second * - ln(decay_per_frame))
   * with frame_per_second = 60 and decay_per_frame = 0.95, tau ~= 325ms
   * see http://ariya.ofilabs.com/2011/10/flick-list-with-its-momentum-scrolling-and-deceleration.html */
  tau = 1000.0f / (reference_fps * - logf (priv->deceleration_rate));

  /* See where the decreasing velocity reaches $min_velocity px/ms
   * v(t) = v(0) * exp(-t/tau) = min_velocity
   * t = - tau * ln( min_velocity / |v(0)|) */
  duration = - tau * logf (min_velocity / (ABS (velocity) *
                                           priv->acceleration_factor));

  /* Target point: x(t) = v(0) * tau * [1 - exp(-t/tau)] */
  priv->target_x = (velocity_x * priv->acceleration_factor * tau *
                    (1 - exp ((float)-duration / tau)));
  priv->target_y = (velocity_y * priv->acceleration_factor * tau *
                    (1 - exp ((float)-duration / tau)));

  if (ABS (velocity) * priv->acceleration_factor > min_velocity &&
      duration > FLOAT_EPSILON)
    {
      ClutterActor *pan_actor =
        clutter_actor_meta_get_actor (CLUTTER_ACTOR_META (gesture));

      priv->interpolated_x = priv->interpolated_y = 0.0f;
      priv->deceleration_timeline = clutter_timeline_new_for_actor (pan_actor,
                                                                    duration);
      clutter_timeline_set_progress_mode (priv->deceleration_timeline,
                                          CLUTTER_EASE_OUT_EXPO);

      g_signal_connect (priv->deceleration_timeline, "new_frame",
                        G_CALLBACK (on_deceleration_new_frame), self);
      g_signal_connect (priv->deceleration_timeline, "stopped",
                        G_CALLBACK (on_deceleration_stopped), self);
      clutter_timeline_start (priv->deceleration_timeline);
    }
  else
    {
      emit_pan_stopped (self, actor);
    }
}

static void
clutter_pan_action_set_property (GObject      *gobject,
                                 guint         prop_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gobject);

  switch (prop_id)
    {
    case PROP_PAN_AXIS:
      clutter_pan_action_set_pan_axis (self, g_value_get_enum (value));
      break;

    case PROP_INTERPOLATE :
      clutter_pan_action_set_interpolate (self, g_value_get_boolean (value));
      break;

    case PROP_DECELERATION :
      clutter_pan_action_set_deceleration (self, g_value_get_double (value));
      break;

    case PROP_ACCELERATION_FACTOR :
      clutter_pan_action_set_acceleration_factor (self,
                                                  g_value_get_double (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
clutter_pan_action_get_property (GObject    *gobject,
                                 guint       prop_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (gobject);
  ClutterPanActionPrivate *priv = self->priv;

  switch (prop_id)
    {
    case PROP_PAN_AXIS:
      g_value_set_enum (value, priv->pan_axis);
      break;

    case PROP_INTERPOLATE :
      g_value_set_boolean (value, priv->should_interpolate);
      break;

    case PROP_DECELERATION :
      g_value_set_double (value, priv->deceleration_rate);
      break;

    case PROP_ACCELERATION_FACTOR :
      g_value_set_double (value, priv->acceleration_factor);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
clutter_pan_action_constructed (GObject *gobject)
{
  ClutterGestureAction *gesture;
  ClutterGestureTriggerEdge edge;

  gesture = CLUTTER_GESTURE_ACTION (gobject);
  edge = CLUTTER_GESTURE_TRIGGER_EDGE_AFTER;
  clutter_gesture_action_set_threshold_trigger_edge (gesture, edge);
}

static void
clutter_pan_action_dispose (GObject *gobject)
{
  ClutterPanActionPrivate *priv = CLUTTER_PAN_ACTION (gobject)->priv;

  g_clear_object (&priv->deceleration_timeline);

  G_OBJECT_CLASS (clutter_pan_action_parent_class)->dispose (gobject);
}

static void
clutter_pan_action_set_actor (ClutterActorMeta *meta,
                              ClutterActor     *actor)
{
  ClutterPanAction *self = CLUTTER_PAN_ACTION (meta);
  ClutterPanActionPrivate *priv = self->priv;
  ClutterActor *old_actor;

  old_actor = clutter_actor_meta_get_actor (CLUTTER_ACTOR_META (self));
  if (old_actor != actor)
    {
      /* make sure we reset the state */
      if (priv->state == PAN_STATE_INTERPOLATING)
        g_clear_object (&priv->deceleration_timeline);
      else if (priv->deceleration_timeline)
        clutter_timeline_set_actor (priv->deceleration_timeline, actor);
    }

  CLUTTER_ACTOR_META_CLASS (clutter_pan_action_parent_class)->set_actor (meta,
                                                                         actor);
}


static void
clutter_pan_action_class_init (ClutterPanActionClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  ClutterActorMetaClass *meta_class = CLUTTER_ACTOR_META_CLASS (klass);
  ClutterGestureActionClass *gesture_class =
      CLUTTER_GESTURE_ACTION_CLASS (klass);

  gesture_class->gesture_prepare = gesture_prepare;
  gesture_class->gesture_begin = gesture_begin;
  gesture_class->gesture_progress = gesture_progress;
  gesture_class->gesture_cancel = gesture_cancel;
  gesture_class->gesture_end = gesture_end;

  meta_class->set_actor = clutter_pan_action_set_actor;

  /**
   * ClutterPanAction:pan-axis:
   *
   * Constraints the panning action to the specified axis
   *
   * Since: 1.12
   */
  pan_props[PROP_PAN_AXIS] =
    g_param_spec_enum ("pan-axis",
                       P_("Pan Axis"),
                       P_("Constraints the panning to an axis"),
                       CLUTTER_TYPE_PAN_AXIS,
                       CLUTTER_PAN_AXIS_NONE,
                       CLUTTER_PARAM_READWRITE);

  /**
   * ClutterPanAction:interpolate:
   *
   * Whether interpolated events emission is enabled.
   *
   * Since: 1.12
   */
  pan_props[PROP_INTERPOLATE] =
    g_param_spec_boolean ("interpolate",
                          P_("Interpolate"),
                          P_("Whether interpolated events emission is enabled."),
                          FALSE,
                          CLUTTER_PARAM_READWRITE);

  /**
   * ClutterPanAction:deceleration:
   *
   * The rate at which the interpolated panning will decelerate in
   *
   * #ClutterPanAction will emit interpolated ::pan events with decreasing
   * scroll deltas, using the rate specified by this property.
   *
   * Since: 1.12
   */
  pan_props[PROP_DECELERATION] =
    g_param_spec_double ("deceleration",
                         P_("Deceleration"),
                         P_("Rate at which the interpolated panning will decelerate in"),
                         FLOAT_EPSILON, 1.0, default_deceleration_rate,
                         CLUTTER_PARAM_READWRITE);

  /**
   * ClutterPanAction:acceleration-factor:
   *
   * The initial acceleration factor
   *
   * The kinetic momentum measured at the time of releasing the pointer will
   * be multiplied by the factor specified by this property before being used
   * to generate interpolated ::pan events.
   *
   * Since: 1.12
   */
  pan_props[PROP_ACCELERATION_FACTOR] =
    g_param_spec_double ("acceleration-factor",
                         P_("Initial acceleration factor"),
                         P_("Factor applied to the momentum when starting the interpolated phase"),
                         1.0, G_MAXDOUBLE, default_acceleration_factor,
                         CLUTTER_PARAM_READWRITE);

  gobject_class->constructed = clutter_pan_action_constructed;
  gobject_class->set_property = clutter_pan_action_set_property;
  gobject_class->get_property = clutter_pan_action_get_property;
  gobject_class->dispose = clutter_pan_action_dispose;
  g_object_class_install_properties  (gobject_class,
                                      PROP_LAST,
                                      pan_props);

  /**
   * ClutterPanAction::pan:
   * @action: the #ClutterPanAction that emitted the signal
   * @actor: the #ClutterActor attached to the @action
   * @is_interpolated: if the event is the result of interpolating
   *                   the motion velocity at the end of the drag
   *
   * The ::pan signal is emitted to keep track of the motion during
   * a pan gesture. @is_interpolated is set to %TRUE during the
   * interpolation phase of the pan, after the drag has ended and
   * the :interpolate property was set to %TRUE.
   *
   * Return value: %TRUE if the pan should continue, and %FALSE if
   *   the pan should be cancelled.
   *
   * Since: 1.12
   */
  pan_signals[PAN] =
    g_signal_new (I_("pan"),
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0, g_signal_accumulator_true_handled, NULL,
                  _clutter_marshal_BOOLEAN__OBJECT_BOOLEAN,
                  G_TYPE_BOOLEAN, 2,
                  CLUTTER_TYPE_ACTOR,
                  G_TYPE_BOOLEAN);

  /**
   * ClutterPanAction::pan-stopped:
   * @action: the #ClutterPanAction that emitted the signal
   * @actor: the #ClutterActor attached to the @action
   *
   * The ::pan-stopped signal is emitted at the end of the interpolation
   * phase of the pan action, only when :interpolate is set to %TRUE.
   *
   * Since: 1.12
   */
  pan_signals[PAN_STOPPED] =
    g_signal_new (I_("pan-stopped"),
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (ClutterPanActionClass, pan_stopped),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1,
                  CLUTTER_TYPE_ACTOR);
}

static void
clutter_pan_action_init (ClutterPanAction *self)
{
  self->priv = clutter_pan_action_get_instance_private (self);
  self->priv->deceleration_rate = default_deceleration_rate;
  self->priv->acceleration_factor = default_acceleration_factor;
  self->priv->state = PAN_STATE_INACTIVE;
}

/**
 * clutter_pan_action_new:
 *
 * Creates a new #ClutterPanAction instance
 *
 * Return value: the newly created #ClutterPanAction
 *
 * Since: 1.12
 */
ClutterAction *
clutter_pan_action_new (void)
{
  return g_object_new (CLUTTER_TYPE_PAN_ACTION, NULL);
}

/**
 * clutter_pan_action_set_pan_axis:
 * @self: a #ClutterPanAction
 * @axis: the axis to constraint the panning to
 *
 * Restricts the panning action to a specific axis
 *
 * Since: 1.12
 */
void
clutter_pan_action_set_pan_axis (ClutterPanAction *self,
                                 ClutterPanAxis    axis)
{
  ClutterPanActionPrivate *priv;

  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));
  g_return_if_fail (axis >= CLUTTER_PAN_AXIS_NONE &&
                    axis <= CLUTTER_PAN_AXIS_AUTO);

  priv = self->priv;

  if (priv->pan_axis == axis)
    return;

  priv->pan_axis = axis;

  g_object_notify_by_pspec (G_OBJECT (self), pan_props[PROP_PAN_AXIS]);
}

/**
 * clutter_pan_action_get_pan_axis:
 * @self: a #ClutterPanAction
 *
 * Retrieves the axis constraint set by clutter_pan_action_set_pan_axis()
 *
 * Return value: the axis constraint
 *
 * Since: 1.12
 */
ClutterPanAxis
clutter_pan_action_get_pan_axis (ClutterPanAction *self)
{
  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self),
                        CLUTTER_PAN_AXIS_NONE);

  return self->priv->pan_axis;
}

/**
 * clutter_pan_action_set_interpolate:
 * @self: a #ClutterPanAction
 * @should_interpolate: whether to enable interpolated pan events
 *
 * Sets whether the action should emit interpolated ::pan events
 * after the drag has ended, to emulate the gesture kinetic inertia.
 *
 * Since: 1.12
 */
void
clutter_pan_action_set_interpolate (ClutterPanAction *self,
                                    gboolean          should_interpolate)
{
  ClutterPanActionPrivate *priv;

  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));

  priv = self->priv;

  should_interpolate = !!should_interpolate;

  if (priv->should_interpolate == should_interpolate)
    return;

  priv->should_interpolate = should_interpolate;

  g_object_notify_by_pspec (G_OBJECT (self), pan_props[PROP_INTERPOLATE]);
}

/**
 * clutter_pan_action_get_interpolate:
 * @self: a #ClutterPanAction
 *
 * Checks if the action should emit ::pan events even after releasing
 * the pointer during a panning gesture, to emulate some kind of
 * kinetic inertia.
 *
 * Return value: %TRUE if interpolated events emission is active.
 *
 * Since: 1.12
 */
gboolean
clutter_pan_action_get_interpolate (ClutterPanAction *self)
{
  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self),
                        FALSE);

  return self->priv->should_interpolate;
}

/**
 * clutter_pan_action_set_deceleration:
 * @self: A #ClutterPanAction
 * @rate: The deceleration rate
 *
 * Sets the deceleration rate of the interpolated ::pan events generated
 * after a pan gesture. This is approximately the value that the momentum
 * at the time of releasing the pointer is divided by every 60th of a second.
 *
 * Since: 1.12
 */
void
clutter_pan_action_set_deceleration (ClutterPanAction *self,
                                     gdouble           rate)
{
  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));
  g_return_if_fail (rate <= 1.0);
  g_return_if_fail (rate > 0.0);

  self->priv->deceleration_rate = rate;
  g_object_notify_by_pspec (G_OBJECT (self), pan_props[PROP_DECELERATION]);
}

/**
 * clutter_pan_action_get_deceleration:
 * @self: A #ClutterPanAction
 *
 * Retrieves the deceleration rate of interpolated ::pan events.
 *
 * Return value: The deceleration rate of the interpolated events.
 *
 * Since: 1.12
 */
gdouble
clutter_pan_action_get_deceleration (ClutterPanAction *self)
{
  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self), 0.95);
  return self->priv->deceleration_rate;
}

/**
 * clutter_pan_action_set_acceleration_factor:
 * @self: A #ClutterPanAction
 * @factor: The acceleration factor
 *
 * Factor applied to the momentum velocity at the time of releasing the
 * pointer when generating interpolated ::pan events.
 *
 * Since: 1.12
 */
void
clutter_pan_action_set_acceleration_factor (ClutterPanAction *self,
                                            gdouble           factor)
{
  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));
  g_return_if_fail (factor >= 0.0);

  self->priv->acceleration_factor = factor;
  g_object_notify_by_pspec (G_OBJECT (self), pan_props[PROP_ACCELERATION_FACTOR]);
}

/**
 * clutter_pan_action_get_acceleration_factor:
 * @self: A #ClutterPanAction
 *
 * Retrieves the initial acceleration factor for interpolated ::pan events.
 *
 * Return value: The initial acceleration factor for interpolated events.
 *
 * Since: 1.12
 */
gdouble
clutter_pan_action_get_acceleration_factor (ClutterPanAction *self)
{
  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self), 1.0);
  return self->priv->acceleration_factor;
}

/**
 * clutter_pan_action_get_interpolated_coords:
 * @self: A #ClutterPanAction
 * @interpolated_x: (out) (allow-none): return location for the latest
 *   interpolated event's X coordinate
 * @interpolated_y: (out) (allow-none): return location for the latest
 *   interpolated event's Y coordinate
 *
 * Retrieves the coordinates, in stage space, of the latest interpolated
 * event, analogous to clutter_gesture_action_get_motion_coords().
 *
 * Since: 1.12
 */
void
clutter_pan_action_get_interpolated_coords (ClutterPanAction *self,
                                            gfloat           *interpolated_x,
                                            gfloat           *interpolated_y)
{
  ClutterPanActionPrivate *priv;

  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));

  priv = self->priv;

  if (interpolated_x)
    *interpolated_x = priv->release_x + priv->interpolated_x;

  if (interpolated_y)
    *interpolated_y = priv->release_y + priv->interpolated_y;
}

/**
 * clutter_pan_action_get_interpolated_delta:
 * @self: A #ClutterPanAction
 * @delta_x: (out) (allow-none): return location for the X delta since
 *   the latest interpolated event
 * @delta_y: (out) (allow-none): return location for the Y delta since
 *   the latest interpolated event
 *
 * Retrieves the delta, in stage space, since the latest interpolated
 * event, analogous to clutter_gesture_action_get_motion_delta().
 *
 * Return value: the distance since the latest interpolated event
 *
 * Since: 1.12
 */
gfloat
clutter_pan_action_get_interpolated_delta (ClutterPanAction *self,
                                           gfloat           *delta_x,
                                           gfloat           *delta_y)
{
  ClutterPanActionPrivate *priv;

  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self), 0.0f);

  priv = self->priv;

  if (delta_x)
    *delta_x = priv->dx;

  if (delta_y)
    *delta_y = priv->dy;

  return sqrt ((priv->dx * priv->dx) + (priv->dy * priv->dy));
}

/**
 * clutter_pan_action_get_constrained_motion_delta:
 * @self: A #ClutterPanAction
 * @point: the touch point index, with 0 being the first touch
  *   point received by the action
 * @delta_x: (out) (optional): return location for the X delta
 * @delta_y: (out) (optional): return location for the Y delta
 *
 * Retrieves the delta, in stage space, dependent on the current state
 * of the #ClutterPanAction, and respecting the constraint specified by the
 * #ClutterPanAction:pan-axis property.
 *
 * Return value: the distance since last motion event
 *
 * Since: 1.24
 */
gfloat
clutter_pan_action_get_constrained_motion_delta (ClutterPanAction *self,
                                                 guint             point,
                                                 gfloat           *out_delta_x,
                                                 gfloat           *out_delta_y)
{
  ClutterPanActionPrivate *priv;
  gfloat delta_x = 0.f, delta_y = 0.f, distance;

  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self), 0.0f);

  priv = self->priv;

  distance = clutter_pan_action_get_motion_delta (self, point,
                                                  &delta_x,
                                                  &delta_y);

  switch (priv->pan_axis)
    {
    case CLUTTER_PAN_AXIS_NONE:
      break;

    case CLUTTER_PAN_AXIS_AUTO:
      if (priv->pin_state == SCROLL_PINNED_VERTICAL)
        delta_x = 0.0f;
      else if (priv->pin_state == SCROLL_PINNED_HORIZONTAL)
        delta_y = 0.0f;
      break;

    case CLUTTER_PAN_X_AXIS:
      delta_y = 0.0f;
      break;

    case CLUTTER_PAN_Y_AXIS:
      delta_x = 0.0f;
      break;
    }

  if (out_delta_x)
    *out_delta_x = delta_x;

  if (out_delta_y)
    *out_delta_y = delta_y;

  return distance;
}

/**
 * clutter_pan_action_get_motion_delta:
 * @self: A #ClutterPanAction
 * @point: the touch point index, with 0 being the first touch
 *   point received by the action
 * @delta_x: (out) (allow-none): return location for the X delta
 * @delta_y: (out) (allow-none): return location for the Y delta
 *
 * Retrieves the delta, in stage space, dependent on the current state
 * of the #ClutterPanAction. If it is inactive, both fields will be
 * set to 0. If it is panning by user action, the values will be equivalent
 * to those returned by clutter_gesture_action_get_motion_delta().
 * If it is interpolating with some form of kinetic scrolling, the values
 * will be equivalent to those returned by
 * clutter_pan_action_get_interpolated_delta(). This is a convenience
 * method designed to be used in replacement "pan" signal handlers.
 *
 * Since: 1.14
 */
gfloat
clutter_pan_action_get_motion_delta (ClutterPanAction *self,
                                     guint             point,
                                     gfloat           *delta_x,
                                     gfloat           *delta_y)
{
  ClutterPanActionPrivate *priv;

  g_return_val_if_fail (CLUTTER_IS_PAN_ACTION (self), 0.0f);

  priv = self->priv;

  switch (priv->state)
    {
    case PAN_STATE_INACTIVE:
      if (delta_x)
        *delta_x = 0;

      if (delta_y)
        *delta_y = 0;

      return 0;
    case PAN_STATE_PANNING:
      return clutter_gesture_action_get_motion_delta (CLUTTER_GESTURE_ACTION (self),
                                                      point, delta_x, delta_y);
    case PAN_STATE_INTERPOLATING:
      return clutter_pan_action_get_interpolated_delta (self, delta_x, delta_y);
    default:
      g_assert_not_reached ();
      return 0.0f;
    }
}

/**
 * clutter_pan_action_get_motion_coords:
 * @self: A #ClutterPanAction
 * @point: the touch point index, with 0 being the first touch
 *   point received by the action
 * @motion_x: (out) (allow-none): return location for the X coordinate
 * @motion_y: (out) (allow-none): return location for the Y coordinate
 *
 * Retrieves the coordinates, in stage space, dependent on the current state
 * of the #ClutterPanAction. If it is inactive, both fields will be
 * set to 0. If it is panning by user action, the values will be equivalent
 * to those returned by clutter_gesture_action_get_motion_coords().
 * If it is interpolating with some form of kinetic scrolling, the values
 * will be equivalent to those returned by
 * clutter_pan_action_get_interpolated_coords(). This is a convenience
 * method designed to be used in replacement "pan" signal handlers.
 *
 * Since: 1.14
 */
void
clutter_pan_action_get_motion_coords (ClutterPanAction *self,
                                      guint             point,
                                      gfloat           *motion_x,
                                      gfloat           *motion_y)
{
  ClutterPanActionPrivate *priv;

  g_return_if_fail (CLUTTER_IS_PAN_ACTION (self));

  priv = self->priv;

  switch (priv->state)
    {
    case PAN_STATE_INACTIVE:
      if (motion_x)
        *motion_x = 0;

      if (motion_y)
        *motion_y = 0;
      break;
    case PAN_STATE_PANNING:
      clutter_gesture_action_get_motion_coords (CLUTTER_GESTURE_ACTION (self),
                                                point, motion_x, motion_y);
      break;
    case PAN_STATE_INTERPOLATING:
      clutter_pan_action_get_interpolated_coords (self, motion_x, motion_y);
      break;
    default:
      g_assert_not_reached ();
    }
}
