/*
 * Copyright © 2025 Red Hat, Inc
 *
 * 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.1 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/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Authors: Matthias Clasen <mclasen@redhat.com>
 */

#include "border-paintable.h"
#include "path-paintable.h"
#include "gtk/svg/gtksvgelementprivate.h"

struct _BorderPaintable
{
  GObject parent_instance;

  gboolean show_bounds;
  gboolean show_spines;
  gboolean show_grid;

  PathPaintable *paintable;
};

/* {{{ Utilities */

static GskPath *
circle_path_new (float cx,
                 float cy,
                 float radius)
{
  GskPathBuilder *builder = gsk_path_builder_new ();
  gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (cx, cy), radius);
  return gsk_path_builder_free_to_path (builder);
}

static void
get_origin_location (GskPath          *path,
                     float             origin,
                     graphene_point_t *pos)
{
  g_autoptr (GskPathMeasure) measure = NULL;
  float length;
  GskPathPoint point;

  measure = gsk_path_measure_new (path);
  length = gsk_path_measure_get_length (measure);
  gsk_path_measure_get_point (measure, origin * length, &point);
  gsk_path_point_get_position (&point, path, pos);
}

/* }}} */
/* {{{ GtkSymbolicPaintable implementation */

static void
snapshot_spines (GtkSnapshot           *snapshot,
                 graphene_rect_t       *bounds,
                 PathPaintable         *paintable,
                 SvgElement                 *shape,
                 unsigned int           state,
                 const graphene_rect_t *viewport,
                 float                  scale,
                 const GdkRGBA         *c,
                 GskStroke             *stroke)
{
  switch ((unsigned int) svg_element_get_type (shape))
    {
    case SVG_ELEMENT_SVG:
    case SVG_ELEMENT_GROUP:
      for (unsigned int i = 0; i < svg_element_get_n_children (shape); i++)
        {
          SvgElement *sh = svg_element_get_child (shape, i);
          snapshot_spines (snapshot, bounds, paintable, sh, state, viewport, scale, c, stroke);
        }
      break;
    case SVG_ELEMENT_LINE:
    case SVG_ELEMENT_POLYLINE:
    case SVG_ELEMENT_POLYGON:
    case SVG_ELEMENT_CIRCLE:
    case SVG_ELEMENT_ELLIPSE:
    case SVG_ELEMENT_PATH:
      {
        uint64_t states = svg_element_get_states (shape);

        if (states & (G_GUINT64_CONSTANT (1) << state))
          {
            GskPath *path = svg_element_get_path (shape, viewport, FALSE);
            double origin = svg_element_get_gpa_origin (shape);
            SvgElement * attach_to = NULL;
            double attach_pos;

            graphene_point_t pos;
            g_autoptr (GskPath) dot = NULL;

            gtk_snapshot_push_stroke (snapshot, path, stroke);
            gtk_snapshot_append_color (snapshot, c, bounds);
            gtk_snapshot_pop (snapshot);

            get_origin_location (path, origin, &pos);

            dot = circle_path_new (pos.x, pos.y, 4.f/scale);
            gtk_snapshot_push_fill (snapshot, dot, GSK_FILL_RULE_WINDING);
            gtk_snapshot_append_color (snapshot, c, bounds);
            gtk_snapshot_pop (snapshot);

            path_paintable_get_attach_path_for_shape (paintable, shape, &attach_to, &attach_pos);

            if (attach_to != NULL)
              {
                GskPathBuilder *builder;
                GskPath *arrow;

                builder = gsk_path_builder_new ();
                gsk_path_builder_move_to (builder, pos.x, pos.y);
                gsk_path_builder_rel_line_to (builder, 20.f/scale, 0);
                gsk_path_builder_rel_move_to (builder, -4.f/scale, -3.f/scale);
                gsk_path_builder_rel_line_to (builder, 4.f/scale, 3.f/scale);
                gsk_path_builder_rel_line_to (builder, -4.f/scale, 3.f/scale);
                arrow = gsk_path_builder_free_to_path (builder);
                gtk_snapshot_push_stroke (snapshot, arrow, stroke);
                gtk_snapshot_append_color (snapshot, c, bounds);
                gtk_snapshot_pop (snapshot);
                gsk_path_unref (arrow);
              }
          }
      }
      break;
    default:
      break;
    }
}

