/* GSK - The GTK Scene Kit
 *
 * Copyright 2025  Benjamin Otte
 *
 * 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 "config.h"

#include "gskdisplacementnodeprivate.h"

#include "gskcontainernodeprivate.h"
#include "gskrectprivate.h"
#include "gskrendernodeprivate.h"
#include "gskrenderreplay.h"

#include "gdk/gdkcairoprivate.h"

#if 0
#define DEBUG g_print
#else
#define DEBUG(...)
#endif

/*< private >
 * GskDisplacementNode:
 *
 * A render node that uses a displacement map to offset each pixel of the
 * child.
 */
struct _GskDisplacementNode
{
  GskRenderNode render_node;

  union {
    GskRenderNode *children[2];
    struct {
      GskRenderNode *child;
      GskRenderNode *displacement;
    };
  };
  GdkColorChannel channels[2];
  graphene_size_t max;
  graphene_size_t scale;
  graphene_point_t offset;
  GskRectSnap snap;
};

static void
gsk_displacement_node_finalize (GskRenderNode *node)
{
  GskDisplacementNode *self = (GskDisplacementNode *) node;
  GskRenderNodeClass *parent_class = g_type_class_peek (g_type_parent (GSK_TYPE_DISPLACEMENT_NODE));

  gsk_render_node_unref (self->child);
  gsk_render_node_unref (self->displacement);

  parent_class->finalize (node);
}

static float
get_channel (guint32         pixel,
             GdkColorChannel channel)
{
  float alpha = (pixel >> 24) / 255.0;
  float result;

  if (channel == GDK_COLOR_CHANNEL_ALPHA)
    return alpha;
  
  if (alpha == 0.0f)
    return 0.0f;

  result = (pixel >> 8 * (2 - channel)) & 0xFF;
  result = result / 255.0f;
  /* unpremultiply */
  result /= alpha;
  return result;
}

static guint32
sample_image (guchar *image,
              gsize   width,
              gsize   height,
              gsize   stride,
              float   x,
              float   y)
{
  int ix, iy;

  ix = round (x);
  iy = round (y);

  if (ix < 0 || ix >= width ||
      iy < 0 || iy >= height)
    return 0; /* transparent */

  return *(guint32 *) (image + iy * stride + ix * sizeof (guint32));
}

static void
get_matrix (cairo_pattern_t *pattern,
            cairo_surface_t *surface,
            cairo_matrix_t  *out_matrix)
{
  cairo_matrix_t pattern_matrix, surface_matrix;

  cairo_pattern_get_matrix (pattern, &pattern_matrix);
  gdk_cairo_surface_get_device_matrix (surface, &surface_matrix);

  DEBUG ("pattern: %g %g %g\n         %g %g %g\n",
           pattern_matrix.xx, pattern_matrix.yx, pattern_matrix.x0,
           pattern_matrix.yx, pattern_matrix.yy, pattern_matrix.y0);
  DEBUG ("surface: %g %g %g\n         %g %g %g\n",
           surface_matrix.xx, surface_matrix.yx, surface_matrix.x0,
           surface_matrix.yx, surface_matrix.yy, surface_matrix.y0);

  cairo_matrix_multiply (out_matrix, &pattern_matrix, &surface_matrix);
}

