/*
 * 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 "path-editor.h"
#include "gtk/svg/gtksvgelementprivate.h"


struct _PathEditor
{
  GtkWidget parent_instance;

  double width;
  double height;
  GskPath *path;
  PathPaintable *paintable;

  GtkStack *path_cmds_stack;
  GtkLabel *path_cmds_label;
  GtkEntry *path_cmds_entry;
  GtkButton *external_edit_button;
  GtkMenuButton *id_dropdown;
};

enum
{
  PROP_PATH = 1,
  PROP_WIDTH,
  PROP_HEIGHT,
  PROP_ID,
  NUM_PROPERTIES,
};

static GParamSpec *properties[NUM_PROPERTIES];

/* {{{ Path-to-SVG */

static gboolean
collect_path (GskPathOperation        op,
              const graphene_point_t *pts,
              size_t                  n_pts,
              float                   weight,
              gpointer                user_data)
{
  GskPathBuilder *builder = user_data;

  switch (op)
    {
    case GSK_PATH_MOVE:
      gsk_path_builder_move_to (builder, pts[0].x, pts[0].y);
      break;

    case GSK_PATH_CLOSE:
      gsk_path_builder_close (builder);
      break;

    case GSK_PATH_LINE:
      gsk_path_builder_line_to (builder, pts[1].x, pts[1].y);
      break;

    case GSK_PATH_QUAD:
      gsk_path_builder_quad_to (builder, pts[1].x, pts[1].y,
                                         pts[2].x, pts[2].y);
      break;

    case GSK_PATH_CUBIC:
      gsk_path_builder_cubic_to (builder, pts[1].x, pts[1].y,
                                          pts[2].x, pts[2].y,
                                          pts[3].x, pts[3].y);
      break;

    case GSK_PATH_CONIC:
      gsk_path_builder_conic_to (builder, pts[1].x, pts[1].y,
                                          pts[2].x, pts[2].y,
                                          weight);
      break;

    default:
      g_assert_not_reached ();
    }

  return TRUE;
}

static GskPath *
path_to_svg_path (GskPath *path)
{
  GskPathBuilder *builder = gsk_path_builder_new ();

  gsk_path_foreach (path,
                    GSK_PATH_FOREACH_ALLOW_QUAD | GSK_PATH_FOREACH_ALLOW_CUBIC,
                    collect_path,
                    builder);

  return gsk_path_builder_free_to_path (builder);
}

/* }}} */
/* {{{ Callbacks */

static void
collect_ids (SvgElement    *shape,
             gpointer  data)
{
  GMenu *menu = data;

  if (svg_element_get_type (shape) == SVG_ELEMENT_CLIP_PATH &&
      svg_element_get_id (shape) != NULL)
    {
      g_autoptr (GMenuItem) item = NULL;

      item = g_menu_item_new (svg_element_get_id (shape), NULL);
      g_menu_item_set_action_and_target (item, "set_id", "s", svg_element_get_id (shape));
      g_menu_append_item (menu, item);
    }
}

static void
path_cmds_clicked (PathEditor *self)
{
  if (self->paintable)
    {
      g_autoptr (GMenu) menu = g_menu_new ();
      g_autoptr (GMenuItem) item = NULL;

      item = g_menu_item_new ("None", NULL);
      g_menu_item_set_action_and_target (item, "set_id", "s", "");
      g_menu_append_item (menu, item);

      svg_element_foreach (path_paintable_get_svg (self->paintable)->content, collect_ids, menu);

      gtk_menu_button_set_menu_model (self->id_dropdown, G_MENU_MODEL (menu));
    }

  gtk_stack_set_visible_child_name (self->path_cmds_stack, "entry");

  gtk_entry_grab_focus_without_selecting (self->path_cmds_entry);
}

