/*
 * Copyright (C) 2020 Endless OS Foundation, LLC
 * Copyright (C) 2018 Canonical 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/>.
 */

#include "clutter-pick-stack-private.h"
#include "clutter-private.h"

typedef struct
{
  graphene_point3d_t vertices[4];
  CoglMatrixEntry *matrix_entry;
  ClutterActorBox rect;
  gboolean projected;
} Record;

typedef struct
{
  Record base;
  ClutterActor *actor;
  int clip_index;
  gboolean is_overlap;
} PickRecord;

typedef struct
{
  Record base;
  int prev;
} PickClipRecord;

struct _ClutterPickStack
{
  grefcount ref_count;

  CoglMatrixStack *matrix_stack;
  GArray *vertices_stack;
  GArray *clip_stack;
  int current_clip_stack_top;

  gboolean sealed : 1;
};

G_DEFINE_BOXED_TYPE (ClutterPickStack, clutter_pick_stack,
                     clutter_pick_stack_ref, clutter_pick_stack_unref)

static void
project_vertices (CoglMatrixEntry       *matrix_entry,
                  const ClutterActorBox *box,
                  graphene_point3d_t     vertices[4])
{
  graphene_matrix_t m;
  int i;

  cogl_matrix_entry_get (matrix_entry, &m);

  graphene_point3d_init (&vertices[0], box->x1, box->y1, 0.f);
  graphene_point3d_init (&vertices[1], box->x2, box->y1, 0.f);
  graphene_point3d_init (&vertices[2], box->x2, box->y2, 0.f);
  graphene_point3d_init (&vertices[3], box->x1, box->y2, 0.f);

  for (i = 0; i < 4; i++)
    {
      float w = 1.f;

      cogl_graphene_matrix_project_point (&m,
                                          &vertices[i].x,
                                          &vertices[i].y,
                                          &vertices[i].z,
                                          &w);
    }
}

static void
maybe_project_record (Record *rec)
{
  if (!rec->projected)
    {
      project_vertices (rec->matrix_entry, &rec->rect, rec->vertices);
      rec->projected = TRUE;
    }
}

static inline gboolean
is_axis_aligned_2d_rectangle (const graphene_point3d_t vertices[4])
{
  int i;

  for (i = 0; i < 4; i++)
    {
      if (!G_APPROX_VALUE (vertices[i].z,
                           vertices[(i + 1) % 4].z,
                           FLT_EPSILON))
        return FALSE;

      if (!G_APPROX_VALUE (vertices[i].x,
                           vertices[(i + 1) % 4].x,
                           FLT_EPSILON) &&
          !G_APPROX_VALUE (vertices[i].y,
                           vertices[(i + 1) % 4].y,
                           FLT_EPSILON))
        return FALSE;
    }

  return TRUE;
}

static gboolean
ray_intersects_input_region (Record                   *rec,
                             const graphene_ray_t     *ray,
                             const graphene_point3d_t *point)
{
  maybe_project_record (rec);

  if (G_LIKELY (is_axis_aligned_2d_rectangle (rec->vertices)))
    {
      graphene_box_t box;
      graphene_box_t right_border;
      graphene_box_t bottom_border;

      /* Graphene considers both the start and end coordinates of boxes to be
       * inclusive, while the vertices of a clutter actor are exclusive. So we
       * need to manually exclude hits on these borders
       */

      graphene_box_init_from_points (&box, 4, rec->vertices);
      graphene_box_init_from_points (&right_border, 2, rec->vertices + 1);
      graphene_box_init_from_points (&bottom_border, 2, rec->vertices + 2);

      /* Fast path for actors without 3D transforms */
      if (graphene_box_contains_point (&box, point))
        {
          return !graphene_box_contains_point (&right_border, point) &&
                 !graphene_box_contains_point (&bottom_border, point);
        }

      return graphene_ray_intersects_box (ray, &box) &&
             !graphene_ray_intersects_box (ray, &right_border) &&
             !graphene_ray_intersects_box (ray, &bottom_border);
    }
  else
    {
      graphene_triangle_t t0, t1;

      /*
       * Degrade the projected quad into the following triangles:
       *
       * 0 -------------- 1
       * |  •             |
       * |     •     t0   |
       * |        •       |
       * |   t1      •    |
       * |              • |
       * 3 -------------- 2
       */

      graphene_triangle_init_from_point3d (&t0,
                                           &rec->vertices[0],
                                           &rec->vertices[1],
                                           &rec->vertices[2]);

      graphene_triangle_init_from_point3d (&t1,
                                           &rec->vertices[0],
                                           &rec->vertices[2],
                                           &rec->vertices[3]);

      return graphene_triangle_contains_point (&t0, point) ||
             graphene_triangle_contains_point (&t1, point) ||
             graphene_ray_intersects_triangle (ray, &t0) ||
             graphene_ray_intersects_triangle (ray, &t1);
    }
}

