/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
#include "config.h"
#include 
#include 
#include 
#include "libpikamath/pikamath.h"
#include "display-types.h"
#include "core/pikacoords.h"
#include "core/pikacoords-interpolate.h"
#include "core/pikamarshal.h"
#include "pikamotionbuffer.h"
/* Velocity unit is screen pixels per millisecond we pass to tools as 1. */
#define VELOCITY_UNIT        3.0
#define EVENT_FILL_PRECISION 6.0
#define DIRECTION_RADIUS     (1.0 / MAX (scale_x, scale_y))
#define SMOOTH_FACTOR        0.3
enum
{
  PROP_0
};
enum
{
  STROKE,
  HOVER,
  LAST_SIGNAL
};
/*  local function prototypes  */
static void     pika_motion_buffer_dispose             (GObject          *object);
static void     pika_motion_buffer_finalize            (GObject          *object);
static void     pika_motion_buffer_set_property        (GObject          *object,
                                                        guint             property_id,
                                                        const GValue     *value,
                                                        GParamSpec       *pspec);
static void     pika_motion_buffer_get_property        (GObject          *object,
                                                        guint             property_id,
                                                        GValue           *value,
                                                        GParamSpec       *pspec);
static void     pika_motion_buffer_push_event_history  (PikaMotionBuffer *buffer,
                                                        const PikaCoords *coords);
static void     pika_motion_buffer_pop_event_queue     (PikaMotionBuffer *buffer,
                                                        PikaCoords       *coords);
static void     pika_motion_buffer_interpolate_stroke  (PikaMotionBuffer *buffer,
                                                        PikaCoords       *coords);
