/*
 * Copyright © 2019 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.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/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 */

#include "config.h"

#include "node-editor-window.h"

#include "gtkrendererpaintableprivate.h"

#include "gsk/gskrendernodeparserprivate.h"
#ifdef GDK_WINDOWING_BROADWAY
#include "gsk/broadway/gskbroadwayrenderer.h"
#endif

#include <glib/gstdio.h>

#include <cairo.h>
#ifdef CAIRO_HAS_SVG_SURFACE
#include <cairo-svg.h>
#endif

/* Hardcode zoom levels that fractional scaling is likely to use.
 * Be aware that people using this might have fractional
 * scaling enabled themselves. and might want to view stuff at device scale */
static float zoom_levels[] = {
  /* TINY */ 1.0 / 8.0,
  /*  25% */ 1.0 / 4.0,
  /*  33% */ 1.0 / 3.0,
  /*  40% */ 2.0 / 5.0,
  /*  50% */ 1.0 / 2.0,
  /*  67% */ 2.0 / 3.0,
  /*  80% */ 4.0 / 5.0,
  /* d'oh */ 1.0,
  /* 125% */ 5.0 / 4.0,
  /* 150% */ 3.0 / 2.0,
  /* 175% */ 7.0 / 4.0,
  /* 200% */ 2.0,
  /* 250% */ 5.0 / 2.0,
  /* 300% */ 3.0,
  /* 400% */ 4.0,
  /* HUGE */ 8.0
};

typedef struct
{
  gsize  start_chars;
  gsize  end_chars;
  char  *message;
} TextViewError;

struct _NodeEditorWindow
{
  GtkApplicationWindow parent;

  GtkWidget *picture;
  GtkWidget *text_view;
  GtkTextBuffer *text_buffer;
  GtkTextTagTable *tag_table;

  GtkWidget *testcase_popover;
  GtkWidget *testcase_error_label;
  GtkWidget *testcase_cairo_checkbutton;
  GtkWidget *testcase_name_entry;
  GtkWidget *testcase_save_button;
  GtkWidget *zoom_in;
  GtkWidget *zoom_out;
  GtkWidget *zoom_label;
  GtkWidget *crash_warning;

  GtkWidget *renderer_listbox;
  GListStore *renderers;
  GskRenderNode *node;

  GFile *file;
  GFileMonitor *file_monitor;

  GArray *errors;

  gboolean auto_reload;
  gboolean mark_as_safe_pending;
  gulong after_paint_handler;

  int zoom_level;
  gboolean dark_mode;
  int paned_position;
};

struct _NodeEditorWindowClass
{
  GtkApplicationWindowClass parent_class;
};

enum {
  PROP_AUTO_RELOAD = 1,
  PROP_ZOOM_LEVEL,
  PROP_DARK_MODE,
  PROP_PANED_POSITION,
  NUM_PROPERTIES
};

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };

G_DEFINE_TYPE(NodeEditorWindow, node_editor_window, GTK_TYPE_APPLICATION_WINDOW);

static void
text_view_error_free (TextViewError *e)
{
  g_free (e->message);
}

static char *
get_current_text (GtkTextBuffer *buffer)
{
  GtkTextIter start, end;

  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_get_end_iter (buffer, &end);

  return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
}

static void
text_buffer_remove_all_tags (GtkTextBuffer *buffer)
{
  GtkTextIter start, end;

  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_get_end_iter (buffer, &end);
  gtk_text_buffer_remove_all_tags (buffer, &start, &end);
}

static void
deserialize_error_func (const GskParseLocation *start_location,
                        const GskParseLocation *end_location,
                        const GError           *error,
                        gpointer                user_data)
{
  NodeEditorWindow *self = user_data;
  GtkTextIter start_iter, end_iter;
  TextViewError text_view_error;

  gtk_text_buffer_get_iter_at_line_offset (self->text_buffer, &start_iter,
                                           start_location->lines,
                                           start_location->line_chars);
  gtk_text_buffer_get_iter_at_line_offset (self->text_buffer, &end_iter,
                                           end_location->lines,
                                           end_location->line_chars);

  gtk_text_buffer_apply_tag_by_name (self->text_buffer, "error",
                                     &start_iter, &end_iter);

  text_view_error.start_chars = start_location->chars;
  text_view_error.end_chars = end_location->chars;
  text_view_error.message = g_strdup (error->message);
  g_array_append_val (self->errors, text_view_error);
}

static void
text_iter_skip_alpha_backward (GtkTextIter *iter)
{
  /* Just skip to the previous non-whitespace char */

  while (!gtk_text_iter_is_start (iter))
    {
      gunichar c = gtk_text_iter_get_char (iter);

      if (g_unichar_isspace (c))
        {
          gtk_text_iter_forward_char (iter);
          break;
        }

      gtk_text_iter_backward_char (iter);
    }
}

static void
text_iter_skip_whitespace_backward (GtkTextIter *iter)
{
  while (!gtk_text_iter_is_start (iter))
    {
      gunichar c = gtk_text_iter_get_char (iter);

      if (g_unichar_isalpha (c))
        {
          gtk_text_iter_forward_char (iter);
          break;
        }

      gtk_text_iter_backward_char (iter);
    }
}

static void
highlight_text (NodeEditorWindow *self)
{
  GtkTextIter iter;
  GtkTextIter start, end;

  gtk_text_buffer_get_start_iter (self->text_buffer, &iter);

  while (!gtk_text_iter_is_end (&iter))
    {
      gunichar c = gtk_text_iter_get_char (&iter);

      if (c == '/')
        {
          GtkTextIter comment_start = iter;
          gtk_text_iter_forward_char (&iter);

          c = gtk_text_iter_get_char (&iter);
          if (c == '*')
            {
              gtk_text_iter_forward_char (&iter);
              while (!gtk_text_iter_is_end (&iter))
                {
                  c = gtk_text_iter_get_char (&iter);
                  gtk_text_iter_forward_char (&iter);

                  if (c == '*')
                    {
                      c = gtk_text_iter_get_char (&iter);
                      gtk_text_iter_forward_char (&iter);
                      if (c == '/')
                        break;
                    }
                }
            }

          gtk_text_buffer_apply_tag_by_name (self->text_buffer, "comment", &comment_start, &iter);
        }
      else if (c == '{')
        {
          GtkTextIter word_end = iter;
          GtkTextIter word_start;

          gtk_text_iter_backward_char (&word_end);
          text_iter_skip_whitespace_backward (&word_end);

          word_start = word_end;
          gtk_text_iter_backward_word_start (&word_start);
          text_iter_skip_alpha_backward (&word_start);

          gtk_text_buffer_apply_tag_by_name (self->text_buffer, "nodename", &word_start, &word_end);
        }
      else if (c == ':')
        {
          GtkTextIter word_end = iter;
          GtkTextIter word_start;

          gtk_text_iter_backward_char (&word_end);
          text_iter_skip_whitespace_backward (&word_end);

          word_start = word_end;
          gtk_text_iter_backward_word_start (&word_start);
          text_iter_skip_alpha_backward (&word_start);

          gtk_text_buffer_apply_tag_by_name (self->text_buffer, "propname", &word_start, &word_end);
        }
      else if (c == '"')
        {
          GtkTextIter string_start = iter;
          GtkTextIter string_end = iter;

          gtk_text_iter_forward_char (&iter);
          while (!gtk_text_iter_is_end (&iter))
            {
              c = gtk_text_iter_get_char (&iter);

              if (c == '"')
                {
                  gtk_text_iter_forward_char (&iter);
                  string_end = iter;
                  break;
                }

              gtk_text_iter_forward_char (&iter);
            }

          gtk_text_buffer_apply_tag_by_name (self->text_buffer, "string", &string_start, &string_end);
        }

      gtk_text_iter_forward_char (&iter);
    }

  gtk_text_buffer_get_bounds (self->text_buffer, &start, &end);
  gtk_text_buffer_apply_tag_by_name (self->text_buffer, "no-hyphens", &start, &end);
}