static gboolean
ray_intersects_record (ClutterPickStack         *pick_stack,
                       PickRecord               *rec,
                       const graphene_point3d_t *point,
                       const graphene_ray_t     *ray)
{
  int clip_index;

  if (!ray_intersects_input_region (&rec->base, ray, point))
    return FALSE;

  clip_index = rec->clip_index;
  while (clip_index >= 0)
    {
      PickClipRecord *clip =
        &g_array_index (pick_stack->clip_stack, PickClipRecord, clip_index);

      if (!ray_intersects_input_region (&clip->base, ray, point))
        return FALSE;

      clip_index = clip->prev;
    }

  return TRUE;
}

static void
add_pick_stack_weak_refs (ClutterPickStack *pick_stack)
{
  int i;

  g_assert (!pick_stack->sealed);

  for (i = 0; i < pick_stack->vertices_stack->len; i++)
    {
      PickRecord *rec =
        &g_array_index (pick_stack->vertices_stack, PickRecord, i);

      if (rec->actor)
        g_object_add_weak_pointer (G_OBJECT (rec->actor),
                                   (gpointer) &rec->actor);
    }
}

static void
remove_pick_stack_weak_refs (ClutterPickStack *pick_stack)
{
  int i;

  for (i = 0; i < pick_stack->vertices_stack->len; i++)
    {
      PickRecord *rec =
        &g_array_index (pick_stack->vertices_stack, PickRecord, i);

      if (rec->actor)
        g_object_remove_weak_pointer (G_OBJECT (rec->actor),
                                      (gpointer) &rec->actor);
    }
}

static void
clutter_pick_stack_dispose (ClutterPickStack *pick_stack)
{
  remove_pick_stack_weak_refs (pick_stack);
  g_clear_pointer (&pick_stack->matrix_stack, cogl_object_unref);
  g_clear_pointer (&pick_stack->vertices_stack, g_array_unref);
  g_clear_pointer (&pick_stack->clip_stack, g_array_unref);
}

static void
clear_pick_record (gpointer data)
{
  PickRecord *rec = data;
  g_clear_pointer (&rec->base.matrix_entry, cogl_matrix_entry_unref);
}

static void
clear_clip_record (gpointer data)
{
  PickClipRecord *clip = data;
  g_clear_pointer (&clip->base.matrix_entry, cogl_matrix_entry_unref);
}

/**
 * clutter_pick_stack_new:
 * @context: a #CoglContext
 *
 * Creates a new #ClutterPickStack.
 *
 * Returns: (transfer full): A newly created #ClutterPickStack
 */
ClutterPickStack *
clutter_pick_stack_new (CoglContext *context)
{
  ClutterPickStack *pick_stack;

  pick_stack = g_new0 (ClutterPickStack, 1);
  g_ref_count_init (&pick_stack->ref_count);
  pick_stack->matrix_stack = cogl_matrix_stack_new (context);
  pick_stack->vertices_stack = g_array_new (FALSE, FALSE, sizeof (PickRecord));
  pick_stack->clip_stack = g_array_new (FALSE, FALSE, sizeof (PickClipRecord));
  pick_stack->current_clip_stack_top = -1;

  g_array_set_clear_func (pick_stack->vertices_stack, clear_pick_record);
  g_array_set_clear_func (pick_stack->clip_stack, clear_clip_record);

  return pick_stack;
}