static void
border_paintable_snapshot_with_weight (GtkSymbolicPaintable  *paintable,
                                       GtkSnapshot           *snapshot,
                                       double                 width,
                                       double                 height,
                                       const GdkRGBA         *colors,
                                       size_t                 n_colors,
                                       double                 weight)
{
  BorderPaintable *self = BORDER_PAINTABLE (paintable);
  GtkSvg *svg = path_paintable_get_svg (self->paintable);
  double w, h;
  float scale;

  if (!self->paintable)
    return;

  w = svg->width;
  h = svg->height;

  if (w == 0 || h == 0)
    return;

  scale = MIN (width / w, height / h);

  if (self->show_grid && scale >= 3.f)
    {
      GskPathBuilder *builder;
      GdkRGBA grid_color = colors[GTK_SYMBOLIC_COLOR_FOREGROUND];
      g_autoptr (GskStroke) stroke = NULL;
      g_autoptr (GskPath) grid_path = NULL;

      builder = gsk_path_builder_new ();
      stroke = gsk_stroke_new (1.f);
      grid_color.alpha /= 2.f;

      for (float x = 0; x <= w; x += 1.f)
        {
          gsk_path_builder_move_to (builder, scale * x, 0);
          gsk_path_builder_rel_line_to (builder, 0, height);
        }

      for (float y = 0; y <= h; y += 1.f)
        {
          gsk_path_builder_move_to (builder, 0, scale * y);
          gsk_path_builder_rel_line_to (builder, width, 0);
        }

      grid_path = gsk_path_builder_free_to_path (builder);
      gtk_snapshot_push_stroke (snapshot, grid_path, stroke);
      gtk_snapshot_append_color (snapshot,
                                 &grid_color,
                                 &GRAPHENE_RECT_INIT (0, 0, width, height));
      gtk_snapshot_pop (snapshot);
    }

  if (self->show_bounds)
    {
      float border_width[4];
      GdkRGBA border_color[4];

      for (int i = 0; i < 4; i++)
        {
          border_width[i] = 1;
          border_color[i] = colors[GTK_SYMBOLIC_COLOR_FOREGROUND];
        }

      gtk_snapshot_append_border (snapshot,
                                  &GSK_ROUNDED_RECT_INIT (0, 0, scale * w, scale * h),
                                  border_width, border_color);
    }

  gtk_symbolic_paintable_snapshot_with_weight (GTK_SYMBOLIC_PAINTABLE (self->paintable),
                                               snapshot,
                                               width, height,
                                               colors, n_colors,
                                               weight);

  if (self->show_spines)
    {
      graphene_rect_t bounds = GRAPHENE_RECT_INIT (0, 0, width, height);
      GdkRGBA c = (GdkRGBA) { 1, 0, 0, 1 };
      g_autoptr (GskStroke) stroke = NULL;
      unsigned int state;
      graphene_rect_t viewport;
      SvgElement *shape;

      stroke = gsk_stroke_new (1.f/scale);

      gtk_snapshot_save (snapshot);
      gtk_snapshot_scale (snapshot, scale, scale);

      state = gtk_svg_get_state (svg);
      graphene_rect_init (&viewport, 0, 0, svg->width, svg->height);
      shape = svg->content;

      snapshot_spines (snapshot, &bounds, self->paintable, shape, state, &viewport, scale, &c, stroke);

      gtk_snapshot_restore (snapshot);
    }
}

static void
border_paintable_snapshot_symbolic (GtkSymbolicPaintable  *paintable,
                                    GtkSnapshot           *snapshot,
                                    double                 width,
                                    double                 height,
                                    const GdkRGBA         *colors,
                                    size_t                 n_colors)
{
  border_paintable_snapshot_with_weight (paintable,
                                         snapshot,
                                         width, height,
                                         colors, n_colors,
                                         400);
}

static void
border_paintable_init_symbolic_paintable_interface (GtkSymbolicPaintableInterface *iface)
{
  iface->snapshot_symbolic = border_paintable_snapshot_symbolic;
  iface->snapshot_with_weight = border_paintable_snapshot_with_weight;
}

/* }}} */
/* {{{ GdkPaintable implementation */

static void
border_paintable_snapshot (GdkPaintable  *paintable,
                           GtkSnapshot   *snapshot,
                           double         width,
                           double         height)
{
  border_paintable_snapshot_symbolic (GTK_SYMBOLIC_PAINTABLE (paintable),
                                      snapshot,
                                      width, height,
                                      NULL, 0);
}

static int
border_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
  BorderPaintable *self = BORDER_PAINTABLE (paintable);

  if (self->paintable)
    return gdk_paintable_get_intrinsic_width (GDK_PAINTABLE (self->paintable));

  return 0;
}

static int
border_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
  BorderPaintable *self = BORDER_PAINTABLE (paintable);

  if (self->paintable)
    return gdk_paintable_get_intrinsic_height (GDK_PAINTABLE (self->paintable));

  return 0;
}

static void
border_paintable_init_paintable_interface (GdkPaintableInterface *iface)
{
  iface->snapshot = border_paintable_snapshot;
  iface->get_intrinsic_width = border_paintable_get_intrinsic_width;
  iface->get_intrinsic_height = border_paintable_get_intrinsic_height;
}

/* }}} */
/* {{{ GObject boilerplate */

struct _BorderPaintableClass
{
  GObjectClass parent_class;
};