static void
mark_autosave_as_unsafe (void)
{
  char *path1 = NULL;
  char *path2 = NULL;

  path1 = get_autosave_path ("-unsafe");
  path2 = get_autosave_path (NULL);

  g_rename (path2, path1);

  g_free (path1);
  g_free (path2);
}

static void
mark_autosave_as_safe (void)
{
  char *path1 = NULL;
  char *path2 = NULL;

  path1 = get_autosave_path ("-unsafe");
  path2 = get_autosave_path (NULL);

  g_rename (path1, path2);

  g_free (path1);
  g_free (path2);
}

static void
after_paint (GdkFrameClock    *clock,
             NodeEditorWindow *self)
{
  if (self->mark_as_safe_pending)
    {
      self->mark_as_safe_pending = FALSE;
      mark_autosave_as_safe ();
    }
}

static void
reload (NodeEditorWindow *self)
{
  char *text;
  GBytes *bytes;
  GskRenderNode *big_node;

  mark_autosave_as_unsafe ();

  text = get_current_text (self->text_buffer);
  bytes = g_bytes_new_take (text, strlen (text));

  g_clear_pointer (&self->node, gsk_render_node_unref);

  /* If this is too slow, go fix the parser performance */
  self->node = gsk_render_node_deserialize (bytes, deserialize_error_func, self);

  if (self->node && self->zoom_level != 0)
    {
      GskTransform *transform;
      float scale;

      scale = zoom_levels [self->zoom_level];

      transform = gsk_transform_scale (NULL, scale, scale);
      big_node = gsk_transform_node_new (self->node, transform);

      gsk_transform_unref (transform);
    }
  else if (self->node)
    {
      big_node = gsk_render_node_ref (self->node);
    }
  else
    {
      big_node = NULL;
    }

  g_bytes_unref (bytes);
  if (self->node)
    {
      /* XXX: Is this code necessary or can we have API to turn nodes into paintables? */
      GtkSnapshot *snapshot;
      GdkPaintable *paintable;
      graphene_rect_t bounds;
      guint i;

      snapshot = gtk_snapshot_new ();
      gsk_render_node_get_bounds (big_node, &bounds);
      gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- bounds.origin.x, - bounds.origin.y));
      gtk_snapshot_append_node (snapshot, big_node);
      paintable = gtk_snapshot_free_to_paintable (snapshot, &bounds.size);
      gtk_picture_set_paintable (GTK_PICTURE (self->picture), paintable);
      g_clear_object (&paintable);

      snapshot = gtk_snapshot_new ();
      gsk_render_node_get_bounds (self->node, &bounds);
      gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- bounds.origin.x, - bounds.origin.y));
      gtk_snapshot_append_node (snapshot, self->node);
      paintable = gtk_snapshot_free_to_paintable (snapshot, &bounds.size);

      for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->renderers)); i++)
        {
          gpointer item = g_list_model_get_item (G_LIST_MODEL (self->renderers), i);
          gtk_renderer_paintable_set_paintable (item, paintable);
          g_object_unref (item);
        }

      g_clear_object (&paintable);
    }
  else
    {
      gtk_picture_set_paintable (GTK_PICTURE (self->picture), NULL);
    }

  g_clear_pointer (&big_node, gsk_render_node_unref);

  self->mark_as_safe_pending = TRUE;
}

static void
text_changed (GtkTextBuffer    *buffer,
              NodeEditorWindow *self)
{
  g_array_remove_range (self->errors, 0, self->errors->len);
  text_buffer_remove_all_tags (self->text_buffer);

  if (self->auto_reload)
    reload (self);

  highlight_text (self);
}

static gboolean
text_view_query_tooltip_cb (GtkWidget        *widget,
                            int               x,
                            int               y,
                            gboolean          keyboard_tip,
                            GtkTooltip       *tooltip,
                            NodeEditorWindow *self)
{
  GtkTextIter iter;
  guint i;
  GString *text;

  if (keyboard_tip)
    {
      int offset;

      g_object_get (self->text_buffer, "cursor-position", &offset, NULL);
      gtk_text_buffer_get_iter_at_offset (self->text_buffer, &iter, offset);
    }
  else
    {
      int bx, by, trailing;

      gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (self->text_view), GTK_TEXT_WINDOW_TEXT,
                                             x, y, &bx, &by);
      gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (self->text_view), &iter, &trailing, bx, by);
    }

  text = g_string_new ("");

  for (i = 0; i < self->errors->len; i ++)
    {
      const TextViewError *e = &g_array_index (self->errors, TextViewError, i);
      GtkTextIter start_iter, end_iter;

      gtk_text_buffer_get_iter_at_offset (self->text_buffer, &start_iter, e->start_chars);
      gtk_text_buffer_get_iter_at_offset (self->text_buffer, &end_iter, e->end_chars);

      if (gtk_text_iter_in_range (&iter, &start_iter, &end_iter))
        {
          if (text->len > 0)
            g_string_append (text, "\n");
          g_string_append (text, e->message);
        }
    }

  if (text->len > 0)
    {
      gtk_tooltip_set_text (tooltip, text->str);
      g_string_free (text, TRUE);
      return TRUE;
    }
  else
    {
      g_string_free (text, TRUE);
      return FALSE;
    }
}

static gboolean
load_bytes (NodeEditorWindow *self,
            GBytes           *bytes);

static void
load_error (NodeEditorWindow *self,
            const char        *error_message)
{
  PangoLayout *layout;
  GtkSnapshot *snapshot;
  GskRenderNode *node;
  GBytes *bytes;

  layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), error_message);
  pango_layout_set_width (layout, 300 * PANGO_SCALE);
  snapshot = gtk_snapshot_new ();
  gtk_snapshot_append_layout (snapshot, layout, &(GdkRGBA) { 0.7, 0.13, 0.13, 1.0 });
  node = gtk_snapshot_free_to_node (snapshot);
  bytes = gsk_render_node_serialize (node);

  load_bytes (self, bytes);

  gsk_render_node_unref (node);
  g_object_unref (layout);
}

static gboolean
load_bytes (NodeEditorWindow *self,
            GBytes           *bytes)
{
  if (!g_utf8_validate (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL))
    {
      load_error (self, "Invalid UTF-8");
      g_bytes_unref (bytes);
      return FALSE;
    }

  gtk_text_buffer_set_text (self->text_buffer,
                            g_bytes_get_data (bytes, NULL),
                            g_bytes_get_size (bytes));

  g_bytes_unref (bytes);

  return TRUE;
}

static gboolean
load_file_contents (NodeEditorWindow *self,
                    GFile            *file)
{
  GError *error = NULL;
  GBytes *bytes;

  bytes = g_file_load_bytes (file, NULL, NULL, &error);
  if (bytes == NULL)
    {
      load_error (self, error->message);
      g_clear_error (&error);
      return FALSE;
    }

  return load_bytes (self, bytes);
}

static GdkContentProvider *
on_picture_drag_prepare_cb (GtkDragSource    *source,
                            double            x,
                            double            y,
                            NodeEditorWindow *self)
{
  if (self->node == NULL)
    return NULL;

  return gdk_content_provider_new_typed (GSK_TYPE_RENDER_NODE, self->node);
}

static void
on_picture_drop_read_done_cb (GObject      *source,
                              GAsyncResult *res,
                              gpointer      data)
{
  NodeEditorWindow *self = data;
  GOutputStream *stream = G_OUTPUT_STREAM (source);
  GdkDrop *drop = g_object_get_data (source, "drop");
  GdkDragAction action = 0;
  GBytes *bytes;

  if (g_output_stream_splice_finish (stream, res, NULL) >= 0)
    {
      bytes = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (stream));
      if (load_bytes (self, bytes))
        action = GDK_ACTION_COPY;
    }

  g_object_unref (self);
  gdk_drop_finish (drop, action);
  g_object_unref (drop);
  return;
}

