/* 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 "gskrenderreplay.h"

#include "gskrendernodeprivate.h"

/**
 * GskRenderReplay: (free-func gsk_render_replay_free)
 *
 * A facility to replay a [class@Gsk.RenderNode] and its children, potentially
 * modifying them.
 *
 * This is a utility tool to walk a rendernode tree. The most powerful way
 * is to provide a function via [method@Gsk.RenderReplay.set_node_filter]
 * to filter each individual node and then run
 * [method@Gsk.RenderReplay.filter_node] on the nodes you want to filter.
 *
 * If you want to just walk the node tree and extract information
 * without any modifications, you can also use [method@Gsk.RenderNode.get_children].
 *
 * Here is a little example application that redacts text in a node file:
 *
 * ```
 * #include <gtk/gtk.h>
 *
 * static GskRenderNode *
 * redact_nodes (GskRenderReplay *replay,
 *               GskRenderNode   *node,
 *               gpointer         user_data)
 * {
 *   GskRenderNode *result;
 *
 *   if (gsk_render_node_get_node_type (node) == GSK_TEXT_NODE)
 *     {
 *       graphene_rect_t bounds;
 *       const GdkRGBA *color;
 *
 *       gsk_render_node_get_bounds (node, &bounds);
 *       color = gsk_text_node_get_color (node);
 *
 *       result = gsk_color_node_new (color, &bounds);
 *     }
 *   else
 *     {
 *       result = gsk_render_replay_default (replay, node);
 *     }
 *
 *   return result;
 * }
 *
 * int
 * main (int argc, char *argv[])
 * {
 *   GFile *file;
 *   GBytes *bytes;
 *   GskRenderNode *result, *node;
 *   GskRenderReplay *replay;
 *
 *   gtk_init ();
 *
 *   if (argc != 3)
 *     {
 *       g_print ("usage: %s INFILE OUTFILE\n", argv[0]);
 *       return 0;
 *     }
 *
 *   file = g_file_new_for_commandline_arg (argv[1]);
 *   bytes = g_file_load_bytes (file, NULL, NULL, NULL);
 *   g_object_unref (file);
 *   if (bytes == NULL)
 *     return 1;
 *
 *   node = gsk_render_node_deserialize (bytes, NULL, NULL);
 *   g_bytes_unref (bytes);
 *   if (node == NULL)
 *     return 1;
 *
 *   replay = gsk_render_replay_new ();
 *   gsk_render_replay_set_node_filter (replay, redact_nodes, NULL, NULL);
 *   result = gsk_render_replay_filter_node (replay, node);
 *   gsk_render_replay_free (replay);
 *
 *   if (!gsk_render_node_write_to_file (result, argv[2], NULL))
 *     return 1;
 *
 *   gsk_render_node_unref (result);
 *   gsk_render_node_unref (node);
 *
 *   return 0;
 * }
 * ```
 *
 * Since: 4.22
 */

struct _GskRenderReplay
{
  GskRenderReplayNodeFilter node_filter;
  gpointer node_filter_data;
  GDestroyNotify node_filter_destroy;

  GskRenderReplayTextureFilter texture_filter;
  gpointer texture_filter_data;
  GDestroyNotify texture_filter_destroy;

  GskRenderReplayFontFilter font_filter;
  gpointer font_filter_data;
  GDestroyNotify font_filter_destroy;
};

/**
 * gsk_render_replay_new:
 *
 * Creates a new replay object to replay nodes.
 *
 * Returns: A new replay object to replay nodes
 *
 * Since: 4.22
 **/
GskRenderReplay *
gsk_render_replay_new (void)
{
  GskRenderReplay *self;

  self = g_new0 (GskRenderReplay, 1);

  return self;
}

/**
 * gsk_render_replay_free:
 * @self: the replay object to free
 *
 * Frees a `GskRenderReplay`.
 *
 * Since: 4.22
 **/
void
gsk_render_replay_free (GskRenderReplay *self)
{
  g_return_if_fail (self != NULL);

  gsk_render_replay_set_node_filter (self, NULL, NULL, NULL);
  gsk_render_replay_set_texture_filter (self, NULL, NULL, NULL);
  gsk_render_replay_set_font_filter (self, NULL, NULL, NULL);

  g_free (self);
}

/**
 * gsk_render_replay_set_node_filter:
 * @filter: (nullable) (scope notified) (closure user_data) (destroy user_destroy):
 *   the function to call to replay nodes
 * @user_data: user data to pass to @func
 * @user_destroy: destroy notify that will be called to release
 *   the user data parameter
 *
 * Sets the function to use as a node filter.
 *
 * This is the most complex function to use for replaying nodes.
 * It can either:
 *
 * * keep the node and just return it unchanged
 *
 * * create a replacement node and return that
 *
 * * discard the node by returning `NULL`
 *
 * * call [method@Gsk.RenderReplay.default] to have the default handler
 *   run for this node, which calls your function on its children
 *
 * Since: 4.22
 */
void
gsk_render_replay_set_node_filter (GskRenderReplay           *self,
                                   GskRenderReplayNodeFilter  filter,
                                   gpointer                   user_data,
                                   GDestroyNotify             user_destroy)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (filter || user_data == NULL);
  g_return_if_fail (user_data || !user_destroy);

  if (self->node_filter_destroy)
    self->node_filter_destroy (self->node_filter_data);

  self->node_filter = filter;
  self->node_filter_data = user_data;
  self->node_filter_destroy = user_destroy;
}

