/* -*- 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:meta-pointer-confinement-wayland
 * @title: MetaPointerConfinementWayland
 * @short_description: A #MetaPointerConstraint implementing pointer confinement
 *
 * A MetaPointerConfinementConstraint implements the client pointer constraint
 * "pointer confinement": the cursor should not be able to "break out" of a
 * certain area defined by the client requesting it.
 */

#include "config.h"

#include "wayland/meta-pointer-confinement-wayland.h"

#include <glib-object.h>
#include <cairo.h>

#include "backends/meta-backend-private.h"
#include "backends/meta-pointer-constraint.h"
#include "compositor/meta-surface-actor-wayland.h"
#include "core/meta-border.h"
#include "wayland/meta-wayland-pointer-constraints.h"
#include "wayland/meta-wayland-pointer.h"
#include "wayland/meta-wayland-seat.h"
#include "wayland/meta-wayland-surface.h"

struct _MetaPointerConfinementWayland
{
  MetaPointerConstraint parent;

  MetaWaylandPointerConstraint *constraint;
};

typedef struct _MetaBox
{
  int x1;
  int y1;
  int x2;
  int y2;
} MetaBox;

G_DEFINE_TYPE (MetaPointerConfinementWayland, meta_pointer_confinement_wayland,
               META_TYPE_POINTER_CONSTRAINT);

static MetaBorder *
add_border (GArray *borders,
            float x1, float y1,
            float x2, float y2,
            MetaBorderMotionDirection blocking_directions)
{
  MetaBorder border;

  border = (MetaBorder) {
    .line = (MetaLine2) {
      .a = (MetaVector2) {
        .x = x1,
        .y = y1,
      },
      .b = (MetaVector2) {
        .x = x2,
        .y = y2,
      },
    },
    .blocking_directions = blocking_directions,
  };

  g_array_append_val (borders, border);

  return &g_array_index (borders, MetaBorder, borders->len - 1);
}

static gint
compare_lines_x (gconstpointer a, gconstpointer b)
{
  const MetaBorder *border_a = a;
  const MetaBorder *border_b = b;

  if (border_a->line.a.x == border_b->line.a.x)
    return border_a->line.b.x < border_b->line.b.x;
  else
    return border_a->line.a.x > border_b->line.a.x;
}

static void
add_non_overlapping_edges (MetaBox     *boxes,
                           unsigned int band_above_start,
                           unsigned int band_below_start,
                           unsigned int band_below_end,
                           GArray      *borders)
{
  unsigned int i;
  GArray *band_merge;
  MetaBorder *border;
  MetaBorder *prev_border;
  MetaBorder *new_border;

  band_merge = g_array_new (FALSE, FALSE, sizeof *border);

  /* Add bottom band of previous row, and top band of current row, and
   * sort them so lower left x coordinate comes first. If there are two
   * borders with the same left x coordinate, the wider one comes first.
   */
  for (i = band_above_start; i < band_below_start; i++)
    {
      MetaBox *box = &boxes[i];
      add_border (band_merge, box->x1, box->y2, box->x2, box->y2,
                  META_BORDER_MOTION_DIRECTION_POSITIVE_Y);
    }
  for (i = band_below_start; i < band_below_end; i++)
    {
      MetaBox *box= &boxes[i];
      add_border (band_merge, box->x1, box->y1, box->x2, box->y1,
                  META_BORDER_MOTION_DIRECTION_NEGATIVE_Y);
    }
  g_array_sort (band_merge, compare_lines_x);

  /* Combine the two combined bands so that any overlapping border is
   * eliminated. */
  prev_border = NULL;
  for (i = 0; i < band_merge->len; i++)
    {
      border = &g_array_index (band_merge, MetaBorder, i);

      g_assert (border->line.a.y == border->line.b.y);
      g_assert (!prev_border ||
                prev_border->line.a.y == border->line.a.y);
      g_assert (!prev_border ||
                (prev_border->line.a.x != border->line.a.x ||
                 prev_border->line.b.x != border->line.b.x));
      g_assert (!prev_border ||
                prev_border->line.a.x <= border->line.a.x);

      if (prev_border &&
          prev_border->line.a.x == border->line.a.x)
        {
          /*
           * ------------ +
           * -------      =
           * [     ]-----
           */
          prev_border->line.a.x = border->line.b.x;
        }
      else if (prev_border &&
               prev_border->line.b.x == border->line.b.x)
        {
          /*
           * ------------ +
           *       ------ =
           * ------[    ]
           */
          prev_border->line.b.x = border->line.a.x;
        }
      else if (prev_border &&
               prev_border->line.b.x == border->line.a.x)
        {
          /*
           * --------        +
           *         ------  =
           * --------------
           */
          prev_border->line.b.x = border->line.b.x;
        }
      else if (prev_border &&
               prev_border->line.b.x >= border->line.a.x)
        {
          /*
           * --------------- +
           *      ------     =
           * -----[    ]----
           */
          new_border = add_border (borders,
                                   border->line.b.x,
                                   border->line.b.y,
                                   prev_border->line.b.x,
                                   prev_border->line.b.y,
                                   prev_border->blocking_directions);
          prev_border->line.b.x = border->line.a.x;
          prev_border = new_border;
        }
      else
        {
          g_assert (!prev_border ||
                    prev_border->line.b.x < border->line.a.x);
          /*
           * First border or non-overlapping.
           *
           * -----           +
           *        -----    =
           * -----  -----
           */
          g_array_append_val (borders, *border);
          prev_border = &g_array_index (borders, MetaBorder, borders->len - 1);
        }
    }

  g_array_free (band_merge, FALSE);
}