static void
on_picture_drop_read_cb (GObject      *source,
                         GAsyncResult *res,
                         gpointer      data)
{
  NodeEditorWindow *self = data;
  GdkDrop *drop = GDK_DROP (source);
  GInputStream *input;
  GOutputStream *output;

  input = gdk_drop_read_finish (drop, res, NULL, NULL);
  if (input == NULL)
    {
      g_object_unref (self);
      gdk_drop_finish (drop, GDK_ACTION_NONE);
      return;
    }

  output = g_memory_output_stream_new_resizable ();
  g_object_set_data (G_OBJECT (output), "drop", drop);
  g_object_ref (drop);

  g_output_stream_splice_async (output,
                                input,
                                G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
                                G_PRIORITY_DEFAULT,
                                NULL,
                                on_picture_drop_read_done_cb,
                                self);
  g_object_unref (output);
  g_object_unref (input);
}

static gboolean
on_picture_drop_cb (GtkDropTargetAsync *dest,
                    GdkDrop            *drop,
                    double              x,
                    double              y,
                    NodeEditorWindow   *self)
{
  gdk_drop_read_async (drop,
                       (const char *[2]) { "application/x-gtk-render-node", NULL },
                       G_PRIORITY_DEFAULT,
                       NULL,
                       on_picture_drop_read_cb,
                       g_object_ref (self));

  return TRUE;
}

static void
file_changed_cb (GFileMonitor      *monitor,
                 GFile             *file,
                 GFile             *other_file,
                 GFileMonitorEvent  event_type,
                 gpointer           user_data)
{
  NodeEditorWindow *self = user_data;

  if (event_type == G_FILE_MONITOR_EVENT_CHANGED)
    load_file_contents (self, file);
}

gboolean
node_editor_window_load (NodeEditorWindow *self,
                         GFile            *file)
{
  GError *error = NULL;

  g_clear_object (&self->file);
  g_clear_object (&self->file_monitor);

  if (!load_file_contents (self, file))
    return FALSE;

  self->file = g_object_ref (file);
  self->file_monitor = g_file_monitor_file (self->file, G_FILE_MONITOR_NONE, NULL, &error);

  if (error)
    {
      g_warning ("couldn't monitor file: %s", error->message);
      g_error_free (error);
      g_clear_object (&self->file_monitor);
    }
  else
    {
      g_signal_connect (self->file_monitor, "changed", G_CALLBACK (file_changed_cb), self);
    }

  return TRUE;
}

static void
open_response_cb (GObject *source,
                  GAsyncResult *result,
                  void *user_data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  NodeEditorWindow *self = user_data;
  GFile *file;

  file = gtk_file_dialog_open_finish (dialog, result, NULL);
  if (file)
    {
      node_editor_window_load (self, file);
      g_object_unref (file);
    }
}

static void
show_open_filechooser (NodeEditorWindow *self)
{
  GtkFileDialog *dialog;

  dialog = gtk_file_dialog_new ();
  gtk_file_dialog_set_title (dialog, "Open node file");
  if (self->file)
    {
      gtk_file_dialog_set_initial_file (dialog, self->file);
    }
  else
    {
      GFile *cwd;
      cwd = g_file_new_for_path (".");
      gtk_file_dialog_set_initial_folder (dialog, cwd);
      g_object_unref (cwd);
    }

  gtk_file_dialog_open (dialog, GTK_WINDOW (self),
                        NULL, open_response_cb, self);
  g_object_unref (dialog);
}

static void
open_cb (GtkWidget        *button,
         NodeEditorWindow *self)
{
  show_open_filechooser (self);
}

static void
save_response_cb (GObject *source,
                  GAsyncResult *result,
                  void *user_data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  NodeEditorWindow *self = user_data;
  GFile *file;

  file = gtk_file_dialog_save_finish (dialog, result, NULL);
  if (file)
    {
      char *text;
      GError *error = NULL;

      text = get_current_text (self->text_buffer);

      g_file_replace_contents (file, text, strlen (text),
                               NULL, FALSE,
                               G_FILE_CREATE_NONE,
                               NULL,
                               NULL,
                               &error);
      if (error != NULL)
        {
          GtkAlertDialog *alert;

          alert = gtk_alert_dialog_new ("Saving failed");
          gtk_alert_dialog_set_detail (alert, error->message);
          gtk_alert_dialog_show (alert,
                                 GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))));
          g_object_unref (alert);
          g_error_free (error);
        }

      g_free (text);
      g_object_unref (file);
    }
}

static void
save_cb (GtkWidget        *button,
         NodeEditorWindow *self)
{
  GtkFileDialog *dialog;

  dialog = gtk_file_dialog_new ();
  gtk_file_dialog_set_title (dialog, "Save node");
  if (self->file)
    {
      gtk_file_dialog_set_initial_file (dialog, self->file);
    }
  else
    {
      GFile *cwd = g_file_new_for_path (".");
      gtk_file_dialog_set_initial_folder (dialog, cwd);
      gtk_file_dialog_set_initial_name (dialog, "demo.node");
      g_object_unref (cwd);
    }

  gtk_file_dialog_save (dialog,
                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
                        NULL,
                        save_response_cb, self);
  g_object_unref (dialog);
}

static GskRenderNode *
create_node (NodeEditorWindow *self)
{
  GdkPaintable *paintable;
  GtkSnapshot *snapshot;
  GskRenderNode *node;

  paintable = gtk_picture_get_paintable (GTK_PICTURE (self->picture));
  if (paintable == NULL ||
      gdk_paintable_get_intrinsic_width (paintable) <= 0 ||
      gdk_paintable_get_intrinsic_height (paintable) <= 0)
    return NULL;

  snapshot = gtk_snapshot_new ();
  gdk_paintable_snapshot (paintable, snapshot, gdk_paintable_get_intrinsic_width (paintable), gdk_paintable_get_intrinsic_height (paintable));
  node = gtk_snapshot_free_to_node (snapshot);

  return node;
}

static GdkTexture *
create_texture (NodeEditorWindow *self)
{
  GskRenderer *renderer;
  GskRenderNode *node;
  GdkTexture *texture;

  node = create_node (self);
  if (node == NULL)
    return NULL;

  renderer = gtk_native_get_renderer (gtk_widget_get_native (GTK_WIDGET (self)));
  texture = gsk_renderer_render_texture (renderer, node, NULL);
  gsk_render_node_unref (node);

  return texture;
}

#ifdef CAIRO_HAS_SVG_SURFACE
static cairo_status_t
cairo_serializer_write (gpointer             user_data,
                        const unsigned char *data,
                        unsigned int         length)
{
  g_byte_array_append (user_data, data, length);

  return CAIRO_STATUS_SUCCESS;
}

static GBytes *
create_svg (GskRenderNode  *node,
            GError        **error)
{
  cairo_surface_t *surface;
  cairo_t *cr;
  graphene_rect_t bounds;
  GByteArray *array;

  gsk_render_node_get_bounds (node, &bounds);
  array = g_byte_array_new ();

  surface = cairo_svg_surface_create_for_stream (cairo_serializer_write,
                                                 array,
                                                 bounds.size.width,
                                                 bounds.size.height);
  cairo_svg_surface_set_document_unit (surface, CAIRO_SVG_UNIT_PX);
  cairo_surface_set_device_offset (surface, -bounds.origin.x, -bounds.origin.y);

  cr = cairo_create (surface);
  gsk_render_node_draw (node, cr);
  cairo_destroy (cr);

  cairo_surface_finish (surface);
  if (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS)
    {
      cairo_surface_destroy (surface);
      return g_byte_array_free_to_bytes (array);
    }
  else
    {
      g_set_error (error,
                   G_IO_ERROR, G_IO_ERROR_FAILED,
                   "%s", cairo_status_to_string (cairo_surface_status (surface)));
      cairo_surface_destroy (surface);
      g_byte_array_unref (array);
      return NULL;
    }
}
#endif