static gboolean
path_cmds_key (PathEditor      *self,
               unsigned int     keyval,
               unsigned int     keycode,
               GdkModifierType  state)
{
  if (keyval == GDK_KEY_Escape)
    {
      g_autofree char *text = NULL;

      text = gsk_path_to_string (self->path);
      gtk_editable_set_text (GTK_EDITABLE (self->path_cmds_entry), text);

      gtk_widget_remove_css_class (GTK_WIDGET (self->path_cmds_entry), "error");
      gtk_accessible_reset_state (GTK_ACCESSIBLE (self->path_cmds_entry), GTK_ACCESSIBLE_STATE_INVALID);
      gtk_stack_set_visible_child_name (self->path_cmds_stack, "label");
      return TRUE;
    }

  return FALSE;
}

static void
path_cmds_activated (PathEditor *self)
{
  const char *text;
  g_autoptr (GskPath) path = NULL;
  const char *id = NULL;

  text = gtk_editable_get_text (GTK_EDITABLE (self->path_cmds_entry));
  path = gsk_path_parse (text);

  if (self->paintable)
    {
      SvgElement *shape = path_paintable_get_shape_by_id (self->paintable, text);
      if (shape && svg_element_get_type (shape) == SVG_ELEMENT_CLIP_PATH)
        id = text;
    }

  if (!path && !id)
    {
      gtk_widget_error_bell (GTK_WIDGET (self->path_cmds_entry));
      gtk_widget_add_css_class (GTK_WIDGET (self->path_cmds_entry), "error");
      gtk_accessible_update_state (GTK_ACCESSIBLE (self->path_cmds_entry),
                                   GTK_ACCESSIBLE_STATE_INVALID, GTK_ACCESSIBLE_INVALID_TRUE,
                                   -1);
      return;
    }

  gtk_widget_remove_css_class (GTK_WIDGET (self->path_cmds_entry), "error");
  gtk_accessible_reset_state (GTK_ACCESSIBLE (self->path_cmds_entry), GTK_ACCESSIBLE_STATE_INVALID);

  if (strcmp (text, "") == 0)
    gtk_label_set_label (self->path_cmds_label, "—");
  else
    gtk_label_set_label (self->path_cmds_label, text);
  gtk_stack_set_visible_child_name (self->path_cmds_stack, "label");

  g_clear_pointer (&self->path, gsk_path_unref);
  if (path)
    self->path = gsk_path_ref (path);

  gtk_widget_set_sensitive (GTK_WIDGET (self->external_edit_button), path != NULL);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PATH]);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ID]);
}

static void
show_error (PathEditor *self,
            const char *title,
            const char *detail)
{
  GtkWindow *window = GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self)));
  g_autoptr (GtkAlertDialog) alert = NULL;

  alert = gtk_alert_dialog_new ("title");
  gtk_alert_dialog_set_detail (alert, detail);
  gtk_alert_dialog_show (alert, window);
}

static void
temp_file_changed (GFileMonitor      *monitor,
                   GFile             *file,
                   GFile             *other,
                   GFileMonitorEvent  event,
                   PathEditor        *self)
{
  if (event == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
    {
      g_autoptr (GBytes) bytes = NULL;
      g_autoptr (GError) error = NULL;
      g_autoptr (PathPaintable) paintable = NULL;

      bytes = g_file_load_bytes (file, NULL, NULL, &error);
      if (!bytes)
        {
          show_error (self, "Editing Failed", error->message);
          return;
        }

      paintable = path_paintable_new_from_bytes (bytes, &error);
      if (!paintable)
        {
          show_error (self, "Editing Failed", error->message);
          return;
        }

      gsk_path_unref (self->path);
      self->path = gsk_path_ref (path_paintable_get_path_by_id (paintable, "path0"));

      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PATH]);
    }
}

static void
file_launched (GObject      *source,
               GAsyncResult *result,
               gpointer      data)
{
  GtkFileLauncher *launcher = GTK_FILE_LAUNCHER (source);
  g_autoptr (GError) error = NULL;

  if (!gtk_file_launcher_launch_finish (launcher, result, &error))
    {
      g_print ("Failed to launch path editor: %s", error->message);
    }
}

