/*
 * 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"
#include "gsk/gl/gskglrenderer.h"
#ifdef GDK_WINDOWING_BROADWAY
#include "gsk/broadway/gskbroadwayrenderer.h"
#endif
#ifdef GDK_RENDERING_VULKAN
#include "gsk/vulkan/gskvulkanrenderer.h"
#endif

#ifndef NODE_EDITOR_SOURCE_DIR
#define NODE_EDITOR_SOURCE_DIR "." /* Fallback */
#endif

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 *renderer_listbox;
  GListStore *renderers;
  GdkPaintable *paintable;

  GFileMonitor *file_monitor;

  GArray *errors;
};

struct _NodeEditorWindowClass
{
  GtkApplicationWindowClass parent_class;
};

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 GtkCssSection *section,
                        const GError        *error,
                        gpointer             user_data)
{
  const GtkCssLocation *start_location = gtk_css_section_get_start_location (section);
  const GtkCssLocation *end_location = gtk_css_section_get_end_location (section);
  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
text_changed (GtkTextBuffer    *buffer,
              NodeEditorWindow *self)
{
  GskRenderNode *node;
  char *text;
  GBytes *bytes;

  g_array_remove_range (self->errors, 0, self->errors->len);
  text = get_current_text (self->text_buffer);
  text_buffer_remove_all_tags (self->text_buffer);
  bytes = g_bytes_new_take (text, strlen (text));

  /* If this is too slow, go fix the parser performance */
  node = gsk_render_node_deserialize (bytes, deserialize_error_func, self);
  g_bytes_unref (bytes);
  if (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 (node, &bounds);
      gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (- bounds.origin.x, - bounds.origin.y));
      gtk_snapshot_append_node (snapshot, node);
      gsk_render_node_unref (node);
      paintable = gtk_snapshot_free_to_paintable (snapshot, &bounds.size);
      gtk_picture_set_paintable (GTK_PICTURE (self->picture), paintable);
      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);
    }

  GtkTextIter iter;

  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 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);
    }
}

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_file_contents (NodeEditorWindow *self,
                    GFile            *file)
{
  GBytes *bytes;

  bytes = g_file_load_bytes (file, NULL, NULL, NULL);
  if (bytes == NULL)
    return FALSE;

  if (!g_utf8_validate (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL))
    {
      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 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;

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

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


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

  return TRUE;
}

static void
open_response_cb (GtkWidget        *dialog,
                  int               response,
                  NodeEditorWindow *self)
{
  gtk_widget_hide (dialog);

  if (response == GTK_RESPONSE_ACCEPT)
    {
      GFile *file;

      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
      node_editor_window_load (self, file);
      g_object_unref (file);
    }

  gtk_window_destroy (GTK_WINDOW (dialog));
}

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

  dialog = gtk_file_chooser_dialog_new ("Open node file",
                                        GTK_WINDOW (self),
                                        GTK_FILE_CHOOSER_ACTION_OPEN,
                                        "_Cancel", GTK_RESPONSE_CANCEL,
                                        "_Load", GTK_RESPONSE_ACCEPT,
                                        NULL);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);

  GFile *cwd = g_file_new_for_path (".");
  gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd, NULL);
  g_object_unref (cwd);

  g_signal_connect (dialog, "response", G_CALLBACK (open_response_cb), self);
  gtk_widget_show (dialog);
}

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