static void
apply_displacement (GskDisplacementNode *self,
                    cairo_surface_t     *image_surface,
                    cairo_pattern_t     *displacement_pattern)
{
  cairo_surface_t *d_surface, *d_image, *i_image;
  cairo_matrix_t d_matrix, i_matrix;
  guchar *d_data, *i_data;
  gsize x, y, d_width, d_height, d_stride, i_width, i_height, i_stride;
  double s, t;
  float h, v;
  guint32* pixel_data;

  cairo_pattern_get_surface (displacement_pattern, &d_surface);
  d_image = cairo_surface_map_to_image (d_surface, NULL);
  g_assert (cairo_image_surface_get_format (d_image) == CAIRO_FORMAT_ARGB32);
  get_matrix (displacement_pattern, d_surface, &d_matrix);
  d_data = cairo_image_surface_get_data (d_image);
  d_width = cairo_image_surface_get_width (d_image);
  d_height = cairo_image_surface_get_height (d_image);
  d_stride = cairo_image_surface_get_stride (d_image);
  DEBUG ("displacement image: %zu %zu\n", d_width, d_height);

  i_image = cairo_surface_map_to_image (image_surface, NULL);
  g_assert (cairo_image_surface_get_format (i_image) == CAIRO_FORMAT_ARGB32);
  gdk_cairo_surface_get_device_matrix (image_surface, &i_matrix);
  DEBUG ("image:   %g %g %g\n         %g %g %g\n",
           i_matrix.xx, i_matrix.yx, i_matrix.x0,
           i_matrix.yx, i_matrix.yy, i_matrix.y0);
  i_data = cairo_image_surface_get_data (i_image);
  i_width = cairo_image_surface_get_width (i_image);
  i_height = cairo_image_surface_get_height (i_image);
  i_stride = cairo_image_surface_get_stride (i_image);
  DEBUG ("child image: %zu %zu\n", i_width, i_height);

  cairo_matrix_invert (&d_matrix);

  for (y = 0; y < d_height; y++)
    {
      pixel_data = (guint32 *) d_data;
      for (x = 0; x < d_width; x++)
        {
          s = x;
          t = y;
          DEBUG ("pixel       : %08X\n", pixel_data[x]);
          DEBUG ("coordinate  : %zu %zu\n", x, y);
          cairo_matrix_transform_point (&d_matrix, &s, &t);
          DEBUG ("node space  : %g %g\n", s, t);
          h = get_channel (pixel_data[x], self->channels[0]);
          v = get_channel (pixel_data[x], self->channels[1]);
          DEBUG ("displacement: %g %g\n", h, v);
          h = CLAMP (self->scale.width * (h - self->offset.x), -self->max.width, self->max.width);
          v = CLAMP (self->scale.height * (v - self->offset.y), -self->max.height, self->max.height);
          DEBUG ("transformed : %g %g\n", h, v);
          s += h;
          t += v;
          DEBUG ("coordinate  : %g %g\n", s, t);
          cairo_matrix_transform_point (&i_matrix, &s, &t);
          DEBUG ("final       : %g %g\n", s, t);

          pixel_data[x] = sample_image (i_data, i_width, i_height, i_stride, s, t);
          DEBUG ("result      : %08X\n", pixel_data[x]);
        }
      d_data += d_stride;
    }

  cairo_surface_mark_dirty (d_image);
  cairo_surface_unmap_image (d_surface, d_image);
  /* https://gitlab.freedesktop.org/cairo/cairo/-/merge_requests/487 */
  cairo_surface_mark_dirty (d_surface);
  cairo_surface_unmap_image (image_surface, i_image);
}

static void
gsk_displacement_node_draw (GskRenderNode *node,
                            cairo_t       *cr,
                            GskCairoData  *data)
{
  GskDisplacementNode *self = (GskDisplacementNode *) node;
  cairo_pattern_t *displacement;
  cairo_surface_t *child_surface;
  cairo_t *child_cr;
  graphene_rect_t bounds, child_bounds;

  if (!gsk_cairo_rect_snap (cr, &node->bounds, self->snap, &bounds))
    return;

  graphene_rect_inset (&child_bounds, - self->max.width, - self->max.height);
  if (!gsk_rect_intersection (&bounds, &self->child->bounds, &child_bounds))
    return;

  /* clip so the push_group() creates a smaller surface */
  gdk_cairo_rect (cr, &bounds);
  cairo_clip (cr);
  if (gdk_cairo_is_all_clipped (cr))
    return;

  cairo_push_group_with_content (cr, CAIRO_CONTENT_COLOR_ALPHA);
  gsk_render_node_draw_full (self->displacement, cr, data);
  displacement = cairo_pop_group (cr);

  child_surface = gdk_cairo_create_similar_surface (cr, CAIRO_CONTENT_COLOR_ALPHA, &child_bounds);
  child_cr = cairo_create (child_surface);
  gsk_render_node_draw_full (self->child, child_cr, data);
  cairo_destroy (child_cr);

  apply_displacement (self, child_surface, displacement);

  cairo_set_source (cr, displacement);
  cairo_paint (cr);

  cairo_surface_destroy (child_surface);
  cairo_pattern_destroy (displacement);
}

static void
gdk_cairo_region_grow (cairo_region_t *region,
                       int             grow_x,
                       int             grow_y)
{
  cairo_region_t *tmp;
  guint i, n;

  n = cairo_region_num_rectangles (region);
  tmp = cairo_region_create ();

  for (i = 0; i < n; i++)
    {
      cairo_rectangle_int_t rect;
      cairo_region_get_rectangle (region, i, &rect);
      rect.x -= grow_x;
      rect.y -= grow_y;
      rect.width += 2 * grow_x;
      rect.height += 2 * grow_y;
      cairo_region_union_rectangle (tmp ,&rect);
    }
  
  cairo_region_subtract (region, region);
  cairo_region_union (region, tmp);
  cairo_region_destroy (tmp);
}