static GdkTexture *
create_cairo_texture (NodeEditorWindow *self)
{
  GdkPaintable *paintable;
  GtkSnapshot *snapshot;
  GskRenderer *renderer;
  GskRenderNode *node;
  GdkTexture *texture;

  paintable = gtk_picture_get_paintable (GTK_PICTURE (self->picture));
  if (paintable == NULL ||
      gdk_paintable_get_intrinsic_width (paintable) <= 0 ||
      gdk_paintable_get_intrinsic_height (paintable) <= 0)
    return NULL;
  snapshot = gtk_snapshot_new ();
  gdk_paintable_snapshot (paintable, snapshot, gdk_paintable_get_intrinsic_width (paintable), gdk_paintable_get_intrinsic_height (paintable));
  node = gtk_snapshot_free_to_node (snapshot);
  if (node == NULL)
    return NULL;

  renderer = gsk_cairo_renderer_new ();
  gsk_renderer_realize_for_display (renderer, gtk_widget_get_display (GTK_WIDGET (self)), NULL);

  texture = gsk_renderer_render_texture (renderer, node, NULL);
  gsk_render_node_unref (node);
  gsk_renderer_unrealize (renderer);
  g_object_unref (renderer);

  return texture;
}

static void
export_image_saved_cb (GObject      *source,
                       GAsyncResult *result,
                       void         *user_data)
{
  GError *error = NULL;

  if (!g_file_replace_contents_finish (G_FILE (source), result, NULL, &error))
    {
      GtkAlertDialog *alert;

      alert = gtk_alert_dialog_new ("Exporting to image failed");
      gtk_alert_dialog_set_detail (alert, error->message);
      gtk_alert_dialog_show (alert, NULL);
      g_object_unref (alert);
      g_clear_error (&error);
    }
}

static void
export_image_response_cb (GObject      *source,
                          GAsyncResult *result,
                          void         *user_data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  GskRenderNode *node = user_data;
  GFile *file;
  char *uri;
  GBytes *bytes;

  file = gtk_file_dialog_save_finish (dialog, result, NULL);
  if (file == NULL)
    {
      gsk_render_node_unref (node);
      return;
    }

  uri = g_file_get_uri (file);
#ifdef CAIRO_HAS_SVG_SURFACE
  if (g_str_has_suffix (uri, "svg"))
    {
      GError *error = NULL;

      bytes = create_svg (node, &error);
      if (bytes == NULL)
        {
          GtkAlertDialog *alert;

          alert = gtk_alert_dialog_new ("Exporting to image failed");
          gtk_alert_dialog_set_detail (alert, error->message);
          gtk_alert_dialog_show (alert, NULL);
          g_object_unref (alert);
          g_clear_error (&error);
        }
    }
  else
#endif
    {
      GdkTexture *texture;
      GskRenderer *renderer;

      renderer = gsk_gl_renderer_new ();
      if (!gsk_renderer_realize_for_display (renderer, gdk_display_get_default (), NULL))
        {
          g_object_unref (renderer);
          renderer = gsk_cairo_renderer_new ();
          if (!gsk_renderer_realize_for_display (renderer, gdk_display_get_default (), NULL))
            {
              g_assert_not_reached ();
            }
        }
      texture = gsk_renderer_render_texture (renderer, node, NULL);
      gsk_renderer_unrealize (renderer);
      g_object_unref (renderer);

      if (g_str_has_suffix (uri, "tiff"))
        bytes = gdk_texture_save_to_tiff_bytes (texture);
      else
        bytes = gdk_texture_save_to_png_bytes (texture);
      g_object_unref (texture);
    }
  g_free (uri);

  if (bytes)
    {
      g_file_replace_contents_bytes_async (file,
                                           bytes,
                                           NULL,
                                           FALSE,
                                           0,
                                           NULL,
                                           export_image_saved_cb,
                                           NULL);
      g_bytes_unref (bytes);
    }
  gsk_render_node_unref (node);
  g_object_unref (file);
}

static void
export_image_cb (GtkWidget        *button,
                 NodeEditorWindow *self)
{
  GskRenderNode *node;
  GtkFileDialog *dialog;
  GtkFileFilter *filter;
  GListStore *filters;

  node = create_node (self);
  if (node == NULL)
    return;

  filters = g_list_store_new (GTK_TYPE_FILE_FILTER);
  filter = gtk_file_filter_new ();
  gtk_file_filter_add_mime_type (filter, "image/png");
  g_list_store_append (filters, filter);
  g_object_unref (filter);
  filter = gtk_file_filter_new ();
  gtk_file_filter_add_mime_type (filter, "image/svg+xml");
  g_list_store_append (filters, filter);
  g_object_unref (filter);
  filter = gtk_file_filter_new ();
  gtk_file_filter_add_mime_type (filter, "image/tiff");
  g_list_store_append (filters, filter);
  g_object_unref (filter);

  dialog = gtk_file_dialog_new ();
  gtk_file_dialog_set_title (dialog, "");
  gtk_file_dialog_set_initial_name (dialog, "example.png");
  gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters));
  gtk_file_dialog_save (dialog,
                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
                        NULL,
                        export_image_response_cb, node);
  g_object_unref (filters);
  g_object_unref (dialog);
}


static void
clip_image_cb (GtkWidget        *button,
               NodeEditorWindow *self)
{
  GdkTexture *texture;
  GdkClipboard *clipboard;

  texture = create_texture (self);
  if (texture == NULL)
    return;

  clipboard = gtk_widget_get_clipboard (GTK_WIDGET (self));

  gdk_clipboard_set_texture (clipboard, texture);

  g_object_unref (texture);
}

static void
testcase_name_entry_changed_cb (GtkWidget        *button,
                                GParamSpec       *pspec,
                                NodeEditorWindow *self)

{
  const char *text = gtk_editable_get_text (GTK_EDITABLE (self->testcase_name_entry));

  if (strlen (text) > 0)
    gtk_widget_set_sensitive (self->testcase_save_button, TRUE);
  else
    gtk_widget_set_sensitive (self->testcase_save_button, FALSE);
}

/* Returns the location where gsk test cases are stored in
 * the GTK testsuite, if we can determine it.
 *
 * When running node editor outside of a GTK build, you can
 * set GTK_SOURCE_DIR to point it at the checkout.
 */
static char *
get_source_dir (void)
{
  const char *subdir = "testsuite/gsk/compare";
  const char *source_dir;
  char *current_dir;
  char *dir;

  source_dir = g_getenv ("GTK_SOURCE_DIR");
  current_dir = g_get_current_dir ();

  if (source_dir)
    {
      char *abs_source_dir = g_canonicalize_filename (source_dir, NULL);
      dir = g_canonicalize_filename (subdir, abs_source_dir);
      g_free (abs_source_dir);
    }
  else
    {
      dir = g_canonicalize_filename (subdir, current_dir);
    }

  if (g_file_test (dir, G_FILE_TEST_EXISTS))
    {
      g_free (current_dir);

      return dir;
    }

  g_free (dir);

  return current_dir;
}