/**
 * gsk_render_replay_filter_node:
 * @self: the replay
 * @node: the node to replay
 *
 * Replays a node using the replay's filter function.
 *
 * After the replay the node may be unchanged, or it may be
 * removed, which will result in %NULL being returned.
 *
 * If no filter node is set, [method@Gsk.RenderReplay.default] is
 * called instead.
 *
 * Returns: (transfer full) (nullable): The replayed node
 *
 * Since: 4.22
 **/
GskRenderNode *
gsk_render_replay_filter_node (GskRenderReplay *self,
                               GskRenderNode   *node)
{
  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (node), NULL);

  if (self->node_filter)
    return self->node_filter (self, node, self->node_filter_data);
  else
    return gsk_render_replay_default (self, node);
}

/**
 * gsk_render_replay_default:
 * @self: the replay
 * @node: the node to replay
 *
 * Replays the node using the default method.
 *
 * The default method calls [method@Gsk.RenderReplay.filter_node]
 * on all its child nodes and the filter functions for all its
 * properties. If none of them are changed, it returns the passed
 * in node. Otherwise it constructs a new node with the changed
 * children and properties.
 *
 * It may not be possible to construct a new node when any of the
 * callbacks return NULL. In that case, this function will return
 * NULL, too.
 *
 * Returns: (transfer full) (nullable): The replayed node 
 *
 * Since: 4.22
 **/
GskRenderNode *
gsk_render_replay_default (GskRenderReplay *self,
                           GskRenderNode   *node)
{
  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (GSK_IS_RENDER_NODE (node), NULL);

  return GSK_RENDER_NODE_GET_CLASS (node)->replay (node, self);
}

/**
 * gsk_render_replay_set_texture_filter:
 * @self: the replay
 * @filter: (nullable) (scope notified) (closure user_data) (destroy user_destroy):
 *   the texture filter function
 * @user_data: user data to pass to @filter
 * @user_destroy: destroy notify that will be called to release
 *   the user data parameter
 *
 * Sets a filter function to be called by [method@Gsk.RenderReplay.default]
 * for nodes that contain textures.
 *
 * You can call [method@GskRenderReplay.filter_texture] to filter
 * a texture yourself.
 *
 * Since: 4.22
 **/
void
gsk_render_replay_set_texture_filter (GskRenderReplay              *self,
                                      GskRenderReplayTextureFilter  filter,
                                      gpointer                      user_data,
                                      GDestroyNotify                user_destroy)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (filter || user_data == NULL);
  g_return_if_fail (user_data || !user_destroy);

  if (self->texture_filter_destroy)
    self->texture_filter_destroy (self->texture_filter_data);

  if (filter)
    self->texture_filter = filter;
  self->texture_filter_data = user_data;
  self->texture_filter_destroy = user_destroy;
}

/**
 * gsk_render_replay_filter_texture:
 * @self: the replay
 * @texture: The texture to filter
 *
 * Filters a texture using the current filter function.
 *
 * Returns: (transfer full): the filtered texture
 *
 * Since: 4.22
 **/
GdkTexture *
gsk_render_replay_filter_texture (GskRenderReplay *self,
                                  GdkTexture      *texture)
{
  GdkTexture *result;

  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (GDK_IS_TEXTURE (texture), NULL);

  if (!self->texture_filter)
    return g_object_ref (texture);

  result = self->texture_filter (self, texture, self->texture_filter_data);
  g_assert (result != NULL);

  return result;
}

/**
 * gsk_render_replay_set_font_filter:
 * @self: the replay
 * @filter: (nullable) (scope notified) (closure user_data) (destroy user_destroy):
 *   the font filter function
 * @user_data: user data to pass to @filter
 * @user_destroy: destroy notify that will be called to release
 *   the user data parameter
 *
 * Sets a filter function to be called by [method@Gsk.RenderReplay.default]
 * for nodes that contain fonts.
 *
 * You can call [method@GskRenderReplay.filter_font] to filter
 * a font yourself.
 *
 * Since: 4.22
 **/
void
gsk_render_replay_set_font_filter (GskRenderReplay           *self,
                                   GskRenderReplayFontFilter  filter,
                                   gpointer                   user_data,
                                   GDestroyNotify             user_destroy)
{
  g_return_if_fail (self != NULL);
  g_return_if_fail (filter || user_data == NULL);
  g_return_if_fail (user_data || !user_destroy);

  if (self->font_filter_destroy)
    self->font_filter_destroy (self->font_filter_data);

  if (filter)
    self->font_filter = filter;
  self->font_filter_data = user_data;
  self->font_filter_destroy = user_destroy;
}

/**
 * gsk_render_replay_filter_font:
 * @self: the replay
 * @font: The font to filter
 *
 * Filters a font using the current filter function.
 *
 * Returns: (transfer full): the filtered font
 *
 * Since: 4.22
 **/
PangoFont *
gsk_render_replay_filter_font (GskRenderReplay *self,
                               PangoFont       *font)
{
  PangoFont *result;

  g_return_val_if_fail (self != NULL, NULL);
  g_return_val_if_fail (PANGO_IS_FONT (font), NULL);

  if (!self->font_filter)
    return g_object_ref (font);

  result = self->font_filter (self, font, self->font_filter_data);
  g_assert (result != NULL);

  return result;
}

G_DEFINE_POINTER_TYPE (GskRenderReplay, gsk_render_replay)