static void
gsk_displacement_node_diff (GskRenderNode *node1,
                            GskRenderNode *node2,
                            GskDiffData   *data)
{
  GskDisplacementNode *self1 = (GskDisplacementNode *) node1;
  GskDisplacementNode *self2 = (GskDisplacementNode *) node2;
  cairo_region_t *child_region, *displacement_region;

  if (!gsk_rect_equal (&node1->bounds, &node2->bounds) ||
      self1->snap != self2->snap ||
      self1->channels[0] != self2->channels[0] ||
      self1->channels[1] != self2->channels[1] ||
      !graphene_size_equal (&self1->max, &self2->max) ||
      !graphene_size_equal (&self1->scale, &self2->scale) ||
      !graphene_point_equal (&self1->offset, &self2->offset))
    {
      gsk_render_node_diff_impossible (node1, node2, data);
      return;
    }

  child_region = cairo_region_create ();
  gsk_render_node_diff (self1->child, self2->child, &(GskDiffData) { child_region, data->copies, data->surface });
  displacement_region = cairo_region_create ();
  gsk_render_node_diff (self1->displacement, self2->displacement, &(GskDiffData) { displacement_region, data->copies, data->surface });
  cairo_region_union (child_region, displacement_region);
  gdk_cairo_region_grow (child_region, ceilf (self1->max.width), ceilf (self1->max.height));
  cairo_region_union (data->region, child_region);

  cairo_region_destroy (displacement_region);
  cairo_region_destroy (child_region);
}

static GskRenderNode **
gsk_displacement_node_get_children (GskRenderNode *node,
                                    gsize         *n_children)
{
  GskDisplacementNode *self = (GskDisplacementNode *) node;

  *n_children = G_N_ELEMENTS (self->children);

  return self->children;
}

static GskRenderNode *
gsk_displacement_node_replay (GskRenderNode   *node,
                              GskRenderReplay *replay)
{
  GskDisplacementNode *self = (GskDisplacementNode *) node;
  GskRenderNode *result, *child, *displacement;

  child = gsk_render_replay_filter_node (replay, self->child);
  displacement = gsk_render_replay_filter_node (replay, self->displacement);

  if (displacement == NULL)
    displacement = gsk_container_node_new (NULL, 0);
  if (child == NULL)
    child = gsk_container_node_new (NULL, 0);

  if (child == self->child && displacement == self->displacement)
    {
      result = gsk_render_node_ref (node);
    }
  else
    {
      result = gsk_displacement_node_new (&node->bounds,
                                          self->snap,
                                          child,
                                          displacement,
                                          self->channels,
                                          &self->max,
                                          &self->scale,
                                          &self->offset);
    }

  gsk_render_node_unref (child);
  gsk_render_node_unref (displacement);

  return result;
}

static void
gsk_displacement_node_render_opacity (GskRenderNode  *node,
                                      GskOpacityData *data)
{
  GskDisplacementNode *self = (GskDisplacementNode *) node;
  GskOpacityData child_data = GSK_OPACITY_DATA_INIT_EMPTY (data->copies);

  gsk_render_node_render_opacity (self->child, &child_data);

  if (!gsk_rect_is_empty (&child_data.opaque))
    {
      graphene_rect_inset (&child_data.opaque, self->max.width, self->max.height);
      if (gsk_rect_is_empty (&data->opaque))
        data->opaque = child_data.opaque;
      else
        gsk_rect_coverage (&data->opaque, &child_data.opaque, &data->opaque);
    }
}

static void
gsk_displacement_node_class_init (gpointer g_class,
                                  gpointer class_data)
{
  GskRenderNodeClass *node_class = g_class;

  node_class->node_type = GSK_DISPLACEMENT_NODE;

  node_class->finalize = gsk_displacement_node_finalize;
  node_class->draw = gsk_displacement_node_draw;
  node_class->diff = gsk_displacement_node_diff;
  node_class->get_children = gsk_displacement_node_get_children;
  node_class->replay = gsk_displacement_node_replay;
  node_class->render_opacity = gsk_displacement_node_render_opacity;
}

GSK_DEFINE_RENDER_NODE_TYPE (GskDisplacementNode, gsk_displacement_node)