static void
testcase_save_clicked_cb (GtkWidget        *button,
                          NodeEditorWindow *self)
{
  const char *testcase_name = gtk_editable_get_text (GTK_EDITABLE (self->testcase_name_entry));
  char *source_dir = get_source_dir ();
  char *node_file_name;
  char *node_file;
  char *png_file_name;
  char *png_file;
  char *text = NULL;
  GdkTexture *texture;
  GError *error = NULL;

  node_file_name = g_strconcat (testcase_name, ".node", NULL);
  node_file = g_build_filename (source_dir, node_file_name, NULL);
  g_free (node_file_name);

  g_debug ("Saving testcase in %s", node_file);

  png_file_name = g_strconcat (testcase_name, ".png", NULL);
  png_file = g_build_filename (source_dir, png_file_name, NULL);
  g_free (png_file_name);

  if (gtk_check_button_get_active (GTK_CHECK_BUTTON (self->testcase_cairo_checkbutton)))
    texture = create_cairo_texture (self);
  else
    texture = create_texture (self);

  if (!gdk_texture_save_to_png (texture, png_file))
    {
      gtk_label_set_label (GTK_LABEL (self->testcase_error_label),
                           "Could not save texture file");
      goto out;
    }

  text = get_current_text (self->text_buffer);
  {
    GBytes *bytes;
    GskRenderNode *node;
    gsize size;

    bytes = g_bytes_new_take (text, strlen (text) + 1);
    node = gsk_render_node_deserialize (bytes, NULL, NULL);
    g_bytes_unref (bytes);
    bytes = gsk_render_node_serialize (node);
    gsk_render_node_unref (node);
    text = g_bytes_unref_to_data (bytes, &size);
  }

  if (!g_file_set_contents (node_file, text, -1, &error))
    {
      gtk_label_set_label (GTK_LABEL (self->testcase_error_label), error->message);
      /* TODO: Remove texture file again? */
      goto out;
    }

  gtk_editable_set_text (GTK_EDITABLE (self->testcase_name_entry), "");
  gtk_popover_popdown (GTK_POPOVER (self->testcase_popover));

out:
  g_clear_error (&error);
  g_free (text);
  g_free (png_file);
  g_free (node_file);
  g_free (source_dir);
}

static void
set_dark_mode (NodeEditorWindow *self,
               gboolean          dark_mode)
{
  GtkSettings *settings;

  if (self->dark_mode == dark_mode)
    return;

  self->dark_mode = dark_mode;

  settings = gtk_widget_get_settings (GTK_WIDGET (self));

  if (dark_mode)
    {
      GtkInterfaceColorScheme color_scheme;

      g_object_get (settings, "gtk-interface-color-scheme", &color_scheme, NULL);
      if (color_scheme == GTK_INTERFACE_COLOR_SCHEME_DARK)
        color_scheme = GTK_INTERFACE_COLOR_SCHEME_LIGHT;
      else
        color_scheme = GTK_INTERFACE_COLOR_SCHEME_DARK;
      g_object_set (settings, "gtk-interface-color-scheme", color_scheme, NULL);
    }
  else
    gtk_settings_reset_property (settings, "gtk-interface-color-scheme");

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

static void
node_editor_window_dispose (GObject *object)
{
  gtk_widget_dispose_template (GTK_WIDGET (object), NODE_EDITOR_WINDOW_TYPE);

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

static void
node_editor_window_finalize (GObject *object)
{
  NodeEditorWindow *self = (NodeEditorWindow *)object;

  g_array_free (self->errors, TRUE);

  g_clear_pointer (&self->node, gsk_render_node_unref);
  g_clear_object (&self->renderers);
  g_clear_object (&self->file_monitor);
  g_clear_object (&self->file);

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

static void
node_editor_window_add_renderer (NodeEditorWindow *self,
                                 GskRenderer      *renderer,
                                 const char       *description)
{
  GdkPaintable *paintable;
  GdkDisplay *display;

  display = gtk_widget_get_display (GTK_WIDGET (self));

  if (!gsk_renderer_realize_for_display (renderer, display, NULL))
    {
      GdkSurface *surface = gtk_native_get_surface (GTK_NATIVE (self));
      g_assert (surface != NULL);

      if (!gsk_renderer_realize (renderer, surface, NULL))
        {
          g_object_unref (renderer);
          return;
        }
    }

  paintable = gtk_renderer_paintable_new (renderer, gtk_picture_get_paintable (GTK_PICTURE (self->picture)));
  g_object_set_data_full (G_OBJECT (paintable), "description", g_strdup (description), g_free);
  g_clear_object (&renderer);

  g_list_store_append (self->renderers, paintable);
  g_object_unref (paintable);
}

static void
update_paste_action (GdkClipboard *clipboard,
                     GParamSpec   *pspec,
                     gpointer      data)
{
  GtkWidget *widget = GTK_WIDGET (data);
  gboolean has_node;

  has_node = gdk_content_formats_contain_mime_type (gdk_clipboard_get_formats (clipboard), "application/x-gtk-render-node");

  gtk_widget_action_set_enabled (widget, "paste-node", has_node);
}

static void
node_editor_window_realize (GtkWidget *widget)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
  GdkFrameClock *frameclock;

  GTK_WIDGET_CLASS (node_editor_window_parent_class)->realize (widget);

#if 0
  node_editor_window_add_renderer (self,
                                   NULL,
                                   "Default");
#endif
  node_editor_window_add_renderer (self,
                                   gsk_gl_renderer_new (),
                                   "OpenGL");
  node_editor_window_add_renderer (self,
                                   gsk_vulkan_renderer_new (),
                                   "Vulkan");
#ifdef GDK_WINDOWING_BROADWAY
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
  node_editor_window_add_renderer (self,
                                   gsk_broadway_renderer_new (),
                                   "Broadway");
G_GNUC_END_IGNORE_DEPRECATIONS
#endif
  node_editor_window_add_renderer (self,
                                   gsk_cairo_renderer_new (),
                                   "Cairo");

  frameclock = gtk_widget_get_frame_clock (widget);
  self->after_paint_handler = g_signal_connect (frameclock, "after-paint",
                                                G_CALLBACK (after_paint), self);

  g_signal_connect (gtk_widget_get_clipboard (widget), "notify::formats", G_CALLBACK (update_paste_action), widget);
}

static void
node_editor_window_unrealize (GtkWidget *widget)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
  GdkFrameClock *frameclock;
  guint i;

  g_signal_handlers_disconnect_by_func (gtk_widget_get_clipboard (widget), update_paste_action, widget);

  frameclock = gtk_widget_get_frame_clock (widget);
  g_clear_signal_handler (&self->after_paint_handler, frameclock);

  for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->renderers)); i ++)
    {
      gpointer item = g_list_model_get_item (G_LIST_MODEL (self->renderers), i);
      gsk_renderer_unrealize (gtk_renderer_paintable_get_renderer (item));
      g_object_unref (item);
    }

  g_list_store_remove_all (self->renderers);

  GTK_WIDGET_CLASS (node_editor_window_parent_class)->unrealize (widget);
}

typedef struct
{
  NodeEditorWindow *self;
  GtkTextIter start, end;
} Selection;

static void
color_cb (GObject *source,
          GAsyncResult *result,
          gpointer data)
{
  GtkColorDialog *dialog = GTK_COLOR_DIALOG (source);
  Selection *selection = data;
  NodeEditorWindow *self = selection->self;
  GdkRGBA *color;
  char *text;
  GError *error = NULL;
  GtkTextBuffer *buffer;

  color = gtk_color_dialog_choose_rgba_finish (dialog, result, &error);
  if (!color)
    {
      g_print ("%s\n", error->message);
      g_error_free (error);
      g_free (selection);
      return;
    }

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));

  text = gdk_rgba_to_string (color);
  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
  gtk_text_buffer_insert (buffer, &selection->start, text, -1);

  g_free (text);
  gdk_rgba_free (color);
  g_free (selection);
}

static void
font_cb (GObject *source,
          GAsyncResult *result,
          gpointer data)
{
  GtkFontDialog *dialog = GTK_FONT_DIALOG (source);
  Selection *selection = data;
  NodeEditorWindow *self = selection->self;
  GError *error = NULL;
  PangoFontDescription *desc;
  GtkTextBuffer *buffer;
  char *text;

  desc = gtk_font_dialog_choose_font_finish (dialog, result, &error);
  if (!desc)
    {
      g_print ("%s\n", error->message);
      g_error_free (error);
      g_free (selection);
      return;
    }

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));

  text = pango_font_description_to_string (desc);
  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
  gtk_text_buffer_insert (buffer, &selection->start, text, -1);

  g_free (text);
  pango_font_description_free (desc);
  g_free (selection);
}