static gboolean pika_motion_buffer_event_queue_timeout (PikaMotionBuffer *buffer);
G_DEFINE_TYPE (PikaMotionBuffer, pika_motion_buffer, PIKA_TYPE_OBJECT)
#define parent_class pika_motion_buffer_parent_class
static guint motion_buffer_signals[LAST_SIGNAL] = { 0 };
static void
pika_motion_buffer_class_init (PikaMotionBufferClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  motion_buffer_signals[STROKE] =
    g_signal_new ("stroke",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaMotionBufferClass, stroke),
                  NULL, NULL,
                  pika_marshal_VOID__POINTER_UINT_FLAGS,
                  G_TYPE_NONE, 3,
                  G_TYPE_POINTER,
                  G_TYPE_UINT,
                  GDK_TYPE_MODIFIER_TYPE);
  motion_buffer_signals[HOVER] =
    g_signal_new ("hover",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaMotionBufferClass, hover),
                  NULL, NULL,
                  pika_marshal_VOID__POINTER_FLAGS_BOOLEAN,
                  G_TYPE_NONE, 3,
                  G_TYPE_POINTER,
                  GDK_TYPE_MODIFIER_TYPE,
                  G_TYPE_BOOLEAN);
  object_class->dispose      = pika_motion_buffer_dispose;
  object_class->finalize     = pika_motion_buffer_finalize;
  object_class->set_property = pika_motion_buffer_set_property;
  object_class->get_property = pika_motion_buffer_get_property;
}
static void
pika_motion_buffer_init (PikaMotionBuffer *buffer)
{
  buffer->event_history = g_array_new (FALSE, FALSE, sizeof (PikaCoords));
  buffer->event_queue   = g_array_new (FALSE, FALSE, sizeof (PikaCoords));
}
static void
pika_motion_buffer_dispose (GObject *object)
{
  PikaMotionBuffer *buffer = PIKA_MOTION_BUFFER (object);
  if (buffer->event_delay_timeout)
    {
      g_source_remove (buffer->event_delay_timeout);
      buffer->event_delay_timeout = 0;
    }
  G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
pika_motion_buffer_finalize (GObject *object)
{
  PikaMotionBuffer *buffer = PIKA_MOTION_BUFFER (object);
  if (buffer->event_history)
    {
      g_array_free (buffer->event_history, TRUE);
      buffer->event_history = NULL;
    }
  if (buffer->event_queue)
    {
      g_array_free (buffer->event_queue, TRUE);
      buffer->event_queue = NULL;
    }
  G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
pika_motion_buffer_set_property (GObject      *object,
                                 guint         property_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  switch (property_id)
    {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}
static void
pika_motion_buffer_get_property (GObject    *object,
                                 guint       property_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
  switch (property_id)
    {
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}
/*  public functions  */
PikaMotionBuffer *
pika_motion_buffer_new (void)
{
  return g_object_new (PIKA_TYPE_MOTION_BUFFER,
                       NULL);
}
void
pika_motion_buffer_begin_stroke (PikaMotionBuffer *buffer,
                                 guint32           time,
                                 PikaCoords       *last_motion)
{
  g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
  g_return_if_fail (last_motion != NULL);
  buffer->last_read_motion_time = time;
  *last_motion = buffer->last_coords;
}
void
pika_motion_buffer_end_stroke (PikaMotionBuffer *buffer)
{
  g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
  if (buffer->event_delay_timeout)
    {
      g_source_remove (buffer->event_delay_timeout);
      buffer->event_delay_timeout = 0;
    }
  pika_motion_buffer_event_queue_timeout (buffer);
}
/**
 * pika_motion_buffer_motion_event:
 * @buffer:
 * @coords:
 * @time:
 * @event_fill:
 *
 * This function evaluates the event to decide if the change is big
 * enough to need handling and returns FALSE, if change is less than
 * set filter level taking a whole lot of load off any draw tools that
 * have no use for these events anyway. If the event is seen fit at
 * first look, it is evaluated for speed and smoothed.  Due to lousy
 * time resolution of events pretty strong smoothing is applied to
 * timestamps for sensible speed result. This function is also ideal
 * for other event adjustment like pressure curve or calculating other
 * derived dynamics factors like angular velocity calculation from
 * tilt values, to allow for even more dynamic brushes. Calculated
 * distance to last event is stored in PikaCoords because its a
 * sideproduct of velocity calculation and is currently calculated in
 * each tool. If they were to use this distance, more resources on
 * recalculating the same value would be saved.
 *
 * Returns: %TRUE if the motion was significant enough to be
 *               processed, %FALSE otherwise.
 **/
gboolean
pika_motion_buffer_motion_event (PikaMotionBuffer *buffer,
                                 PikaCoords       *coords,
                                 guint32           time,
                                 gboolean          event_fill)
{
  gdouble  delta_time  = 0.001;
  gdouble  delta_x     = 0.0;
  gdouble  delta_y     = 0.0;
  gdouble  distance    = 1.0;
  gdouble  scale_x     = coords->xscale;
  gdouble  scale_y     = coords->yscale;
  g_return_val_if_fail (PIKA_IS_MOTION_BUFFER (buffer), FALSE);
  g_return_val_if_fail (coords != NULL, FALSE);
  /*  the last_read_motion_time most be set unconditionally, so set
   *  it early
   */
  buffer->last_read_motion_time = time;
  delta_time = (buffer->last_motion_delta_time * (1 - SMOOTH_FACTOR) +
                (time - buffer->last_motion_time) * SMOOTH_FACTOR);
  if (buffer->last_motion_time == 0)
    {
      /*  First pair is invalid to do any velocity calculation, so we
       *  apply a constant value.
       */
      coords->velocity = 1.0;
    }
  else
    {
      PikaCoords last_dir_event = buffer->last_coords;
      gdouble    filter;
      gdouble    dist;
      gdouble    delta_dir;
      gdouble    dir_delta_x = 0.0;
      gdouble    dir_delta_y = 0.0;
      delta_x = last_dir_event.x - coords->x;
      delta_y = last_dir_event.y - coords->y;
      /*  Events with distances less than the screen resolution are
       *  not worth handling.
       */
      filter = MIN (1.0 / scale_x, 1.0 / scale_y) / 2.0;
      if (fabs (delta_x) < filter &&
          fabs (delta_y) < filter)
        {
          return FALSE;
        }
      distance = dist = sqrt (SQR (delta_x) + SQR (delta_y));
      /*  If even smoothed time resolution does not allow to guess for
       *  speed, use last velocity.
       */
      if (delta_time == 0)
        {
          coords->velocity = buffer->last_coords.velocity;
        }
      else
        {
          /*  We need to calculate the velocity in screen coordinates
           *  for human interaction
           */
          gdouble screen_distance = (distance * MIN (scale_x, scale_y));
          /* Calculate raw valocity */
          coords->velocity = ((screen_distance / delta_time) / VELOCITY_UNIT);
          /* Adding velocity dependent smoothing, feels better in tools. */
          coords->velocity = (buffer->last_coords.velocity *
                              (1 - MIN (SMOOTH_FACTOR, coords->velocity)) +
                              coords->velocity *
                              MIN (SMOOTH_FACTOR, coords->velocity));
          /* Speed needs upper limit */
          coords->velocity = MIN (coords->velocity, 1.0);
        }
      if (((fabs (delta_x) > DIRECTION_RADIUS) &&
           (fabs (delta_y) > DIRECTION_RADIUS)) ||
          (buffer->event_history->len < 4))
        {
          dir_delta_x = delta_x;
          dir_delta_y = delta_y;
        }
      else
        {
          gint x = CLAMP ((buffer->event_history->len - 1), 3, 15);
          while (((fabs (dir_delta_x) < DIRECTION_RADIUS) ||
                  (fabs (dir_delta_y) < DIRECTION_RADIUS)) &&
                 (x >= 0))
            {
              last_dir_event = g_array_index (buffer->event_history,
                                              PikaCoords, x);
              dir_delta_x = last_dir_event.x - coords->x;
              dir_delta_y = last_dir_event.y - coords->y;
              x--;
            }
        }
      if ((fabs (dir_delta_x) < DIRECTION_RADIUS) ||
          (fabs (dir_delta_y) < DIRECTION_RADIUS))
        {
          coords->direction = buffer->last_coords.direction;
        }
      else
        {
          coords->direction = pika_coords_direction (&last_dir_event, coords);
        }
      coords->direction = coords->direction - floor (coords->direction);
      delta_dir = coords->direction - buffer->last_coords.direction;
      if (delta_dir < -0.5)
        {
          coords->direction = (0.5 * coords->direction +
                               0.5 * (buffer->last_coords.direction - 1.0));
        }
      else if (delta_dir > 0.5)
        {
          coords->direction = (0.5 * coords->direction +
                               0.5 * (buffer->last_coords.direction + 1.0));
        }
      else
        {
          coords->direction = (0.5 * coords->direction +
                               0.5 * buffer->last_coords.direction);
        }
      coords->direction = coords->direction - floor (coords->direction);
      /* do event fill for devices that do not provide enough events */
      if (distance >= EVENT_FILL_PRECISION &&
          event_fill                       &&
          buffer->event_history->len >= 2)
        {
          if (buffer->event_delay)
            {
              pika_motion_buffer_interpolate_stroke (buffer, coords);
            }
          else
            {
              buffer->event_delay = TRUE;
              pika_motion_buffer_push_event_history (buffer, coords);
            }
        }
      else
        {
          if (buffer->event_delay)
            buffer->event_delay = FALSE;
          pika_motion_buffer_push_event_history (buffer, coords);
        }
#ifdef EVENT_VERBOSE
      g_printerr ("DIST: %f, DT:%f, Vel:%f, Press:%f,smooth_dd:%f, POS: (%f, %f)\n",
                  distance,
                  delta_time,
                  buffer->last_coords.velocity,
                  coords->pressure,
                  distance - dist,
                  coords->x,
                  coords->y);
#endif
    }
  g_array_append_val (buffer->event_queue, *coords);
  buffer->last_coords            = *coords;
  buffer->last_motion_time       = time;
  buffer->last_motion_delta_time = delta_time;
  buffer->last_motion_delta_x    = delta_x;
  buffer->last_motion_delta_y    = delta_y;
  buffer->last_motion_distance   = distance;
  return TRUE;
}
guint32
pika_motion_buffer_get_last_motion_time (PikaMotionBuffer *buffer)
{
  g_return_val_if_fail (PIKA_IS_MOTION_BUFFER (buffer), 0);
  return buffer->last_read_motion_time;
}
void
pika_motion_buffer_request_stroke (PikaMotionBuffer *buffer,
                                   GdkModifierType   state,
                                   guint32           time)
{
  GdkModifierType  event_state;
  gint             keep = 0;
  g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
  if (buffer->event_delay)
    {
      /* If we are in delay we use LAST state, not current */
      event_state = buffer->last_active_state;
      keep = 1; /* Holding one event in buf */
    }
  else
    {
      /* Save the state */
      event_state = state;
    }
  if (buffer->event_delay_timeout)
    {
      g_source_remove (buffer->event_delay_timeout);
      buffer->event_delay_timeout = 0;
    }
  buffer->last_active_state = state;
  while (buffer->event_queue->len > keep)
    {
      PikaCoords buf_coords;
      pika_motion_buffer_pop_event_queue (buffer, &buf_coords);
      g_signal_emit (buffer, motion_buffer_signals[STROKE], 0,
                     &buf_coords, time, event_state);
    }
  if (buffer->event_delay)
    {
      buffer->event_delay_timeout =
        g_timeout_add (50,
                       (GSourceFunc) pika_motion_buffer_event_queue_timeout,
                       buffer);
    }
}
void
pika_motion_buffer_request_hover (PikaMotionBuffer *buffer,
                                  GdkModifierType   state,
                                  gboolean          proximity)
{
  g_return_if_fail (PIKA_IS_MOTION_BUFFER (buffer));
  if (buffer->event_queue->len > 0)
    {
      PikaCoords buf_coords = g_array_index (buffer->event_queue,
                                             PikaCoords,
                                             buffer->event_queue->len - 1);
      g_signal_emit (buffer, motion_buffer_signals[HOVER], 0,
                     &buf_coords, state, proximity);
      g_array_set_size (buffer->event_queue, 0);
    }
}
/*  private functions  */
static void
pika_motion_buffer_push_event_history (PikaMotionBuffer *buffer,
                                       const PikaCoords *coords)
{
  if (buffer->event_history->len == 4)
    g_array_remove_index (buffer->event_history, 0);
  g_array_append_val (buffer->event_history, *coords);
}
static void
pika_motion_buffer_pop_event_queue (PikaMotionBuffer *buffer,
                                    PikaCoords       *coords)
{
  *coords = g_array_index (buffer->event_queue, PikaCoords, 0);
  g_array_remove_index (buffer->event_queue, 0);
}
static void
pika_motion_buffer_interpolate_stroke (PikaMotionBuffer *buffer,
                                       PikaCoords       *coords)
{
  PikaCoords  catmull[4];
  GArray     *ret_coords;
  gint        i = buffer->event_history->len - 1;
  /* Note that there must be exactly one event in buffer or bad things
   * can happen. This must never get called under other circumstances.
   */
  ret_coords = g_array_new (FALSE, FALSE, sizeof (PikaCoords));
  catmull[0] = g_array_index (buffer->event_history, PikaCoords, i - 1);
  catmull[1] = g_array_index (buffer->event_history, PikaCoords, i);
  catmull[2] = g_array_index (buffer->event_queue,   PikaCoords, 0);
  catmull[3] = *coords;
  pika_coords_interpolate_catmull (catmull, EVENT_FILL_PRECISION / 2,
                                   ret_coords, NULL);
  /* Push the last actual event in history */
  pika_motion_buffer_push_event_history (buffer,
                                         &g_array_index (buffer->event_queue,
                                                         PikaCoords, 0));
  g_array_set_size (buffer->event_queue, 0);
  g_array_append_vals (buffer->event_queue,
                       &g_array_index (ret_coords, PikaCoords, 0),
                       ret_coords->len);
  g_array_free (ret_coords, TRUE);
}
static gboolean
pika_motion_buffer_event_queue_timeout (PikaMotionBuffer *buffer)
{
  buffer->event_delay         = FALSE;
  buffer->event_delay_timeout = 0;
  if (buffer->event_queue->len > 0)
    {
      PikaCoords last_coords = g_array_index (buffer->event_queue,
                                              PikaCoords,
                                              buffer->event_queue->len - 1);
      pika_motion_buffer_push_event_history (buffer, &last_coords);
      pika_motion_buffer_request_stroke (buffer,
                                         buffer->last_active_state,
                                         buffer->last_read_motion_time);
    }
  return FALSE;
}