/*< private >
 * gsk_displacement_node_new:
 * @bounds: The rectangle to apply to
 * @snap: how to snap the rectangle to the pixel grid
 * @child: The child to displace
 * @displacement: The dissplacement mask
 * @channels: Which channels to usefor the displacement in horizontal and
 *   vertical direction respectively.
 * @max: The maximum displacement in units
 * @scale: The scale to apply to the displacement value
 * @offset: The offset to apply to the displacement value
 *
 * Creates a `GskRenderNode` that will displace the child according to the
 * displacement mask. This is modeled after [SVG's feDisplacementMap
 * filter](https://www.w3.org/TR/SVG11/filters.html#feDisplacementMapElement).
 *
 * The amount to displace is determine by sampling the displacement
 * at every coordinate, converting its value into the given colorstate and
 * applying the formula `value = scale * (value - offset)` and clamping the
 * resulting value to be between `-max` and `max`.
 *
 * Returns: (transfer full) (type GskDisplacementNode): A new `GskRenderNode`
 */
GskRenderNode *
gsk_displacement_node_new (const graphene_rect_t  *bounds,
                           GskRectSnap             snap,
                           GskRenderNode          *child,
                           GskRenderNode          *displacement,
                           const GdkColorChannel   channels[2],
                           const graphene_size_t  *max,
                           const graphene_size_t  *scale,
                           const graphene_point_t *offset)
{
  GskDisplacementNode *self;
  GskRenderNode *node;

  g_return_val_if_fail (GSK_IS_RENDER_NODE (child), NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (displacement), NULL);
  g_return_val_if_fail (channels[0] < 4, NULL);
  g_return_val_if_fail (channels[1] < 4, NULL);
  g_return_val_if_fail (max->width > 0, NULL);
  g_return_val_if_fail (max->height > 0, NULL);

  self = gsk_render_node_alloc (GSK_TYPE_DISPLACEMENT_NODE);
  node = (GskRenderNode *) self;
  node->bounds = *bounds;
  self->snap = snap;
  self->child = gsk_render_node_ref (child);
  self->displacement = gsk_render_node_ref (displacement);
  self->channels[0] = channels[0];
  self->channels[1] = channels[1];
  self->max = *max;
  self->scale = *scale;
  self->offset = *offset;

  node->preferred_depth = gsk_render_node_get_preferred_depth (child);
  node->is_hdr = gsk_render_node_is_hdr (child);
  node->isolates_background = TRUE;
  if (child->fully_opaque)
    {
      graphene_rect_t child_opaque = child->bounds;
      graphene_rect_inset (&child_opaque, self->max.width, self->max.height);
      node->fully_opaque = gsk_rect_contains_rect (&child_opaque, &node->bounds);
    }
  node->bilevel_opacity = gsk_render_node_is_bilevel_opacity (child);
  node->contains_subsurface_node = gsk_render_node_contains_subsurface_node (child) ||
                                   gsk_render_node_contains_subsurface_node (displacement);
  node->contains_paste_node = gsk_render_node_contains_paste_node (child) ||
                              gsk_render_node_contains_paste_node (displacement);

  return node;
}

/*< private >
 * gsk_displacement_node_get_child:
 * @node: (type GskDisplacementNode): a displacement `GskRenderNode`
 *
 * Gets the child node that is getting displaced by the given @node.
 *
 * Returns: (transfer none): the child `GskRenderNode`
 **/
GskRenderNode *
gsk_displacement_node_get_child (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return self->child;
}

/*< private >
 * gsk_displacement_node_get_displacement:
 * @node: (type GskDisplacementNode): a displacement `GskRenderNode`
 *
 * Gets the node that defines the displacement mask.
 *
 * Returns: (transfer none): the displacement `GskRenderNode`
 **/
GskRenderNode *
gsk_displacement_node_get_displacement (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return self->displacement;
}

const GdkColorChannel *
gsk_displacement_node_get_channels (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return self->channels;
}

const graphene_size_t *
gsk_displacement_node_get_max (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return &self->max;
}

const graphene_size_t *
gsk_displacement_node_get_scale (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return &self->scale;
}

const graphene_point_t *
gsk_displacement_node_get_offset (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return &self->offset;
}

GskRectSnap
gsk_displacement_node_get_snap (const GskRenderNode *node)
{
  const GskDisplacementNode *self = (const GskDisplacementNode *) node;

  return self->snap;
}