static void
edit_path_externally (PathEditor *self)
{
  GString *str;
  g_autoptr (GBytes) bytes = NULL;
  g_autofree char *filename = NULL;
  g_autoptr (GFile) file = NULL;
  g_autoptr (GOutputStream) ostream = NULL;
  gssize written;
  g_autoptr (GError) error = NULL;
  GFileMonitor *monitor;
  g_autoptr (GskPath) path = NULL;
  g_autoptr(GtkFileLauncher) launcher = NULL;
  GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));

  if (!self->path)
    {
      gtk_widget_error_bell (GTK_WIDGET (self));
      return;
    }

  path = path_to_svg_path (self->path);

  str = g_string_new ("");
  g_string_append_printf (str, "<svg width='%g' height='%g'>\n",
                          self->width, self->height);

  g_string_append (str, "<path id='path0'\n"
                        "      d='");
  if (path)
    gsk_path_print (path, str);

  g_string_append (str, "'\n");

  g_string_append (str,
                   "      fill='none'\n"
                   "      stroke='black'\n"
                   "      stroke-width='2'\n"
                   "      stroke-linejoin='round'\n"
                   "      stroke-linecap='round'/>\n");

  g_string_append (str, "</svg>");
  bytes = g_string_free_to_bytes (str);

  filename = g_build_filename (g_get_user_cache_dir (), "org.gtk.Shaper-path.svg", NULL);
  file = g_file_new_for_path (filename);
  ostream = G_OUTPUT_STREAM (g_file_replace (file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error));
  if (!ostream)
    {
      show_error (self, "Editing Failed", error->message);
      return;
    }

  written = g_output_stream_write_bytes (ostream, bytes, NULL, &error);
  if (written == -1)
    {
      show_error (self, "Editing Failed", error->message);
      return;
    }

  g_output_stream_close (ostream, NULL, NULL);

  monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &error);
  if (error)
    {
      show_error (self, "Editing Failed", error->message);
      return;
    }

  g_signal_connect_object (monitor, "changed",
                           G_CALLBACK (temp_file_changed), self, G_CONNECT_DEFAULT);

  launcher = gtk_file_launcher_new (file);

  gtk_file_launcher_set_writable (launcher, TRUE);

  gtk_file_launcher_launch (launcher, GTK_WINDOW (root), NULL, file_launched, self);
}

static void
edit_path (PathEditor *self)
{
  edit_path_externally (self);
}

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

struct _PathEditorClass
{
  GtkWidgetClass parent_class;
};

G_DEFINE_TYPE (PathEditor, path_editor, GTK_TYPE_WIDGET)

static void
path_editor_init (PathEditor *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));
}