static void
add_band_bottom_edges (MetaBox *boxes,
                       int      band_start,
                       int      band_end,
                       GArray  *borders)
{
  int i;

  for (i = band_start; i < band_end; i++)
    {
      add_border (borders,
                  boxes[i].x1, boxes[i].y2,
                  boxes[i].x2, boxes[i].y2,
                  META_BORDER_MOTION_DIRECTION_POSITIVE_Y);
    }
}

static void
region_to_outline (cairo_region_t *region,
                   GArray         *borders)
{
  MetaBox *boxes;
  int num_boxes;
  int i;
  int top_most, bottom_most;
  int current_roof;
  int prev_top;
  int band_start, prev_band_start;

  /*
   * Remove any overlapping lines from the set of rectangles. Note that
   * pixman regions are grouped as rows of rectangles, where rectangles
   * in one row never touch or overlap and are all of the same height.
   *
   *             -------- ---                   -------- ---
   *             |      | | |                   |      | | |
   *   ----------====---- ---         -----------  ----- ---
   *   |            |            =>   |            |
   *   ----==========---------        -----        ----------
   *       |                 |            |                 |
   *       -------------------            -------------------
   *
   */

  num_boxes  = cairo_region_num_rectangles (region);
  boxes = g_new (MetaBox, num_boxes);
  for (i = 0; i < num_boxes; i++)
    {
      cairo_rectangle_int_t rect;
      cairo_region_get_rectangle (region, i, &rect);
      boxes[i] = (MetaBox) {
        .x1 = rect.x,
        .y1 = rect.y,
        .x2 = rect.x + rect.width,
        .y2 = rect.y + rect.height,
      };
    }
  prev_top = 0;
  top_most = boxes[0].y1;
  current_roof = top_most;
  bottom_most = boxes[num_boxes - 1].y2;
  band_start = 0;
  prev_band_start = 0;
  for (i = 0; i < num_boxes; i++)
    {
      /* Detect if there is a vertical empty space, and add the lower
       * level of the previous band if so was the case. */
      if (i > 0 &&
          boxes[i].y1 != prev_top &&
          boxes[i].y1 != boxes[i - 1].y2)
        {
          current_roof = boxes[i].y1;
          add_band_bottom_edges (boxes,
                                 band_start,
                                 i,
                                 borders);
        }

      /* Special case adding the last band, since it won't be handled
       * by the band change detection below. */
      if (boxes[i].y1 != current_roof && i == num_boxes - 1)
        {
          if (boxes[i].y1 != prev_top)
            {
              /* The last band is a single box, so we don't
               * have a prev_band_start to tell us when the
               * previous band started. */
              add_non_overlapping_edges (boxes,
                                         band_start,
                                         i,
                                         i + 1,
                                         borders);
            }
          else
            {
              add_non_overlapping_edges (boxes,
                                         prev_band_start,
                                         band_start,
                                         i + 1,
                                         borders);
            }
        }

      /* Detect when passing a band and combine the top border of the
       * just passed band with the bottom band of the previous band.
       */
      if (boxes[i].y1 != top_most && boxes[i].y1 != prev_top)
        {
          /* Combine the two passed bands. */
          if (prev_top != current_roof)
            {
              add_non_overlapping_edges (boxes,
                                         prev_band_start,
                                         band_start,
                                         i,
                                         borders);
            }

          prev_band_start = band_start;
          band_start = i;
        }

      /* Add the top border if the box is part of the current roof. */
      if (boxes[i].y1 == current_roof)
        {
          add_border (borders,
                      boxes[i].x1, boxes[i].y1,
                      boxes[i].x2, boxes[i].y1,
                      META_BORDER_MOTION_DIRECTION_NEGATIVE_Y);
        }

      /* Add the bottom border of the last band. */
      if (boxes[i].y2 == bottom_most)
        {
          add_border (borders,
                      boxes[i].x1, boxes[i].y2,
                      boxes[i].x2, boxes[i].y2,
                      META_BORDER_MOTION_DIRECTION_POSITIVE_Y);
        }

      /* Always add the left border. */
      add_border (borders,
                  boxes[i].x1, boxes[i].y1,
                  boxes[i].x1, boxes[i].y2,
                  META_BORDER_MOTION_DIRECTION_NEGATIVE_X);

      /* Always add the right border. */
      add_border (borders,
                  boxes[i].x2, boxes[i].y1,
                  boxes[i].x2, boxes[i].y2,
                  META_BORDER_MOTION_DIRECTION_POSITIVE_X);

      prev_top = boxes[i].y1;
    }

  g_free (boxes);
}

