/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

/*
 * Copyright (C) 2015 Red Hat
 *
 * 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Written by:
 *     Jonas Ådahl <jadahl@gmail.com>
 */

/**
 * SECTION:barrier-native
 * @Title: MetaBarrierImplNative
 * @Short_Description: Pointer barriers implementation for the native backend
 */

#include "config.h"

#include "backends/native/meta-barrier-native.h"

#include <stdlib.h>

#include "backends/meta-backend-private.h"
#include "backends/meta-barrier-private.h"
#include "backends/native/meta-seat-native.h"
#include "meta/barrier.h"
#include "meta/util.h"

struct _MetaBarrierManagerNative
{
  GHashTable *barriers;
  GMutex mutex;
};

typedef enum
{
  /* The barrier is active and responsive to pointer motion. */
  META_BARRIER_STATE_ACTIVE,

  /* An intermediate state after a pointer hit the pointer barrier. */
  META_BARRIER_STATE_HIT,

  /* The barrier was hit by a pointer and is still within the hit box and
   * has not been released.*/
  META_BARRIER_STATE_HELD,

  /* The pointer was released by the user. If the following motion hits
   * the barrier, it will pass through. */
  META_BARRIER_STATE_RELEASE,

  /* An intermediate state when the pointer has left the barrier. */
  META_BARRIER_STATE_LEFT,
} MetaBarrierState;

struct _MetaBarrierImplNative
{
  MetaBarrierImpl parent;

  MetaBarrier              *barrier;
  MetaBarrierManagerNative *manager;

  gboolean                  is_active;
  MetaBarrierState          state;
  int                       trigger_serial;
  guint32                   last_event_time;
  MetaBarrierDirection      blocked_dir;
  GMainContext             *main_context;
};

G_DEFINE_TYPE (MetaBarrierImplNative,
               meta_barrier_impl_native,
               META_TYPE_BARRIER_IMPL)

static int
next_serial (void)
{
  static int barrier_serial = 1;

  barrier_serial++;

  /* If it wraps, avoid 0 as it's not a valid serial. */
  if (barrier_serial == 0)
    barrier_serial++;

  return barrier_serial;
}

static gboolean
is_barrier_horizontal (MetaBarrier *barrier)
{
  return meta_border_is_horizontal (&barrier->priv->border);
}

static gboolean
is_barrier_blocking_directions (MetaBarrier         *barrier,
                                MetaBarrierDirection directions)
{
  return meta_border_is_blocking_directions (&barrier->priv->border,
                                             directions);
}

static void
dismiss_pointer (MetaBarrierImplNative *self)
{
  self->state = META_BARRIER_STATE_LEFT;
}

/*
 * Calculate the hit box for a held motion. The hit box is a 2 px wide region
 * in the opposite direction of every direction the barrier blocks. The purpose
 * of this is to allow small movements without receiving a "left" signal. This
 * heuristic comes from the X.org pointer barrier implementation.
 */
static MetaLine2
calculate_barrier_hit_box (MetaBarrier *barrier)
{
  MetaLine2 hit_box = barrier->priv->border.line;

  if (is_barrier_horizontal (barrier))
    {
      if (is_barrier_blocking_directions (barrier,
                                          META_BARRIER_DIRECTION_POSITIVE_Y))
        hit_box.a.y -= 2.0f;
      if (is_barrier_blocking_directions (barrier,
                                          META_BARRIER_DIRECTION_NEGATIVE_Y))
        hit_box.b.y += 2.0f;
    }
  else
    {
      if (is_barrier_blocking_directions (barrier,
                                          META_BARRIER_DIRECTION_POSITIVE_X))
        hit_box.a.x -= 2.0f;
      if (is_barrier_blocking_directions (barrier,
                                          META_BARRIER_DIRECTION_NEGATIVE_X))
        hit_box.b.x += 2.0f;
    }

  return hit_box;
}

static gboolean
is_within_box (MetaLine2   box,
               MetaVector2 point)
{
  return (point.x >= box.a.x && point.x < box.b.x &&
          point.y >= box.a.y && point.y < box.b.y);
}