/**
 * clutter_pick_stack_ref:
 * @pick_stack: A #ClutterPickStack
 *
 * Increments the reference count of @pick_stack by one.
 *
 * Returns: (transfer full): @pick_stack
 */
ClutterPickStack *
clutter_pick_stack_ref (ClutterPickStack *pick_stack)
{
  g_ref_count_inc (&pick_stack->ref_count);
  return pick_stack;
}

/**
 * clutter_pick_stack_unref:
 * @pick_stack: A #ClutterPickStack
 *
 * Decrements the reference count of @pick_stack by one, freeing the structure
 * when the reference count reaches zero.
 */
void
clutter_pick_stack_unref (ClutterPickStack *pick_stack)
{
  if (g_ref_count_dec (&pick_stack->ref_count))
    {
      clutter_pick_stack_dispose (pick_stack);
      g_free (pick_stack);
    }
}

void
clutter_pick_stack_seal (ClutterPickStack *pick_stack)
{
  g_assert (!pick_stack->sealed);
  add_pick_stack_weak_refs (pick_stack);
  pick_stack->sealed = TRUE;
}

void
clutter_pick_stack_log_pick (ClutterPickStack       *pick_stack,
                             const ClutterActorBox  *box,
                             ClutterActor           *actor)
{
  PickRecord rec;

  g_return_if_fail (actor != NULL);

  g_assert (!pick_stack->sealed);

  rec.is_overlap = FALSE;
  rec.actor = actor;
  rec.clip_index = pick_stack->current_clip_stack_top;
  rec.base.rect = *box;
  rec.base.projected = FALSE;
  rec.base.matrix_entry = cogl_matrix_stack_get_entry (pick_stack->matrix_stack);
  cogl_matrix_entry_ref (rec.base.matrix_entry);

  g_array_append_val (pick_stack->vertices_stack, rec);
}

void
clutter_pick_stack_log_overlap (ClutterPickStack *pick_stack,
                                ClutterActor     *actor)
{
  PickRecord rec = { 0 };

  g_assert (!pick_stack->sealed);

  rec.is_overlap = TRUE;
  rec.actor = actor;
  rec.clip_index = pick_stack->current_clip_stack_top;

  g_array_append_val (pick_stack->vertices_stack, rec);
}

void
clutter_pick_stack_push_clip (ClutterPickStack      *pick_stack,
                              const ClutterActorBox *box)
{
  PickClipRecord clip;

  g_assert (!pick_stack->sealed);

  clip.prev = pick_stack->current_clip_stack_top;
  clip.base.rect = *box;
  clip.base.projected = FALSE;
  clip.base.matrix_entry = cogl_matrix_stack_get_entry (pick_stack->matrix_stack);
  cogl_matrix_entry_ref (clip.base.matrix_entry);

  g_array_append_val (pick_stack->clip_stack, clip);
  pick_stack->current_clip_stack_top = pick_stack->clip_stack->len - 1;
}

void
clutter_pick_stack_pop_clip (ClutterPickStack *pick_stack)
{
  const PickClipRecord *top;

  g_assert (!pick_stack->sealed);
  g_assert (pick_stack->current_clip_stack_top >= 0);

  /* Individual elements of clip_stack are not freed. This is so they can
   * be shared as part of a tree of different stacks used by different
   * actors in the pick_stack. The whole clip_stack does however get
   * freed later in clutter_pick_stack_dispose.
   */

  top = &g_array_index (pick_stack->clip_stack,
                        PickClipRecord,
                        pick_stack->current_clip_stack_top);

  pick_stack->current_clip_stack_top = top->prev;
}

void
clutter_pick_stack_push_transform (ClutterPickStack        *pick_stack,
                                   const graphene_matrix_t *transform)
{
  cogl_matrix_stack_push (pick_stack->matrix_stack);
  cogl_matrix_stack_multiply (pick_stack->matrix_stack, transform);
}