static MetaBorder *
get_closest_border (GArray    *borders,
                    MetaLine2 *motion,
                    uint32_t   directions)
{
  MetaBorder *border;
  MetaVector2 intersection;
  MetaVector2 delta;
  float distance_2;
  MetaBorder *closest_border = NULL;
  float closest_distance_2 = DBL_MAX;
  unsigned int i;

  for (i = 0; i < borders->len; i++)
    {
      border = &g_array_index (borders, MetaBorder, i);

      if (!meta_border_is_blocking_directions (border, directions))
        continue;

      if (!meta_line2_intersects_with (&border->line, motion, &intersection))
        continue;

      delta = meta_vector2_subtract (intersection, motion->a);
      distance_2 = delta.x*delta.x + delta.y*delta.y;
      if (distance_2 < closest_distance_2)
        {
          closest_border = border;
          closest_distance_2 = distance_2;
        }
    }

  return closest_border;
}

static void
clamp_to_border (MetaBorder *border,
                 MetaLine2  *motion,
                 uint32_t   *motion_dir)
{
  /*
   * When clamping either rightward or downward motions, the motion needs to be
   * clamped so that the destination coordinate does not end up on the border
   * (see weston_pointer_clamp_event_to_region). Do this by clamping such
   * motions to the border minus the smallest possible wl_fixed_t value.
   *
   * When clamping in either leftward or upward motion, the resulting coordinate
   * needs to be clamped so that it is enough on the inside to avoid the
   * inaccuracies of clutter's stage to actor transformation algorithm (the one
   * used in clutter_actor_transform_stage_point) to make it end up outside the
   * next motion. It also needs to be clamped so that to the wl_fixed_t
   * coordinate may still be right on the border (i.e. at .0). Testing shows
   * that the smallest wl_fixed_t value divided by 10 is small enough to make
   * the wl_fixed_t coordinate .0 and large enough to avoid the inaccuracies of
   * clutters transform algorithm.
   */
  if (meta_border_is_horizontal (border))
    {
      if (*motion_dir & META_BORDER_MOTION_DIRECTION_POSITIVE_Y)
        motion->b.y = border->line.a.y - wl_fixed_to_double (1);
      else
        motion->b.y = border->line.a.y + wl_fixed_to_double (1) / 10;
      *motion_dir &= ~(META_BORDER_MOTION_DIRECTION_POSITIVE_Y |
                       META_BORDER_MOTION_DIRECTION_NEGATIVE_Y);
    }
  else
    {
      if (*motion_dir & META_BORDER_MOTION_DIRECTION_POSITIVE_X)
        motion->b.x = border->line.a.x - wl_fixed_to_double (1);
      else
        motion->b.x = border->line.a.x + wl_fixed_to_double (1) / 10;
      *motion_dir &= ~(META_BORDER_MOTION_DIRECTION_POSITIVE_X |
                       META_BORDER_MOTION_DIRECTION_NEGATIVE_X);
    }
}