static void
file_cb (GObject *source,
          GAsyncResult *result,
          gpointer data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  Selection *selection = data;
  NodeEditorWindow *self = selection->self;
  GError *error = NULL;
  GFile *file;
  GtkTextBuffer *buffer;
  char *text;

  file = gtk_file_dialog_open_finish (dialog, result, &error);
  if (!file)
    {
      g_print ("%s\n", error->message);
      g_error_free (error);
      g_free (selection);
      return;
    }

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));

  text = g_file_get_uri (file);
  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
  gtk_text_buffer_insert (buffer, &selection->start, text, -1);

  g_free (text);
  g_object_unref (file);
  g_free (selection);
}

static void
key_pressed (GtkEventControllerKey *controller,
             unsigned int keyval,
             unsigned int keycode,
             GdkModifierType state,
             gpointer data)
{
  GtkWidget *dd = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (controller));
  Selection *selection = data;
  NodeEditorWindow *self = selection->self;
  unsigned int selected;
  GtkStringList *strings;
  GtkTextBuffer *buffer;
  const char *text;

  if (keyval != GDK_KEY_Escape)
    return;

  strings = GTK_STRING_LIST (gtk_drop_down_get_model (GTK_DROP_DOWN (dd)));
  selected = gtk_drop_down_get_selected (GTK_DROP_DOWN (dd));
  text = gtk_string_list_get_string (strings, selected);

  gtk_text_view_remove (GTK_TEXT_VIEW (self->text_view), dd);

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
  gtk_text_iter_backward_search (&selection->start, "mode:", 0, NULL, &selection->start, NULL);
  gtk_text_iter_forward_search (&selection->start, ";", 0, &selection->end, NULL, NULL);
  gtk_text_buffer_delete (buffer, &selection->start, &selection->end);
  gtk_text_buffer_insert (buffer, &selection->start, " ", -1);
  gtk_text_buffer_insert (buffer, &selection->start, text, -1);
}

static void
node_editor_window_edit (NodeEditorWindow *self,
                         GtkTextIter      *iter)
{
  GtkTextIter start, end;
  GtkTextBuffer *buffer;
  Selection *selection;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));

  gtk_text_iter_set_line_offset (iter, 0);

  if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
      gtk_text_iter_forward_search (iter, "color:", 0, NULL, &start, &end))
    {
      GtkColorDialog *dialog;
      GdkRGBA color;
      char *text;

      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
        gtk_text_iter_forward_char (&start);

      gtk_text_buffer_select_range (buffer, &start, &end);
      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
      gdk_rgba_parse (&color, text);
      g_free (text);

      selection = g_new0 (Selection, 1);
      selection->self = self;
      selection->start = start;
      selection->end = end;

      dialog = gtk_color_dialog_new ();
      gtk_color_dialog_choose_rgba (dialog, GTK_WINDOW (self), &color, NULL, color_cb, selection);
    }
  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
           gtk_text_iter_forward_search (iter, "font:", 0, NULL, &start, &end))
    {
      GtkFontDialog *dialog;
      PangoFontDescription *desc;
      char *text;

      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
        gtk_text_iter_forward_char (&start);

      /* Skip the quotes */
      gtk_text_iter_forward_char (&start);
      gtk_text_iter_backward_char (&end);

      gtk_text_buffer_select_range (buffer, &start, &end);

      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
      desc = pango_font_description_from_string (text);
      g_free (text);

      selection = g_new0 (Selection, 1);
      selection->self = self;
      selection->start = start;
      selection->end = end;

      dialog = gtk_font_dialog_new ();
      gtk_font_dialog_choose_font (dialog, GTK_WINDOW (self), desc, NULL, font_cb, selection);
      pango_font_description_free (desc);
    }
  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
           gtk_text_iter_forward_search (iter, "mode:", 0, NULL, &start, &end))
    {
      /* Assume we have a blend node, for now */
      GEnumClass *class;
      GtkStringList *strings;
      GtkWidget *dd;
      GtkTextChildAnchor *anchor;
      unsigned int selected = 0;
      GtkEventController *key_controller;
      gboolean is_blend_mode = FALSE;
      char *text;

      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
        gtk_text_iter_forward_char (&start);

      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);

      strings = gtk_string_list_new (NULL);
      class = g_type_class_ref (GSK_TYPE_BLEND_MODE);
      for (unsigned int i = 0; i < class->n_values; i++)
        {
          if (strcmp (class->values[i].value_nick, text) == 0)
            is_blend_mode = TRUE;
        }
      g_type_class_unref (class);

      if (is_blend_mode)
        class = g_type_class_ref (GSK_TYPE_BLEND_MODE);
      else
        class = g_type_class_ref (GSK_TYPE_MASK_MODE);

       for (unsigned int i = 0; i < class->n_values; i++)
         {
           if (i == 0 && is_blend_mode)
             gtk_string_list_append (strings, "normal");
           else
             gtk_string_list_append (strings, class->values[i].value_nick);

           if (strcmp (class->values[i].value_nick, text) == 0)
             selected = i;
         }
      g_type_class_unref (class);

      gtk_text_buffer_delete (buffer, &start, &end);

      anchor = gtk_text_buffer_create_child_anchor (buffer, &start);
      dd = gtk_drop_down_new (G_LIST_MODEL (strings), NULL);
      gtk_drop_down_set_selected (GTK_DROP_DOWN (dd), selected);
      gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (self->text_view), dd, anchor);

      selection = g_new0 (Selection, 1);
      selection->self = self;
      selection->start = start;
      selection->end = end;

      key_controller = gtk_event_controller_key_new ();
      g_signal_connect (key_controller, "key-pressed", G_CALLBACK (key_pressed), selection);
      gtk_widget_add_controller (dd, key_controller);
    }
  else if (gtk_text_iter_forward_search (iter, ";", 0, &end, NULL, NULL) &&
           gtk_text_iter_forward_search (iter, "texture:", 0, NULL, &start, &end))
    {
      GtkFileDialog *dialog;
      GtkTextIter skip;
      char *text;
      GFile *file;

      while (g_unichar_isspace (gtk_text_iter_get_char (&start)))
        gtk_text_iter_forward_char (&start);

      skip = start;
      gtk_text_iter_forward_chars (&skip, strlen ("url(\""));
      text = gtk_text_iter_get_text (&start, &skip);
      if (strcmp (text, "url(\"") != 0)
        {
          g_free (text);
          return;
        }
      g_free (text);
      start = skip;

      skip = end;
      gtk_text_iter_backward_chars (&skip, strlen ("\")"));
      text = gtk_text_iter_get_text (&skip, &end);
      if (strcmp (text, "\")") != 0)
        {
          g_free (text);
          return;
        }
      g_free (text);
      end = skip;

      gtk_text_buffer_select_range (buffer, &start, &end);

      text = gtk_text_buffer_get_text (buffer, &start, &end, TRUE);
      file = g_file_new_for_uri (text);
      g_free (text);

      selection = g_new0 (Selection, 1);
      selection->self = self;
      selection->start = start;
      selection->end = end;

      dialog = gtk_file_dialog_new ();
      gtk_file_dialog_set_initial_file (dialog, file);
      gtk_file_dialog_open (dialog, GTK_WINDOW (self), NULL, file_cb, selection);
      g_object_unref (file);
    }
}