void
clutter_pick_stack_get_transform (ClutterPickStack  *pick_stack,
                                  graphene_matrix_t *out_transform)
{
  cogl_matrix_stack_get (pick_stack->matrix_stack, out_transform);
}

void
clutter_pick_stack_pop_transform (ClutterPickStack *pick_stack)
{
  cogl_matrix_stack_pop (pick_stack->matrix_stack);
}

static gboolean
get_verts_rectangle (graphene_point3d_t     verts[4],
                     cairo_rectangle_int_t *rect)
{
  if (verts[0].x != verts[2].x ||
      verts[0].y != verts[1].y ||
      verts[3].x != verts[1].x ||
      verts[3].y != verts[2].y ||
      verts[0].x > verts[3].x ||
      verts[0].y > verts[3].y)
    return FALSE;

  *rect = (cairo_rectangle_int_t) {
    .x = ceilf (verts[0].x),
    .y = ceilf (verts[0].y),
    .width = floor (verts[1].x - ceilf (verts[0].x)),
    .height = floor (verts[2].y - ceilf (verts[0].y)),
  };

  return TRUE;
}

static void
calculate_clear_area (ClutterPickStack  *pick_stack,
                      PickRecord        *pick_rec,
                      int                elem,
                      cairo_region_t   **clear_area)
{
  cairo_region_t *area = NULL;
  graphene_point3d_t verts[4];
  cairo_rectangle_int_t rect;
  int i;

  clutter_actor_get_abs_allocation_vertices (pick_rec->actor,
                                             (graphene_point3d_t *) &verts);
  if (!get_verts_rectangle (verts, &rect))
    {
      if (clear_area)
        *clear_area = NULL;
      return;
    }

  rect.x += ceil (pick_rec->base.rect.x1);
  rect.y += ceil (pick_rec->base.rect.y1);
  rect.width =
    MIN (rect.width, floor (pick_rec->base.rect.x2 - pick_rec->base.rect.x1));
  rect.height =
    MIN (rect.height, floor (pick_rec->base.rect.y2 - pick_rec->base.rect.y1));

  area = cairo_region_create_rectangle (&rect);

  for (i = elem + 1; i < pick_stack->vertices_stack->len; i++)
    {
      PickRecord *rec =
        &g_array_index (pick_stack->vertices_stack, PickRecord, i);
      ClutterActorBox paint_box;

      if (!rec->is_overlap &&
	  (rec->base.rect.x1 == rec->base.rect.x2 ||
	   rec->base.rect.y1 == rec->base.rect.y2))
        continue;

      if (!clutter_actor_get_paint_box (rec->actor, &paint_box))
        continue;

      cairo_region_subtract_rectangle (area,
                                       &(cairo_rectangle_int_t) {
                                         .x = paint_box.x1,
                                         .y = paint_box.y1,
                                         .width = paint_box.x2 - paint_box.x1,
                                         .height = paint_box.y2 - paint_box.y1,
                                       });
    }

  if (clear_area)
    *clear_area = g_steal_pointer (&area);

  g_clear_pointer (&area, cairo_region_destroy);
}

ClutterActor *
clutter_pick_stack_search_actor (ClutterPickStack          *pick_stack,
                                 const graphene_point3d_t  *point,
                                 const graphene_ray_t      *ray,
                                 cairo_region_t           **clear_area)
{
  int i;

  /* Search all "painted" pickable actors from front to back. A linear search
   * is required, and also performs fine since there is typically only
   * on the order of dozens of actors in the list (on screen) at a time.
   */
  for (i = pick_stack->vertices_stack->len - 1; i >= 0; i--)
    {
      PickRecord *rec =
        &g_array_index (pick_stack->vertices_stack, PickRecord, i);

      if (!rec->is_overlap && rec->actor &&
          ray_intersects_record (pick_stack, rec, point, ray))
        {
          if (clear_area)
            calculate_clear_area (pick_stack, rec, i, clear_area);
          return rec->actor;
        }
    }

  return NULL;
}