static uint32_t
get_motion_directions (MetaLine2 *motion)
{
  uint32_t directions = 0;

  if (motion->a.x < motion->b.x)
    directions |= META_BORDER_MOTION_DIRECTION_POSITIVE_X;
  else if (motion->a.x > motion->b.x)
    directions |= META_BORDER_MOTION_DIRECTION_NEGATIVE_X;
  if (motion->a.y < motion->b.y)
    directions |= META_BORDER_MOTION_DIRECTION_POSITIVE_Y;
  else if (motion->a.y > motion->b.y)
    directions |= META_BORDER_MOTION_DIRECTION_NEGATIVE_Y;

  return directions;
}

static void
meta_pointer_confinement_wayland_constrain (MetaPointerConstraint *constraint,
                                            ClutterInputDevice    *device,
                                            guint32                time,
                                            float                  prev_x,
                                            float                  prev_y,
                                            float                  *x,
                                            float                  *y)
{
  MetaPointerConfinementWayland *self =
    META_POINTER_CONFINEMENT_WAYLAND (constraint);
  MetaWaylandSurface *surface;
  cairo_region_t *region;
  float sx, sy;
  float prev_sx, prev_sy;
  GArray *borders;
  MetaLine2 motion;
  MetaBorder *closest_border;
  uint32_t directions;

  surface = meta_wayland_pointer_constraint_get_surface (self->constraint);

  meta_wayland_surface_get_relative_coordinates (surface, *x, *y, &sx, &sy);
  meta_wayland_surface_get_relative_coordinates (surface, prev_x, prev_y,
                                                 &prev_sx, &prev_sy);

  /* For motions in a positive direction on any axis, append the smallest
   * possible value representable in a Wayland absolute coordinate.  This is
   * in order to avoid not clamping motion that as a floating point number
   * won't be clamped, but will be rounded up to be outside of the range
   * of wl_fixed_t. */
  if (sx > prev_sx)
    sx += (float)wl_fixed_to_double(1);
  if (sy > prev_sy)
    sy += (float)wl_fixed_to_double(1);

  borders = g_array_new (FALSE, FALSE, sizeof (MetaBorder));

  /*
   * Generate borders given the confine region we are to use. The borders
   * are defined to be the outer region of the allowed area. This means
   * top/left borders are "within" the allowed area, while bottom/right
   * borders are outside. This needs to be considered when clamping
   * confined motion vectors.
   */
  region =
    meta_wayland_pointer_constraint_calculate_effective_region (self->constraint);
  region_to_outline (region, borders);
  cairo_region_destroy (region);

  motion = (MetaLine2) {
    .a = (MetaVector2) {
      .x = prev_sx,
      .y = prev_sy,
    },
    .b = (MetaVector2) {
      .x = sx,
      .y = sy,
    },
  };
  directions = get_motion_directions (&motion);

  while (directions)
    {
      closest_border = get_closest_border (borders,
                                           &motion,
                                           directions);
      if (closest_border)
        clamp_to_border (closest_border, &motion, &directions);
      else
        break;
    }

  meta_wayland_surface_get_absolute_coordinates (surface,
                                                 motion.b.x, motion.b.y,
                                                 x, y);

  g_array_free (borders, FALSE);
}

static float
point_to_border_distance_2 (MetaBorder *border,
                            float       x,
                            float       y)
{
  float orig_x, orig_y;
  float dx, dy;

  if (meta_border_is_horizontal (border))
    {
      if (x < border->line.a.x)
        orig_x = border->line.a.x;
      else if (x > border->line.b.x)
        orig_x = border->line.b.x;
      else
        orig_x = x;
      orig_y = border->line.a.y;
    }
  else
    {
      if (y < border->line.a.y)
        orig_y = border->line.a.y;
      else if (y > border->line.b.y)
        orig_y = border->line.b.y;
      else
        orig_y = y;
      orig_x = border->line.a.x;
    }

  dx = fabsf (orig_x - x);
  dy = fabsf (orig_y - y);
  return dx*dx + dy*dy;
}