static void
click_gesture_pressed (GtkGestureClick  *gesture,
                       int               n_press,
                       double            x,
                       double            y,
                       NodeEditorWindow *self)
{
  GtkTextIter iter;
  int bx, by, trailing;
  GdkModifierType state;

  state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (gesture));
  if ((state & GDK_CONTROL_MASK) == 0)
    return;

  gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (self->text_view), GTK_TEXT_WINDOW_TEXT, x, y, &bx, &by);
  gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (self->text_view), &iter, &trailing, bx, by);

  node_editor_window_edit (self, &iter);
}

static void
edit_action_cb (GtkWidget  *widget,
                const char *action_name,
                GVariant   *parameter)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);
  GtkTextBuffer *buffer;
  GtkTextIter start, end;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
  gtk_text_buffer_get_selection_bounds (buffer, &start, &end);

  node_editor_window_edit (self, &start);
}

static void
text_received (GObject      *source,
               GAsyncResult *result,
               gpointer      data)
{
  GdkClipboard *clipboard = GDK_CLIPBOARD (source);
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (data);
  char *text;

  text = gdk_clipboard_read_text_finish (clipboard, result, NULL);
  if (text)
    {
      GtkTextBuffer *buffer;
      GtkTextIter start, end;

      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->text_view));
      gtk_text_buffer_begin_user_action (buffer);
      gtk_text_buffer_get_bounds (buffer, &start, &end);
      gtk_text_buffer_delete (buffer, &start, &end);
      gtk_text_buffer_insert (buffer, &start, text, -1);
      gtk_text_buffer_end_user_action (buffer);
      g_free (text);
    }
}

static void
paste_node_cb (GtkWidget  *widget,
               const char *action_name,
               GVariant   *parameter)
{
  GdkClipboard *clipboard = gtk_widget_get_clipboard (widget);

  gdk_clipboard_read_text_async (clipboard, NULL, text_received, widget);
}

static void set_zoom_level (NodeEditorWindow *self,
                            int               zoom_level);

static void
node_editor_window_set_property (GObject      *object,
                                 guint         prop_id,
                                 const GValue *value,
                                 GParamSpec   *pspec)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (object);

  switch (prop_id)
    {
    case PROP_AUTO_RELOAD:
      {
        gboolean auto_reload = g_value_get_boolean (value);
        if (self->auto_reload != auto_reload)
          {
            self->auto_reload = auto_reload;

            if (self->auto_reload)
              reload (self);
          }
      }
      break;

    case PROP_ZOOM_LEVEL:
      set_zoom_level (self, g_value_get_int (value));
      break;

    case PROP_DARK_MODE:
      set_dark_mode (self, g_value_get_boolean (value));
      break;

    case PROP_PANED_POSITION:
      self->paned_position = g_value_get_int (value);
      break;

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

static void
node_editor_window_get_property (GObject    *object,
                                 guint       prop_id,
                                 GValue     *value,
                                 GParamSpec *pspec)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (object);

  switch (prop_id)
    {
    case PROP_AUTO_RELOAD:
      g_value_set_boolean (value, self->auto_reload);
      break;

    case PROP_ZOOM_LEVEL:
      g_value_set_int (value, self->zoom_level);
      break;

    case PROP_DARK_MODE:
      g_value_set_boolean (value, self->dark_mode);
      break;

    case PROP_PANED_POSITION:
      g_value_set_int (value, self->paned_position);
      break;

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

static void
close_crash_warning (GtkButton        *button,
                     NodeEditorWindow *self)
{
  gtk_revealer_set_reveal_child (GTK_REVEALER (self->crash_warning), FALSE);
}

static void
update_zoom_buttons (NodeEditorWindow *self)
{
  GString *s = g_string_new ("");

  gtk_widget_set_sensitive (self->zoom_in, self->zoom_level < G_N_ELEMENTS (zoom_levels) - 1);
  gtk_widget_set_sensitive (self->zoom_out, self->zoom_level > 0);
  g_string_append_printf (s, "%3d%%", (guint) round (100 * zoom_levels[self->zoom_level]));
  g_string_replace (s, " ", " ", 0); /* Replace ASCII space by U+2007 figure space */
  gtk_label_set_text (GTK_LABEL (self->zoom_label), s->str);
  g_string_free (s, TRUE);
}

static void
set_zoom_level (NodeEditorWindow *self,
                int               zoom_level)
{
  zoom_level = CLAMP (zoom_level, 0, G_N_ELEMENTS (zoom_levels) - 1);

  if (self->zoom_level == zoom_level)
    return;

  self->zoom_level = zoom_level;
  update_zoom_buttons (self);
  text_changed (self->text_buffer, self);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ZOOM_LEVEL]);
}

static void
zoom_in_cb (GtkButton        *button,
            NodeEditorWindow *self)
{
  set_zoom_level (self, self->zoom_level + 1);
}

static void
zoom_out_cb (GtkButton        *button,
             NodeEditorWindow *self)
{
  set_zoom_level (self, self->zoom_level - 1);
}

static gboolean
node_editor_window_save_state (GtkApplicationWindow *win,
                               GVariantDict         *state)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (win);

  g_variant_dict_insert (state, "zoom-level", "i", self->zoom_level);
  g_variant_dict_insert (state, "auto-reload", "b", self->auto_reload);
  g_variant_dict_insert (state, "dark-mode", "b", self->dark_mode);
  g_variant_dict_insert (state, "paned-position", "i", self->paned_position);

  return TRUE;
}

static void
node_editor_window_class_init (NodeEditorWindowClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
  GtkApplicationWindowClass *appwin_class = GTK_APPLICATION_WINDOW_CLASS (class);

  GtkShortcutTrigger *trigger;
  GtkShortcutAction *action;
  GtkShortcut *shortcut;

  object_class->dispose = node_editor_window_dispose;
  object_class->finalize = node_editor_window_finalize;
  object_class->set_property = node_editor_window_set_property;
  object_class->get_property = node_editor_window_get_property;

  appwin_class->save_state = node_editor_window_save_state;

  gtk_widget_class_set_template_from_resource (widget_class,
                                               "/org/gtk/gtk4/node-editor/node-editor-window.ui");

  widget_class->realize = node_editor_window_realize;
  widget_class->unrealize = node_editor_window_unrealize;

  properties[PROP_AUTO_RELOAD] = g_param_spec_boolean ("auto-reload", NULL, NULL,
                                                       TRUE,
                                                       G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_ZOOM_LEVEL] = g_param_spec_int ("zoom-level", NULL, NULL,
                                                  0, G_N_ELEMENTS (zoom_levels) - 1, 0,
                                                  G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_DARK_MODE] = g_param_spec_boolean ("dark-mode", NULL, NULL,
                                                     FALSE,
                                                     G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  properties[PROP_PANED_POSITION] = g_param_spec_int ("paned-position", NULL, NULL,
                                                      0, G_MAXINT, 0,
                                                      G_PARAM_READWRITE | G_PARAM_STATIC_NAME);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);

  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, text_view);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, picture);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, renderer_listbox);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_popover);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_error_label);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_cairo_checkbutton);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_name_entry);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, testcase_save_button);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, crash_warning);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, zoom_in);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, zoom_out);
  gtk_widget_class_bind_template_child (widget_class, NodeEditorWindow, zoom_label);

  gtk_widget_class_bind_template_callback (widget_class, text_view_query_tooltip_cb);
  gtk_widget_class_bind_template_callback (widget_class, open_cb);
  gtk_widget_class_bind_template_callback (widget_class, save_cb);
  gtk_widget_class_bind_template_callback (widget_class, export_image_cb);
  gtk_widget_class_bind_template_callback (widget_class, clip_image_cb);
  gtk_widget_class_bind_template_callback (widget_class, testcase_save_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, testcase_name_entry_changed_cb);
  gtk_widget_class_bind_template_callback (widget_class, on_picture_drag_prepare_cb);
  gtk_widget_class_bind_template_callback (widget_class, on_picture_drop_cb);
  gtk_widget_class_bind_template_callback (widget_class, click_gesture_pressed);
  gtk_widget_class_bind_template_callback (widget_class, close_crash_warning);
  gtk_widget_class_bind_template_callback (widget_class, zoom_in_cb);
  gtk_widget_class_bind_template_callback (widget_class, zoom_out_cb);

  gtk_widget_class_install_action (widget_class, "smart-edit", NULL, edit_action_cb);

  trigger = gtk_keyval_trigger_new (GDK_KEY_e, GDK_CONTROL_MASK);
  action = gtk_named_action_new ("smart-edit");
  shortcut = gtk_shortcut_new (trigger, action);
  gtk_widget_class_add_shortcut (widget_class, shortcut);

  gtk_widget_class_install_action (widget_class, "paste-node", NULL, paste_node_cb);

  trigger = gtk_keyval_trigger_new (GDK_KEY_v, GDK_CONTROL_MASK | GDK_SHIFT_MASK);
  action = gtk_named_action_new ("paste-node");
  shortcut = gtk_shortcut_new (trigger, action);
  gtk_widget_class_add_shortcut (widget_class, shortcut);
}