static void
save_response_cb (GtkWidget        *dialog,
                  int               response,
                  NodeEditorWindow *self)
{
  gtk_widget_hide (dialog);

  if (response == GTK_RESPONSE_ACCEPT)
    {
      GFile *file;
      char *text;
      GError *error = NULL;

      text = get_current_text (self->text_buffer);

      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
      g_file_replace_contents (file, text, -1,
                               NULL, FALSE,
                               G_FILE_CREATE_NONE,
                               NULL,
                               NULL,
                               &error);
      if (error != NULL)
        {
          GtkWidget *message_dialog;

          message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (self))),
                                                   GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
                                                   GTK_MESSAGE_INFO,
                                                   GTK_BUTTONS_OK,
                                                   "Saving failed");
          gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message_dialog),
                                                    "%s", error->message);
          g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
          gtk_widget_show (message_dialog);
          g_error_free (error);
        }

      g_free (text);
      g_object_unref (file);
    }

  gtk_window_destroy (GTK_WINDOW (dialog));
}

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

  dialog = gtk_file_chooser_dialog_new ("Save node",
                                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
                                        GTK_FILE_CHOOSER_ACTION_SAVE,
                                        "_Cancel", GTK_RESPONSE_CANCEL,
                                        "_Save", GTK_RESPONSE_ACCEPT,
                                        NULL);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);

  GFile *cwd = g_file_new_for_path (".");
  gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), cwd, NULL);
  g_object_unref (cwd);

  g_signal_connect (dialog, "response", G_CALLBACK (save_response_cb), self);
  gtk_widget_show (dialog);
}

static GdkTexture *
create_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 = 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;
}

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

  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;

  surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (self)));
  renderer = gsk_cairo_renderer_new ();
  gsk_renderer_realize (renderer, surface, 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_response_cb (GtkWidget  *dialog,
                          int         response,
                          GdkTexture *texture)
{
  gtk_widget_hide (dialog);

  if (response == GTK_RESPONSE_ACCEPT)
    {
      GFile *file;

      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
      if (!gdk_texture_save_to_png (texture, g_file_peek_path (file)))
        {
          GtkWidget *message_dialog;

          message_dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_window_get_transient_for (GTK_WINDOW (dialog))),
                                                   GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
                                                   GTK_MESSAGE_INFO,
                                                   GTK_BUTTONS_OK,
                                                   "Exporting to image failed");
          g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_window_destroy), NULL);
          gtk_widget_show (message_dialog);
        }

      g_object_unref (file);
    }

  gtk_window_destroy (GTK_WINDOW (dialog));
  g_object_unref (texture);
}

static void
export_image_cb (GtkWidget        *button,
                 NodeEditorWindow *self)
{
  GdkTexture *texture;
  GtkWidget *dialog;

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

  dialog = gtk_file_chooser_dialog_new ("",
                                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (button))),
                                        GTK_FILE_CHOOSER_ACTION_SAVE,
                                        "_Cancel", GTK_RESPONSE_CANCEL,
                                        "_Save", GTK_RESPONSE_ACCEPT,
                                        NULL);

  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
  g_signal_connect (dialog, "response", G_CALLBACK (export_image_response_cb), texture);
  gtk_widget_show (dialog);
}

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);
}

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 = g_canonicalize_filename (NODE_EDITOR_SOURCE_DIR, NULL);
  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);

  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);
  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_free (text);
  g_free (png_file);
  g_free (node_file);
  g_free (source_dir);
}

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

  g_array_free (self->errors, TRUE);

  g_clear_object (&self->renderers);

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

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

  surface = gtk_native_get_surface (GTK_NATIVE (self));
  g_assert (surface != NULL);

  if (renderer != NULL && !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
node_editor_window_realize (GtkWidget *widget)
{
  NodeEditorWindow *self = NODE_EDITOR_WINDOW (widget);

  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");
#ifdef GDK_RENDERING_VULKAN
  node_editor_window_add_renderer (self,
                                   gsk_vulkan_renderer_new (),
                                   "Vulkan");
#endif
#ifdef GDK_WINDOWING_BROADWAY
  node_editor_window_add_renderer (self,
                                   gsk_broadway_renderer_new (),
                                   "Broadway");
#endif
  node_editor_window_add_renderer (self,
                                   gsk_cairo_renderer_new (),
                                   "Cairo");
}

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

  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);
}

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

  object_class->finalize = node_editor_window_finalize;

  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;

  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_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, testcase_save_clicked_cb);
  gtk_widget_class_bind_template_callback (widget_class, testcase_name_entry_changed_cb);
}

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 },
};

static void
node_editor_window_init (NodeEditorWindow *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  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);

  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", "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));

  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);
}

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