static void
warp_to_behind_border (MetaBorder *border,
                       float      *sx,
                       float      *sy)
{
  switch (border->blocking_directions)
    {
    case META_BORDER_MOTION_DIRECTION_POSITIVE_X:
    case META_BORDER_MOTION_DIRECTION_NEGATIVE_X:
      if (border->blocking_directions == META_BORDER_MOTION_DIRECTION_POSITIVE_X)
        *sx = border->line.a.x - wl_fixed_to_double (1);
      else
        *sx = border->line.a.x + wl_fixed_to_double (1);
      if (*sy < border->line.a.y)
        *sy = border->line.a.y + wl_fixed_to_double (1);
      else if (*sy > border->line.b.y)
        *sy = border->line.b.y - wl_fixed_to_double (1);
      break;
    case META_BORDER_MOTION_DIRECTION_POSITIVE_Y:
    case META_BORDER_MOTION_DIRECTION_NEGATIVE_Y:
      if (border->blocking_directions == META_BORDER_MOTION_DIRECTION_POSITIVE_Y)
        *sy = border->line.a.y - wl_fixed_to_double (1);
      else
        *sy = border->line.a.y + wl_fixed_to_double (1);
      if (*sx < border->line.a.x)
        *sx = border->line.a.x + wl_fixed_to_double (1);
      else if (*sx > (border->line.b.x))
        *sx = border->line.b.x - wl_fixed_to_double (1);
      break;
    }
}

static void
meta_pointer_confinement_wayland_maybe_warp (MetaPointerConfinementWayland *self)
{
  MetaWaylandSeat *seat;
  MetaWaylandSurface *surface;
  graphene_point_t point;
  float sx;
  float sy;
  cairo_region_t *region;

  seat = meta_wayland_pointer_constraint_get_seat (self->constraint);
  surface = meta_wayland_pointer_constraint_get_surface (self->constraint);

  clutter_input_device_get_coords (seat->pointer->device, NULL, &point);
  meta_wayland_surface_get_relative_coordinates (surface,
                                                 point.x, point.y,
                                                 &sx, &sy);

  region =
    meta_wayland_pointer_constraint_calculate_effective_region (self->constraint);

  if (!cairo_region_contains_point (region, (int)sx, (int)sy))
    {
      GArray *borders;
      float closest_distance_2 = FLT_MAX;
      MetaBorder *closest_border = NULL;
      ClutterSeat *seat;
      unsigned int i;
      float x;
      float y;

      borders = g_array_new (FALSE, FALSE, sizeof (MetaBorder));

      region_to_outline (region, borders);

      for (i = 0; i < borders->len; i++)
        {
          MetaBorder *border = &g_array_index (borders, MetaBorder, i);
          float distance_2;

          distance_2 = point_to_border_distance_2 (border, sx, sy);
          if (distance_2 < closest_distance_2)
            {
              closest_border = border;
              closest_distance_2 = distance_2;
            }
        }

      warp_to_behind_border (closest_border, &sx, &sy);

      meta_wayland_surface_get_absolute_coordinates (surface, sx, sy, &x, &y);

      seat = clutter_backend_get_default_seat (clutter_get_default_backend ());
      clutter_seat_warp_pointer (seat, (int)x, (int)y);
    }

  cairo_region_destroy (region);
}

static void
surface_geometry_changed (MetaWaylandSurface            *surface,
                          MetaPointerConfinementWayland *self)
{
  meta_pointer_confinement_wayland_maybe_warp (self);
}

static void
window_position_changed (MetaWindow                    *window,
                         MetaPointerConfinementWayland *self)
{
  meta_pointer_confinement_wayland_maybe_warp (self);
}

MetaPointerConstraint *
meta_pointer_confinement_wayland_new (MetaWaylandPointerConstraint *constraint)
{
  GObject *object;
  MetaPointerConfinementWayland *confinement;
  MetaWaylandSurface *surface;
  MetaWindow *window;

  object = g_object_new (META_TYPE_POINTER_CONFINEMENT_WAYLAND, NULL);
  confinement = META_POINTER_CONFINEMENT_WAYLAND (object);

  confinement->constraint = constraint;

  surface = meta_wayland_pointer_constraint_get_surface (constraint);
  g_signal_connect_object (surface,
                           "geometry-changed",
                           G_CALLBACK (surface_geometry_changed),
                           confinement,
                           0);

  window = meta_wayland_surface_get_window (surface);
  if (window)
    {
      g_signal_connect_object (window,
                               "position-changed",
                               G_CALLBACK (window_position_changed),
                               confinement,
                               0);
    }

  return META_POINTER_CONSTRAINT (confinement);
}

static void
meta_pointer_confinement_wayland_init (MetaPointerConfinementWayland *confinement_wayland)
{
}

static void
meta_pointer_confinement_wayland_class_init (MetaPointerConfinementWaylandClass *klass)
{
  MetaPointerConstraintClass *pointer_constraint_class =
    META_POINTER_CONSTRAINT_CLASS (klass);

  pointer_constraint_class->constrain = meta_pointer_confinement_wayland_constrain;
}