static void
maybe_release_barrier (gpointer key,
                       gpointer value,
                       gpointer user_data)
{
  MetaBarrierImplNative *self = key;
  MetaBarrier *barrier = self->barrier;
  MetaLine2 *motion = user_data;
  MetaLine2 hit_box;

  if (self->state != META_BARRIER_STATE_HELD)
    return;

  /* Release if we end up outside barrier end points. */
  if (is_barrier_horizontal (barrier))
    {
      if (motion->b.x > MAX (barrier->priv->border.line.a.x,
                             barrier->priv->border.line.b.x) ||
          motion->b.x < MIN (barrier->priv->border.line.a.x,
                             barrier->priv->border.line.b.x))
        {
          dismiss_pointer (self);
          return;
        }
    }
  else
    {
      if (motion->b.y > MAX (barrier->priv->border.line.a.y,
                             barrier->priv->border.line.b.y) ||
          motion->b.y < MIN (barrier->priv->border.line.a.y,
                             barrier->priv->border.line.b.y))
        {
          dismiss_pointer (self);
          return;
        }
    }

  /* Release if we don't intersect and end up outside of hit box. */
  hit_box = calculate_barrier_hit_box (barrier);
  if (!is_within_box (hit_box, motion->b))
    {
      dismiss_pointer (self);
      return;
    }
}

static void
maybe_release_barriers (MetaBarrierManagerNative *manager,
                        float                     prev_x,
                        float                     prev_y,
                        float                     x,
                        float                     y)
{
  MetaLine2 motion = {
    .a = {
      .x = prev_x,
      .y = prev_y,
    },
    .b = {
      .x = x,
      .y = y,
    },
  };

  g_hash_table_foreach (manager->barriers,
                        maybe_release_barrier,
                        &motion);
}

typedef struct _MetaClosestBarrierData
{
  struct
  {
    MetaLine2                   motion;
    MetaBarrierDirection        directions;
  } in;

  struct
  {
    float                       closest_distance_2;
    MetaBarrierImplNative      *barrier_impl;
  } out;
} MetaClosestBarrierData;

static void
update_closest_barrier (gpointer key,
                        gpointer value,
                        gpointer user_data)
{
  MetaBarrierImplNative *self = key;
  MetaBarrier *barrier = self->barrier;
  MetaClosestBarrierData *data = user_data;
  MetaVector2 intersection;
  float dx, dy;
  float distance_2;

  /* Ignore if the barrier is not blocking in any of the motions directions. */
  if (!is_barrier_blocking_directions (barrier, data->in.directions))
    return;

  /* Ignore if the barrier released the pointer. */
  if (self->state == META_BARRIER_STATE_RELEASE)
    return;

  /* Ignore if we are moving away from barrier. */
  if (self->state == META_BARRIER_STATE_HELD &&
      (data->in.directions & self->blocked_dir) == 0)
    return;

  /* Check if the motion intersects with the barrier, and retrieve the
   * intersection point if any. */
  if (!meta_line2_intersects_with (&barrier->priv->border.line,
                                   &data->in.motion,
                                   &intersection))
    return;

  /* Calculate the distance to the barrier and keep track of the closest
   * barrier. */
  dx = intersection.x - data->in.motion.a.x;
  dy = intersection.y - data->in.motion.a.y;
  distance_2 = dx*dx + dy*dy;
  if (data->out.barrier_impl == NULL ||
      distance_2 < data->out.closest_distance_2)
    {
      data->out.barrier_impl = self;
      data->out.closest_distance_2 = distance_2;
    }
}

static gboolean
get_closest_barrier (MetaBarrierManagerNative *manager,
                     float                     prev_x,
                     float                     prev_y,
                     float                     x,
                     float                     y,
                     MetaBarrierDirection      motion_dir,
                     MetaBarrierImplNative   **barrier_impl)
{
  MetaClosestBarrierData closest_barrier_data;

  closest_barrier_data = (MetaClosestBarrierData) {
    .in = {
      .motion = {
        .a = {
          .x = prev_x,
          .y = prev_y,
        },
        .b = {
          .x = x,
          .y = y,
        },
      },
      .directions = motion_dir,
    },
  };

  g_hash_table_foreach (manager->barriers,
                        update_closest_barrier,
                        &closest_barrier_data);

  if (closest_barrier_data.out.barrier_impl != NULL)
    {
      *barrier_impl = closest_barrier_data.out.barrier_impl;
      return TRUE;
    }
  else
    {
      return FALSE;
    }
}

typedef struct _MetaBarrierEventData
{
  guint32             time;
  float               prev_x;
  float               prev_y;
  float               x;
  float               y;
  float               dx;
  float               dy;
} MetaBarrierEventData;

typedef struct
{
  MetaBarrierEvent *event;
  MetaBarrier *barrier;
  MetaBarrierState state;
} MetaBarrierIdleData;

static gboolean
emit_event_idle (MetaBarrierIdleData *idle_data)
{
  if (idle_data->state == META_BARRIER_STATE_HELD)
    _meta_barrier_emit_hit_signal (idle_data->barrier, idle_data->event);
  else
    _meta_barrier_emit_left_signal (idle_data->barrier, idle_data->event);

  meta_barrier_event_unref (idle_data->event);

  return G_SOURCE_REMOVE;
}