enum
{
  PROP_PAINTABLE = 1,
  PROP_SHOW_BOUNDS,
  PROP_SHOW_SPINES,
  PROP_SHOW_GRID,
  NUM_PROPERTIES,
};

static GParamSpec *properties[NUM_PROPERTIES];

G_DEFINE_TYPE_WITH_CODE (BorderPaintable, border_paintable, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
                                                border_paintable_init_paintable_interface)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_SYMBOLIC_PAINTABLE,
                                                border_paintable_init_symbolic_paintable_interface))

static void
border_paintable_init (BorderPaintable *self)
{
}

static void
border_paintable_dispose (GObject *object)
{
  BorderPaintable *self = BORDER_PAINTABLE (object);

  if (self->paintable)
    {
      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_contents, self);
      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_size, self);
    }

  g_clear_object (&self->paintable);

  G_OBJECT_CLASS (border_paintable_parent_class)->dispose (object);
}

static void
border_paintable_get_property (GObject      *object,
                               unsigned int  property_id,
                               GValue       *value,
                               GParamSpec   *pspec)
{
  BorderPaintable *self = BORDER_PAINTABLE (object);

  switch (property_id)
    {
    case PROP_PAINTABLE:
      g_value_set_object (value, self->paintable);
      break;

    case PROP_SHOW_BOUNDS:
      g_value_set_boolean (value, self->show_bounds);
      break;

    case PROP_SHOW_SPINES:
      g_value_set_boolean (value, self->show_spines);
      break;

    case PROP_SHOW_GRID:
      g_value_set_boolean (value, self->show_grid);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
border_paintable_set_property (GObject      *object,
                               unsigned int  property_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  BorderPaintable *self = BORDER_PAINTABLE (object);

  switch (property_id)
    {
    case PROP_PAINTABLE:
      border_paintable_set_paintable (self, g_value_get_object (value));
      break;

    case PROP_SHOW_BOUNDS:
      border_paintable_set_show_bounds (self, g_value_get_boolean (value));
      break;

    case PROP_SHOW_SPINES:
      border_paintable_set_show_spines (self, g_value_get_boolean (value));
      break;

    case PROP_SHOW_GRID:
      border_paintable_set_show_grid (self, g_value_get_boolean (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
border_paintable_class_init (BorderPaintableClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->dispose = border_paintable_dispose;
  object_class->get_property = border_paintable_get_property;
  object_class->set_property = border_paintable_set_property;

  properties[PROP_PAINTABLE] =
    g_param_spec_object ("paintable", NULL, NULL,
                         PATH_PAINTABLE_TYPE,
                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_NAME);

  properties[PROP_SHOW_BOUNDS] =
    g_param_spec_boolean ("show-bounds", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_NAME);

  properties[PROP_SHOW_SPINES] =
    g_param_spec_boolean ("show-spines", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_NAME);

  properties[PROP_SHOW_GRID] =
    g_param_spec_boolean ("show-grid", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_NAME);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
}

/* }}} */
/* {{{ Public API */

BorderPaintable *
border_paintable_new (void)
{
  return g_object_new (border_paintable_get_type (), NULL);
}

void
border_paintable_set_paintable (BorderPaintable *self,
                                PathPaintable   *paintable)
{
  if (self->paintable)
    {
      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_contents, self);
      g_signal_handlers_disconnect_by_func (self->paintable, gdk_paintable_invalidate_size, self);
    }

  if (!g_set_object (&self->paintable, paintable))
    return;

  if (self->paintable)
    {
      g_signal_connect_swapped (self->paintable, "invalidate-contents",
                                G_CALLBACK (gdk_paintable_invalidate_contents), self);
      g_signal_connect_swapped (self->paintable, "invalidate-size",
                                G_CALLBACK (gdk_paintable_invalidate_size), self);
    }

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PAINTABLE]);
  gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

PathPaintable *
border_paintable_get_paintable (BorderPaintable *self)
{
  return self->paintable;
}

void
border_paintable_set_show_bounds (BorderPaintable *self,
                                  gboolean         show_bounds)
{
  if (self->show_bounds == show_bounds)
    return;

  self->show_bounds = show_bounds;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_BOUNDS]);
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

gboolean
border_paintable_get_show_bounds (BorderPaintable *self)
{
  return self->show_bounds;
}

void
border_paintable_set_show_spines (BorderPaintable *self,
                                  gboolean         show_spines)
{
  if (self->show_spines == show_spines)
    return;

  self->show_spines = show_spines;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_SPINES]);
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

gboolean
border_paintable_get_show_spines (BorderPaintable *self)
{
  return self->show_spines;
}

void
border_paintable_set_show_grid (BorderPaintable *self,
                                gboolean         show_grid)
{
  if (self->show_grid == show_grid)
    return;

  self->show_grid = show_grid;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_GRID]);
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
}

gboolean
border_paintable_get_show_grid (BorderPaintable *self)
{
  return self->show_grid;
}

/* }}} */

/* vim:set foldmethod=marker: */