static void
path_editor_set_property (GObject      *object,
                          unsigned int  prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  PathEditor *self = PATH_EDITOR (object);

  switch (prop_id)
    {
    case PROP_PATH:
      path_editor_set_path (self, g_value_get_boxed (value));
      break;

    case PROP_WIDTH:
      self->width = g_value_get_double (value);
      break;

    case PROP_HEIGHT:
      self->height = g_value_get_double (value);
      break;

    case PROP_ID:
      path_editor_set_id (self, g_value_get_string (value));
      break;

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

static void
path_editor_get_property (GObject      *object,
                          unsigned int  prop_id,
                          GValue       *value,
                          GParamSpec   *pspec)
{
  PathEditor *self = PATH_EDITOR (object);

  switch (prop_id)
    {
    case PROP_PATH:
      g_value_set_boxed (value, self->path);
      break;

    case PROP_WIDTH:
      g_value_set_double (value, self->width);
      break;

    case PROP_HEIGHT:
      g_value_set_double (value, self->height);
      break;

    case PROP_ID:
      g_value_set_string (value, gtk_label_get_label (self->path_cmds_label));
      break;

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

static void
path_editor_dispose (GObject *object)
{
  PathEditor *self = PATH_EDITOR (object);

  gtk_widget_dispose_template (GTK_WIDGET (object), PATH_EDITOR_TYPE);

  g_clear_object (&self->paintable);

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

static void
path_editor_finalize (GObject *object)
{
  PathEditor *self = PATH_EDITOR (object);

  g_clear_pointer (&self->path, gsk_path_unref);

  G_OBJECT_CLASS (path_editor_parent_class)->finalize (object);
}

static void
set_id (GtkWidget  *widget,
        const char *action_name,
        GVariant   *parameter)
{
  PathEditor *self = PATH_EDITOR (widget);
  const char *id = g_variant_get_string (parameter, NULL);

  if (strcmp (id, "") == 0)
    {
      g_autoptr (GskPath) path = gsk_path_parse ("");
      path_editor_set_path (self, path);
    }
  else
    path_editor_set_id (self, id);

  gtk_stack_set_visible_child_name (self->path_cmds_stack, "label");
}

static void
path_editor_class_init (PathEditorClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);

  object_class->set_property = path_editor_set_property;
  object_class->get_property = path_editor_get_property;
  object_class->dispose = path_editor_dispose;
  object_class->finalize = path_editor_finalize;

  properties[PROP_PATH] =
    g_param_spec_boxed ("path", NULL, NULL,
                        GSK_TYPE_PATH,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_WIDTH] =
    g_param_spec_double ("width", NULL, NULL,
                         0, DBL_MAX, 0,
                         G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_HEIGHT] =
    g_param_spec_double ("height", NULL, NULL,
                         0, DBL_MAX, 0,
                         G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_ID] =
    g_param_spec_string ("id", NULL, NULL,
                         NULL,
                         G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/Shaper/path-editor.ui");

  gtk_widget_class_bind_template_child (widget_class, PathEditor, path_cmds_stack);
  gtk_widget_class_bind_template_child (widget_class, PathEditor, path_cmds_label);
  gtk_widget_class_bind_template_child (widget_class, PathEditor, path_cmds_entry);
  gtk_widget_class_bind_template_child (widget_class, PathEditor, external_edit_button);
  gtk_widget_class_bind_template_child (widget_class, PathEditor, id_dropdown);

  gtk_widget_class_bind_template_callback (widget_class, path_cmds_clicked);
  gtk_widget_class_bind_template_callback (widget_class, path_cmds_activated);
  gtk_widget_class_bind_template_callback (widget_class, path_cmds_key);
  gtk_widget_class_bind_template_callback (widget_class, edit_path);

  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);

  gtk_widget_class_install_action (widget_class, "set_id", "s", set_id);
}

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

PathEditor *
path_editor_new (GskPath *path,
                 double   width,
                 double   height)
{
  return g_object_new (PATH_EDITOR_TYPE,
                       "path", path,
                       "width", width,
                       "height", height,
                       NULL);
}

GskPath *
path_editor_get_path (PathEditor *self)
{
  g_return_val_if_fail (PATH_IS_EDITOR (self), NULL);

  return self->path;
}

void
path_editor_set_path (PathEditor *self,
                      GskPath    *path)
{
  if (self->path == path ||
      (self->path && path && gsk_path_equal (self->path, path)))
    return;

  g_clear_pointer (&self->path, gsk_path_unref);
  self->path = gsk_path_ref (path);

  if (gsk_path_is_empty (path))
    {
      gtk_label_set_label (self->path_cmds_label, "—");
      gtk_editable_set_text (GTK_EDITABLE (self->path_cmds_entry), "");
    }
  else
    {
      g_autofree char *text = gsk_path_to_string (path);
      gtk_label_set_label (self->path_cmds_label, text);
      gtk_editable_set_text (GTK_EDITABLE (self->path_cmds_entry), text);
    }

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PATH]);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ID]);
}

void
path_editor_set_id (PathEditor *self,
                    const char *id)
{
  g_return_if_fail (self->paintable != NULL);

  g_clear_pointer (&self->path, gsk_path_unref);
  gtk_label_set_label (self->path_cmds_label, id);
  gtk_editable_set_text (GTK_EDITABLE (self->path_cmds_entry), id);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PATH]);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ID]);
}

const char *
path_editor_get_id (PathEditor *self)
{
  if (self->paintable == NULL)
    return NULL;

  return gtk_editable_get_text (GTK_EDITABLE (self->path_cmds_entry));
}

void
path_editor_set_paintable (PathEditor *self,
                           PathPaintable *paintable)
{
  g_clear_object (&self->paintable);
  self->paintable = g_object_ref (paintable);
  gtk_widget_set_visible (GTK_WIDGET (self->id_dropdown), TRUE);
}

/* }}} */

/* vim:set foldmethod=marker: */