static void
queue_event (MetaBarrierImplNative *self,
             MetaBarrierEvent      *event)
{
  MetaBarrierIdleData *idle_data;
  GSource *source;

  idle_data = g_new0 (MetaBarrierIdleData, 1);
  idle_data->state = self->state;
  idle_data->barrier = self->barrier;
  idle_data->event = event;

  source = g_idle_source_new ();
  g_source_set_priority (source, G_PRIORITY_HIGH);
  g_source_set_callback (source,
                         (GSourceFunc) emit_event_idle,
                         idle_data,
                         g_free);

  g_source_attach (source, self->main_context);
  g_source_unref (source);
}

static void
emit_barrier_event (MetaBarrierImplNative *self,
                    guint32                time,
                    float                  prev_x,
                    float                  prev_y,
                    float                  x,
                    float                  y,
                    float                  dx,
                    float                  dy)
{
  MetaBarrierEvent *event = g_new0 (MetaBarrierEvent, 1);
  MetaBarrierState old_state = self->state;

  switch (self->state)
    {
    case META_BARRIER_STATE_HIT:
      self->state = META_BARRIER_STATE_HELD;
      self->trigger_serial = next_serial ();
      event->dt = 0;

      break;
    case META_BARRIER_STATE_RELEASE:
    case META_BARRIER_STATE_LEFT:
      self->state = META_BARRIER_STATE_ACTIVE;

      G_GNUC_FALLTHROUGH;
    case META_BARRIER_STATE_HELD:
      event->dt = time - self->last_event_time;

      break;
    case META_BARRIER_STATE_ACTIVE:
      g_assert_not_reached (); /* Invalid state. */
    }

  event->ref_count = 1;
  event->event_id = self->trigger_serial;
  event->time = time;

  event->x = x;
  event->y = y;
  event->dx = dx;
  event->dy = dy;

  event->grabbed = self->state == META_BARRIER_STATE_HELD;
  event->released = old_state == META_BARRIER_STATE_RELEASE;

  self->last_event_time = time;

  queue_event (self, event);
}

static void
maybe_emit_barrier_event (gpointer key, gpointer value, gpointer user_data)
{
  MetaBarrierImplNative *self = key;
  MetaBarrierEventData *data = user_data;

  switch (self->state)
    {
    case META_BARRIER_STATE_ACTIVE:
      break;
    case META_BARRIER_STATE_HIT:
    case META_BARRIER_STATE_HELD:
    case META_BARRIER_STATE_RELEASE:
    case META_BARRIER_STATE_LEFT:
      emit_barrier_event (self,
                          data->time,
                          data->prev_x,
                          data->prev_y,
                          data->x,
                          data->y,
                          data->dx,
                          data->dy);
      break;
    }
}

/* Clamp (x, y) to the barrier and remove clamped direction from motion_dir. */
static void
clamp_to_barrier (MetaBarrierImplNative *self,
                  MetaBarrierDirection *motion_dir,
                  float *x,
                  float *y)
{
  MetaBarrier *barrier = self->barrier;

  if (is_barrier_horizontal (barrier))
    {
      if (*motion_dir & META_BARRIER_DIRECTION_POSITIVE_Y)
        *y = barrier->priv->border.line.a.y;
      else if (*motion_dir & META_BARRIER_DIRECTION_NEGATIVE_Y)
        *y = barrier->priv->border.line.a.y;

      self->blocked_dir = *motion_dir & (META_BARRIER_DIRECTION_POSITIVE_Y |
                                         META_BARRIER_DIRECTION_NEGATIVE_Y);
      *motion_dir &= ~(META_BARRIER_DIRECTION_POSITIVE_Y |
                       META_BARRIER_DIRECTION_NEGATIVE_Y);
    }
  else
    {
      if (*motion_dir & META_BARRIER_DIRECTION_POSITIVE_X)
        *x = barrier->priv->border.line.a.x;
      else if (*motion_dir & META_BARRIER_DIRECTION_NEGATIVE_X)
        *x = barrier->priv->border.line.a.x;

      self->blocked_dir = *motion_dir & (META_BARRIER_DIRECTION_POSITIVE_X |
                                         META_BARRIER_DIRECTION_NEGATIVE_X);
      *motion_dir &= ~(META_BARRIER_DIRECTION_POSITIVE_X |
                       META_BARRIER_DIRECTION_NEGATIVE_X);
    }

  self->state = META_BARRIER_STATE_HIT;
}