static GtkWidget *
node_editor_window_create_renderer_widget (gpointer item,
                                           gpointer user_data)
{
  GdkPaintable *paintable = item;
  GtkWidget *box, *label, *picture;
  GtkWidget *row;

  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
  gtk_widget_set_size_request (box, 120, 90);

  label = gtk_label_new (g_object_get_data (G_OBJECT (paintable), "description"));
  gtk_widget_add_css_class (label, "title-4");
  gtk_box_append (GTK_BOX (box), label);

  picture = gtk_picture_new_for_paintable (paintable);
  /* don't ever scale up, we want to be as accurate as possible */
  gtk_widget_set_halign (picture, GTK_ALIGN_CENTER);
  gtk_widget_set_valign (picture, GTK_ALIGN_CENTER);
  gtk_box_append (GTK_BOX (box), picture);

  row = gtk_list_box_row_new ();
  gtk_list_box_row_set_child (GTK_LIST_BOX_ROW (row), box);
  gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);

  return row;
}

static void
window_open (GSimpleAction *action,
             GVariant      *parameter,
             gpointer       user_data)
{
  NodeEditorWindow *self = user_data;

  show_open_filechooser (self);
}

static GActionEntry win_entries[] = {
  { "open", window_open, NULL, NULL, NULL },
};

char *
get_autosave_path (const char *suffix)
{
  char *path;
  char *name;

  name = g_strconcat ("autosave", suffix, NULL);
  path = g_build_filename (g_get_user_cache_dir (), "gtk4-node-editor", name, NULL);
  g_free (name);

  return path;
}

static void
set_initial_text (NodeEditorWindow *self)
{
  char *path, *path1;
  char *initial_text;
  gsize len;

  path = get_autosave_path (NULL);
  path1 = get_autosave_path ("-unsafe");

  if (g_file_get_contents (path1, &initial_text, &len, NULL))
    {
      self->auto_reload = FALSE;
      gtk_revealer_set_reveal_child (GTK_REVEALER (self->crash_warning), TRUE);

      gtk_text_buffer_set_text (self->text_buffer, initial_text, len);
      g_free (initial_text);
    }
  else if (g_file_get_contents (path, &initial_text, &len, NULL))
    {
      gtk_text_buffer_set_text (self->text_buffer, initial_text, len);
      g_free (initial_text);
    }
  else
    {
      /* Default */
      gtk_text_buffer_set_text (self->text_buffer,
         "shadow {\n"
         "  child: texture {\n"
         "    bounds: 0 0 128 128;\n"
         "    texture: url(\"resource:///org/gtk/gtk4/node-editor/icons/apps/org.gtk.gtk4.NodeEditor.svg\");\n"
         "  }\n"
         "  shadows: rgba(0,0,0,0.5) 0 1 12;\n"
         "}\n"
         "\n"
         "transform {\n"
         "  child: text {\n"
         "    color: rgb(46,52,54);\n"
         "    font: \"Sans Bold 14.6px\";\n"
         "    glyphs: \"GTK Node Editor\";\n"
         "    offset: 8 14.418;\n"
         "  }\n"
         "  transform: translate(0, 140);\n"
         "}", -1);
    }

  g_free (path);
  g_free (path1);
}

static void
autosave_contents (NodeEditorWindow *self)
{
  char *path = NULL;
  char *dir = NULL;
  char *contents;
  GtkTextIter start, end;

  gtk_text_buffer_get_bounds (self->text_buffer, &start, &end);
  contents = gtk_text_buffer_get_text (self->text_buffer, &start, &end, TRUE);
  path = get_autosave_path ("-unsafe");
  dir = g_path_get_dirname (path);
  g_mkdir_with_parents (dir, 0755);
  g_file_set_contents (path, contents, -1, NULL);

  g_free (dir);
  g_free (path);
  g_free (contents);
}

static void
node_editor_window_init (NodeEditorWindow *self)
{
  GAction *action;
  gsize i;

  gtk_widget_init_template (GTK_WIDGET (self));

  for (i = 0; i < G_N_ELEMENTS (zoom_levels); i++)
    {
      if (zoom_levels[i] == 1.0)
        {
          self->zoom_level = i;
          break;
        }
    }
  self->auto_reload = TRUE;

  self->renderers = g_list_store_new (GDK_TYPE_PAINTABLE);
  gtk_list_box_bind_model (GTK_LIST_BOX (self->renderer_listbox),
                           G_LIST_MODEL (self->renderers),
                           node_editor_window_create_renderer_widget,
                           self,
                           NULL);

  self->errors = g_array_new (FALSE, TRUE, sizeof (TextViewError));
  g_array_set_clear_func (self->errors, (GDestroyNotify)text_view_error_free);

  g_action_map_add_action_entries (G_ACTION_MAP (self), win_entries, G_N_ELEMENTS (win_entries), self);

  action = G_ACTION (g_property_action_new ("auto-reload", self, "auto-reload"));
  g_action_map_add_action (G_ACTION_MAP (self), action);
  g_object_unref (action);

  self->tag_table = gtk_text_tag_table_new ();
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "error",
                                        "underline", PANGO_UNDERLINE_ERROR,
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "comment",
                                        "foreground-rgba", &(GdkRGBA) { 0.8, 0.52, 0.5, 1},
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "nodename",
                                        "foreground-rgba", &(GdkRGBA) { 0.9, 0.78, 0.53, 1},
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "propname",
                                        "foreground-rgba", &(GdkRGBA) { 0.7, 0.55, 0.67, 1},
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "string",
                                        "foreground-rgba", &(GdkRGBA) { 0.63, 0.73, 0.54, 1},
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "number",
                                        "foreground-rgba", &(GdkRGBA) { 0.8, 0.52, 0.43, 1},
                                        NULL));
  gtk_text_tag_table_add (self->tag_table,
                          g_object_new (GTK_TYPE_TEXT_TAG,
                                        "name", "no-hyphens",
                                        "insert-hyphens", FALSE,
                                        NULL));

  self->text_buffer = gtk_text_buffer_new (self->tag_table);
  g_signal_connect (self->text_buffer, "changed", G_CALLBACK (text_changed), self);
  gtk_text_view_set_buffer (GTK_TEXT_VIEW (self->text_view), self->text_buffer);

  set_initial_text (self);

  g_signal_connect_swapped (self->text_buffer, "changed", G_CALLBACK (autosave_contents), self);

  if (g_getenv ("GSK_RENDERER"))
    {
      char *new_title = g_strdup_printf ("GTK Node Editor - %s", g_getenv ("GSK_RENDERER"));
      gtk_window_set_title (GTK_WINDOW (self), new_title);
      g_free (new_title);
    }
}

NodeEditorWindow *
node_editor_window_new (NodeEditorApplication *application)
{
  return g_object_new (NODE_EDITOR_WINDOW_TYPE,
                       "application", application,
                       NULL);
}