void
meta_barrier_manager_native_process_in_impl (MetaBarrierManagerNative *manager,
                                             ClutterInputDevice       *device,
                                             guint32                   time,
                                             float                    *x,
                                             float                    *y)
{
  graphene_point_t prev_pos;
  float prev_x;
  float prev_y;
  float orig_x = *x;
  float orig_y = *y;
  MetaBarrierDirection motion_dir = 0;
  MetaBarrierEventData barrier_event_data;
  MetaBarrierImplNative *barrier_impl;

  if (!clutter_seat_query_state (clutter_input_device_get_seat (device),
                                 device, NULL, &prev_pos, NULL))
    return;

  g_mutex_lock (&manager->mutex);

  prev_x = prev_pos.x;
  prev_y = prev_pos.y;

  /* Get the direction of the motion vector. */
  if (prev_x < *x)
    motion_dir |= META_BARRIER_DIRECTION_POSITIVE_X;
  else if (prev_x > *x)
    motion_dir |= META_BARRIER_DIRECTION_NEGATIVE_X;
  if (prev_y < *y)
    motion_dir |= META_BARRIER_DIRECTION_POSITIVE_Y;
  else if (prev_y > *y)
    motion_dir |= META_BARRIER_DIRECTION_NEGATIVE_Y;

  /* Clamp to the closest barrier in any direction until either there are no
   * more barriers to clamp to or all directions have been clamped. */
  while (motion_dir != 0)
    {
      if (get_closest_barrier (manager,
                               prev_x, prev_y,
                               *x, *y,
                               motion_dir,
                               &barrier_impl))
        clamp_to_barrier (barrier_impl, &motion_dir, x, y);
      else
        break;
    }

  /* Potentially release active barrier movements. */
  maybe_release_barriers (manager, prev_x, prev_y, *x, *y);

  /* Initiate or continue barrier interaction. */
  barrier_event_data = (MetaBarrierEventData) {
    .time = time,
    .prev_x = prev_x,
    .prev_y = prev_y,
    .x = *x,
    .y = *y,
    .dx = orig_x - prev_x,
    .dy = orig_y - prev_y,
  };

  g_hash_table_foreach (manager->barriers,
                        maybe_emit_barrier_event,
                        &barrier_event_data);

  g_mutex_unlock (&manager->mutex);
}

static gboolean
_meta_barrier_impl_native_is_active (MetaBarrierImpl *impl)
{
  MetaBarrierImplNative *self = META_BARRIER_IMPL_NATIVE (impl);

  return self->is_active;
}

static void
_meta_barrier_impl_native_release (MetaBarrierImpl  *impl,
                                   MetaBarrierEvent *event)
{
  MetaBarrierImplNative *self = META_BARRIER_IMPL_NATIVE (impl);

  if (self->state == META_BARRIER_STATE_HELD &&
      event->event_id == self->trigger_serial)
    self->state = META_BARRIER_STATE_RELEASE;
}

static void
_meta_barrier_impl_native_destroy (MetaBarrierImpl *impl)
{
  MetaBarrierImplNative *self = META_BARRIER_IMPL_NATIVE (impl);

  g_mutex_lock (&self->manager->mutex);
  g_hash_table_remove (self->manager->barriers, self);
  g_mutex_unlock (&self->manager->mutex);
  g_main_context_unref (self->main_context);
  self->is_active = FALSE;
}

MetaBarrierImpl *
meta_barrier_impl_native_new (MetaBarrier *barrier)
{
  MetaBarrierImplNative *self;
  MetaBarrierManagerNative *manager;
  ClutterBackend *backend = clutter_get_default_backend ();
  ClutterSeat *seat = clutter_backend_get_default_seat (backend);

  self = g_object_new (META_TYPE_BARRIER_IMPL_NATIVE, NULL);

  self->barrier = barrier;
  self->is_active = TRUE;
  self->main_context = g_main_context_ref_thread_default ();

  manager = meta_seat_native_get_barrier_manager (META_SEAT_NATIVE (seat));
  self->manager = manager;
  g_mutex_lock (&manager->mutex);
  g_hash_table_add (manager->barriers, self);
  g_mutex_unlock (&manager->mutex);

  return META_BARRIER_IMPL (self);
}

static void
meta_barrier_impl_native_class_init (MetaBarrierImplNativeClass *klass)
{
  MetaBarrierImplClass *impl_class = META_BARRIER_IMPL_CLASS (klass);

  impl_class->is_active = _meta_barrier_impl_native_is_active;
  impl_class->release = _meta_barrier_impl_native_release;
  impl_class->destroy = _meta_barrier_impl_native_destroy;
}

static void
meta_barrier_impl_native_init (MetaBarrierImplNative *self)
{
}

MetaBarrierManagerNative *
meta_barrier_manager_native_new (void)
{
  MetaBarrierManagerNative *manager;

  manager = g_new0 (MetaBarrierManagerNative, 1);

  manager->barriers = g_hash_table_new (NULL, NULL);
  g_mutex_init (&manager->mutex);

  return manager;
}
