/*
 * Copyright (c) 2016 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "recorder.h"

#include <gtk/gtkbox.h>
#include <gtk/gtkfilechooserdialog.h>
#include <gtk/gtklabel.h>
#include <gtk/gtklistbox.h>
#include <gtk/gtkmessagedialog.h>
#include <gtk/gtkpopover.h>
#include <gtk/gtktogglebutton.h>
#include <gtk/gtktreeselection.h>
#include <gtk/gtktreeview.h>
#include <gsk/gskrendererprivate.h>
#include <gsk/gskrendernodeprivate.h>
#include <gsk/gskroundedrectprivate.h>

#include <glib/gi18n-lib.h>
#include <gdk/gdktextureprivate.h>
#include "gtk/gtkdebug.h"

#include "gtktreemodelrendernode.h"
#include "recording.h"
#include "rendernodeview.h"
#include "renderrecording.h"
#include "startrecording.h"

struct _GtkInspectorRecorderPrivate
{
  GListModel *recordings;
  GtkTreeModel *render_node_model;

  GtkWidget *recordings_list;
  GtkWidget *render_node_view;
  GtkWidget *render_node_tree;
  GtkWidget *render_node_save_button;
  GtkWidget *node_property_tree;
  GtkTreeModel *render_node_properties;

  GtkInspectorRecording *recording; /* start recording if recording or NULL if not */

  gboolean debug_nodes;
};

enum {
  COLUMN_NODE_NAME,
  /* add more */
  N_NODE_COLUMNS
};

enum
{
  PROP_0,
  PROP_RECORDING,
  PROP_DEBUG_NODES,
  LAST_PROP
};

static GParamSpec *props[LAST_PROP] = { NULL, };

G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorRecorder, gtk_inspector_recorder, GTK_TYPE_BIN)

static void
recordings_clear_all (GtkButton            *button,
                      GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);

  g_list_store_remove_all (G_LIST_STORE (priv->recordings));
}

static void
recordings_list_row_selected (GtkListBox           *box,
                              GtkListBoxRow        *row,
                              GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  GtkInspectorRecording *recording;

  if (row)
    recording = g_list_model_get_item (priv->recordings, gtk_list_box_row_get_index (row));
  else
    recording = NULL;

  if (GTK_INSPECTOR_IS_RENDER_RECORDING (recording))
    {
      gtk_render_node_view_set_render_node (GTK_RENDER_NODE_VIEW (priv->render_node_view),
                                            gtk_inspector_render_recording_get_node (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      gtk_render_node_view_set_clip_region (GTK_RENDER_NODE_VIEW (priv->render_node_view),
                                            gtk_inspector_render_recording_get_clip_region (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      gtk_render_node_view_set_viewport (GTK_RENDER_NODE_VIEW (priv->render_node_view),
                                         gtk_inspector_render_recording_get_area (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      gtk_tree_model_render_node_set_root_node (GTK_TREE_MODEL_RENDER_NODE (priv->render_node_model),
                                                gtk_inspector_render_recording_get_node (GTK_INSPECTOR_RENDER_RECORDING (recording)));
    }
  else
    {
      gtk_render_node_view_set_render_node (GTK_RENDER_NODE_VIEW (priv->render_node_view), NULL);
      gtk_tree_model_render_node_set_root_node (GTK_TREE_MODEL_RENDER_NODE (priv->render_node_model), NULL);
    }

  gtk_tree_view_expand_all (GTK_TREE_VIEW (priv->render_node_tree));

  if (recording)
    g_object_unref (recording);
}

static const char *
node_type_name (GskRenderNodeType type)
{
  switch (type)
    {
    case GSK_NOT_A_RENDER_NODE:
    default:
      g_assert_not_reached ();
      return "Unknown";
    case GSK_CONTAINER_NODE:
      return "Container";
    case GSK_DEBUG_NODE:
      return "Debug";
    case GSK_CAIRO_NODE:
      return "Cairo";
    case GSK_COLOR_NODE:
      return "Color";
    case GSK_LINEAR_GRADIENT_NODE:
      return "Linear Gradient";
    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
      return "Repeating Linear Gradient";
    case GSK_BORDER_NODE:
      return "Border";
    case GSK_TEXTURE_NODE:
      return "Texture";
    case GSK_INSET_SHADOW_NODE:
      return "Inset Shadow";
    case GSK_OUTSET_SHADOW_NODE:
      return "Outset Shadow";
    case GSK_TRANSFORM_NODE:
      return "Transform";
    case GSK_OFFSET_NODE:
      return "Offset";
    case GSK_OPACITY_NODE:
      return "Opacity";
    case GSK_COLOR_MATRIX_NODE:
      return "Color Matrix";
    case GSK_REPEAT_NODE:
      return "Repeat";
    case GSK_CLIP_NODE:
      return "Clip";
    case GSK_ROUNDED_CLIP_NODE:
      return "Rounded Clip";
    case GSK_SHADOW_NODE:
      return "Shadow";
    case GSK_BLEND_NODE:
      return "Blend";
    case GSK_CROSS_FADE_NODE:
      return "CrossFade";
    case GSK_TEXT_NODE:
      return "Text";
    case GSK_BLUR_NODE:
      return "Blur";
    }
}

static char *
node_name (GskRenderNode *node)
{
  switch (gsk_render_node_get_node_type (node))
    {
    case GSK_NOT_A_RENDER_NODE:
    default:
      g_assert_not_reached ();
    case GSK_CONTAINER_NODE:
    case GSK_CAIRO_NODE:
    case GSK_LINEAR_GRADIENT_NODE:
    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
    case GSK_BORDER_NODE:
    case GSK_INSET_SHADOW_NODE:
    case GSK_OUTSET_SHADOW_NODE:
    case GSK_TRANSFORM_NODE:
    case GSK_OPACITY_NODE:
    case GSK_COLOR_MATRIX_NODE:
    case GSK_REPEAT_NODE:
    case GSK_CLIP_NODE:
    case GSK_ROUNDED_CLIP_NODE:
    case GSK_SHADOW_NODE:
    case GSK_BLEND_NODE:
    case GSK_CROSS_FADE_NODE:
    case GSK_TEXT_NODE:
    case GSK_BLUR_NODE:
      return g_strdup (node_type_name (gsk_render_node_get_node_type (node)));

    case GSK_OFFSET_NODE:
      return g_strdup_printf ("Offset %g, %g", gsk_offset_node_get_x_offset (node), gsk_offset_node_get_y_offset (node));

    case GSK_DEBUG_NODE:
      return g_strdup (gsk_debug_node_get_message (node));

    case GSK_COLOR_NODE:
      return gdk_rgba_to_string (gsk_color_node_peek_color (node));

    case GSK_TEXTURE_NODE:
      {
        GdkTexture *texture = gsk_texture_node_get_texture (node);
        return g_strdup_printf ("%dx%d Texture", gdk_texture_get_width (texture), gdk_texture_get_height (texture));
      }
    }
}

static void
render_node_list_get_value (GtkTreeModelRenderNode *model,
                            GskRenderNode          *node,
                            int                     column,
                            GValue                 *value)
{
  switch (column)
    {
    case COLUMN_NODE_NAME:
      g_value_take_string (value, node_name (node));
      break;

    default:
      g_assert_not_reached ();
      break;
    }
}

static GdkTexture *
get_color_texture (const GdkRGBA *color)
{
  GdkTexture *texture;
  guchar pixel[4];
  guchar *data;
  GBytes *bytes;
  gint width = 30;
  gint height = 30;
  gint i;

  pixel[0] = round (color->red * 255);
  pixel[1] = round (color->green * 255);
  pixel[2] = round (color->blue * 255);
  pixel[3] = round (color->alpha * 255);

  data = g_malloc (4 * width * height);
  for (i = 0; i < width * height; i++)
    {
      memcpy (data + 4 * i, pixel, 4);
    }

  bytes = g_bytes_new_take (data, 4 * width * height);
  texture = gdk_memory_texture_new (width,
                                    height,
                                    GDK_MEMORY_R8G8B8A8,
                                    bytes,
                                    width * 4);
  g_bytes_unref (bytes);

  return texture;
}

static GdkTexture *
get_linear_gradient_texture (gsize n_stops, const GskColorStop *stops)
{
  cairo_surface_t *surface;
  cairo_t *cr;
  cairo_pattern_t *pattern;
  GdkTexture *texture;
  int i;

  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 90, 30);
  cr = cairo_create (surface);

  pattern = cairo_pattern_create_linear (0, 0, 90, 0);
  for (i = 0; i < n_stops; i++)
    {
      cairo_pattern_add_color_stop_rgba (pattern,
                                         stops[i].offset,
                                         stops[i].color.red,
                                         stops[i].color.green,
                                         stops[i].color.blue,
                                         stops[i].color.alpha);
    }

  cairo_set_source (cr, pattern);
  cairo_pattern_destroy (pattern);
  cairo_rectangle (cr, 0, 0, 90, 30);
  cairo_fill (cr);
  cairo_destroy (cr);

  texture = gdk_texture_new_for_surface (surface);
  cairo_surface_destroy (surface);

  return texture;
}

static void
add_text_row (GtkListStore *store,
              const char   *name,
              const char   *text)
{
  gtk_list_store_insert_with_values (store, NULL, -1,
                                     0, name,
                                     1, text,
                                     2, FALSE,
                                     3, NULL,
                                     -1);
}

static void
add_color_row (GtkListStore  *store,
               const char    *name,
               const GdkRGBA *color)
{
  char *text;
  GdkTexture *texture;

  text = gdk_rgba_to_string (color);
  texture = get_color_texture (color);
  gtk_list_store_insert_with_values (store, NULL, -1,
                                     0, name,
                                     1, text,
                                     2, TRUE,
                                     3, texture,
                                     -1);
  g_free (text);
  g_object_unref (texture);
}

static void
add_float_row (GtkListStore  *store,
               const char    *name,
               float          value)
{
  char *text = g_strdup_printf ("%.2f", value);
  add_text_row (store, name, text);
  g_free (text);
}

static void
populate_render_node_properties (GtkListStore  *store,
                                 GskRenderNode *node)
{
  graphene_rect_t bounds;
  char *tmp;

  gtk_list_store_clear (store);

  gsk_render_node_get_bounds (node, &bounds);

  add_text_row (store, "Type", node_type_name (gsk_render_node_get_node_type (node)));

  tmp = g_strdup_printf ("%.2f x %.2f + %.2f + %.2f",
                         bounds.size.width,
                         bounds.size.height,
                         bounds.origin.x,
                         bounds.origin.y);
  add_text_row (store, "Bounds", tmp);
  g_free (tmp);

  switch (gsk_render_node_get_node_type (node))
    {
    case GSK_CAIRO_NODE:
      {
        GdkTexture *texture;
        cairo_surface_t *drawn_surface;
        cairo_t *cr;
        gboolean show_inline;

        drawn_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                                    ceilf (node->bounds.size.width),
                                                    ceilf (node->bounds.size.height));
        cr = cairo_create (drawn_surface);
        cairo_save (cr);
        cairo_translate (cr, -node->bounds.origin.x, -node->bounds.origin.y);
        gsk_render_node_draw (node, cr);
        cairo_restore (cr);

        cairo_destroy (cr);

        texture = gdk_texture_new_for_surface (drawn_surface);
        cairo_surface_destroy (drawn_surface);

        show_inline = gdk_texture_get_height (texture) <= 40 &&
                      gdk_texture_get_width (texture) <= 100;

        gtk_list_store_insert_with_values (store, NULL, -1,
                                           0, "Surface",
                                           1, show_inline ? "" : "Yes (click to show)",
                                           2, show_inline,
                                           3, texture,
                                           -1);
      }
      break;

    case GSK_TEXTURE_NODE:
      {
        GdkTexture *texture = g_object_ref (gsk_texture_node_get_texture (node));
        gboolean show_inline;

        show_inline = gdk_texture_get_height (texture) <= 40 &&
                      gdk_texture_get_width (texture) <= 100;

        gtk_list_store_insert_with_values (store, NULL, -1,
                                           0, "Texture",
                                           1, show_inline ? "" : "Yes (click to show)",
                                           2, show_inline,
                                           3, texture,
                                           -1);
      }
      break;

    case GSK_COLOR_NODE:
      add_color_row (store, "Color", gsk_color_node_peek_color (node));
      break;

    case GSK_LINEAR_GRADIENT_NODE:
    case GSK_REPEATING_LINEAR_GRADIENT_NODE:
      {
        const graphene_point_t *start = gsk_linear_gradient_node_peek_start (node);
        const graphene_point_t *end = gsk_linear_gradient_node_peek_end (node);
        const gsize n_stops = gsk_linear_gradient_node_get_n_color_stops (node);
        const GskColorStop *stops = gsk_linear_gradient_node_peek_color_stops (node);
        int i;
        GString *s;
        GdkTexture *texture;

        tmp = g_strdup_printf ("%.2f %.2f ⟶ %.2f %.2f", start->x, start->y, end->x, end->y);
        add_text_row (store, "Direction", tmp);
        g_free (tmp);

        s = g_string_new ("");
        for (i = 0; i < n_stops; i++)
          {
            tmp = gdk_rgba_to_string (&stops[i].color);
            g_string_append_printf (s, "%.2f, %s\n", stops[i].offset, tmp);
            g_free (tmp);
          }

        texture = get_linear_gradient_texture (n_stops, stops);
        gtk_list_store_insert_with_values (store, NULL, -1,
                                           0, "Color Stops",
                                           1, s->str,
                                           2, TRUE,
                                           3, texture,
                                           -1);
        g_string_free (s, TRUE);
        g_object_unref (texture);
      }
      break;

    case GSK_TEXT_NODE:
      {
        const PangoFont *font = gsk_text_node_peek_font (node);
        const PangoGlyphInfo *glyphs = gsk_text_node_peek_glyphs (node);
        const GdkRGBA *color = gsk_text_node_peek_color (node);
        guint num_glyphs = gsk_text_node_get_num_glyphs (node);
        float x = gsk_text_node_get_x (node);
        float y = gsk_text_node_get_y (node);
        PangoFontDescription *desc;
        GString *s;
        int i;

        desc = pango_font_describe ((PangoFont *)font);
        tmp = pango_font_description_to_string (desc);
        add_text_row (store, "Font", tmp);
        g_free (tmp);
        pango_font_description_free (desc);

        s = g_string_sized_new (6 * num_glyphs);
        for (i = 0; i < num_glyphs; i++)
          g_string_append_printf (s, "%x ", glyphs[i].glyph);
        add_text_row (store, "Glyphs", s->str);
        g_string_free (s, TRUE);

        tmp = g_strdup_printf ("%.2f %.2f", x, y);
        add_text_row (store, "Position", tmp);
        g_free (tmp);

        add_color_row (store, "Color", color);
      }
      break;

    case GSK_BORDER_NODE:
      {
        const char *name[4] = { "Top", "Right", "Bottom", "Left" };
        const float *widths = gsk_border_node_peek_widths (node);
        const GdkRGBA *colors = gsk_border_node_peek_colors (node);
        int i;

        for (i = 0; i < 4; i++)
          {
            GdkTexture *texture;
            char *text;

            texture = get_color_texture (&colors[i]);
            text = gdk_rgba_to_string (&colors[i]);
            tmp = g_strdup_printf ("%.2f, %s", widths[i], text);
            gtk_list_store_insert_with_values (store, NULL, -1,
                                               0, name[i],
                                               1, tmp,
                                               2, TRUE,
                                               3, texture,
                                               -1);
            g_free (text);
            g_free (tmp);
            g_object_unref (texture);
          }
      }
      break;

    case GSK_OPACITY_NODE:
      add_float_row (store, "Opacity", gsk_opacity_node_get_opacity (node));
      break;

    case GSK_CROSS_FADE_NODE:
      add_float_row (store, "Progress", gsk_cross_fade_node_get_progress (node));
      break;

    case GSK_BLEND_NODE:
      {
        GskBlendMode mode = gsk_blend_node_get_blend_mode (node);
        tmp = g_enum_to_string (GSK_TYPE_BLEND_MODE, mode);
        add_text_row (store, "Blendmode", tmp);
        g_free (tmp);
      }
      break;

    case GSK_BLUR_NODE:
      add_float_row (store, "Radius", gsk_blur_node_get_radius (node));
      break;

    case GSK_INSET_SHADOW_NODE:
      {
        const GdkRGBA *color = gsk_inset_shadow_node_peek_color (node);
        float dx = gsk_inset_shadow_node_get_dx (node);
        float dy = gsk_inset_shadow_node_get_dy (node);
        float spread = gsk_inset_shadow_node_get_spread (node);
        float radius = gsk_inset_shadow_node_get_blur_radius (node);

        add_color_row (store, "Color", color);

        tmp = g_strdup_printf ("%.2f %.2f", dx, dy);
        add_text_row (store, "Offset", tmp);
        g_free (tmp);

        add_float_row (store, "Spread", spread);
        add_float_row (store, "Radius", radius);
      }
      break;

    case GSK_OUTSET_SHADOW_NODE:
      {
        const GskRoundedRect *outline = gsk_outset_shadow_node_peek_outline (node);
        const GdkRGBA *color = gsk_outset_shadow_node_peek_color (node);
        float dx = gsk_outset_shadow_node_get_dx (node);
        float dy = gsk_outset_shadow_node_get_dy (node);
        float spread = gsk_outset_shadow_node_get_spread (node);
        float radius = gsk_outset_shadow_node_get_blur_radius (node);
        float rect[12];

        gsk_rounded_rect_to_float (outline, rect);
        tmp = g_strdup_printf ("%.2f x %.2f + %.2f + %.2f",
                               rect[2], rect[3], rect[0], rect[1]);
        add_text_row (store, "Outline", tmp);
        g_free (tmp);

        add_color_row (store, "Color", color);

        tmp = g_strdup_printf ("%.2f %.2f", dx, dy);
        add_text_row (store, "Offset", tmp);
        g_free (tmp);

        add_float_row (store, "Spread", spread);
        add_float_row (store, "Radius", radius);
      }
      break;

    case GSK_REPEAT_NODE:
      {
        const graphene_rect_t *child_bounds = gsk_repeat_node_peek_child_bounds (node);

        tmp = g_strdup_printf ("%.2f x %.2f + %.2f + %.2f",
                               child_bounds->size.width,
                               child_bounds->size.height,
                               child_bounds->origin.x,
                               child_bounds->origin.y);
        add_text_row (store, "Child Bounds", tmp);
        g_free (tmp);
      }
      break;

    case GSK_COLOR_MATRIX_NODE:
      {
        const graphene_matrix_t *matrix = gsk_color_matrix_node_peek_color_matrix (node);
        const graphene_vec4_t *offset = gsk_color_matrix_node_peek_color_offset (node);

        tmp = g_strdup_printf ("% .2f % .2f % .2f % .2f\n"
                               "% .2f % .2f % .2f % .2f\n"
                               "% .2f % .2f % .2f % .2f\n"
                               "% .2f % .2f % .2f % .2f",
                               graphene_matrix_get_value (matrix, 0, 0),
                               graphene_matrix_get_value (matrix, 0, 1),
                               graphene_matrix_get_value (matrix, 0, 2),
                               graphene_matrix_get_value (matrix, 0, 3),
                               graphene_matrix_get_value (matrix, 1, 0),
                               graphene_matrix_get_value (matrix, 1, 1),
                               graphene_matrix_get_value (matrix, 1, 2),
                               graphene_matrix_get_value (matrix, 1, 3),
                               graphene_matrix_get_value (matrix, 2, 0),
                               graphene_matrix_get_value (matrix, 2, 1),
                               graphene_matrix_get_value (matrix, 2, 2),
                               graphene_matrix_get_value (matrix, 2, 3),
                               graphene_matrix_get_value (matrix, 3, 0),
                               graphene_matrix_get_value (matrix, 3, 1),
                               graphene_matrix_get_value (matrix, 3, 2),
                               graphene_matrix_get_value (matrix, 3, 3));
        add_text_row (store, "Matrix", tmp);
        g_free (tmp);
        tmp = g_strdup_printf ("%.2f %.2f %.2f %.2f",
                               graphene_vec4_get_x (offset),
                               graphene_vec4_get_y (offset),
                               graphene_vec4_get_z (offset),
                               graphene_vec4_get_w (offset));
        add_text_row (store, "Offset", tmp);
        g_free (tmp);
      }
      break;

    case GSK_CLIP_NODE:
      {
        const graphene_rect_t *clip = gsk_clip_node_peek_clip (node);
        tmp = g_strdup_printf ("%.2f x %.2f + %.2f + %.2f",
                               clip->size.width,
                               clip->size.height,
                               clip->origin.x,
                               clip->origin.y);
        add_text_row (store, "Clip", tmp);
        g_free (tmp);
      }
      break;

    case GSK_ROUNDED_CLIP_NODE:
      {
        const GskRoundedRect *clip = gsk_rounded_clip_node_peek_clip (node);
        tmp = g_strdup_printf ("%.2f x %.2f + %.2f + %.2f",
                               clip->bounds.size.width,
                               clip->bounds.size.height,
                               clip->bounds.origin.x,
                               clip->bounds.origin.y);
        add_text_row (store, "Clip", tmp);
        g_free (tmp);

        tmp = g_strdup_printf ("%.2f x %.2f", clip->corner[0].width, clip->corner[0].height);
        add_text_row (store, "Top Left Corner Size", tmp);
        g_free (tmp);

        tmp = g_strdup_printf ("%.2f x %.2f", clip->corner[1].width, clip->corner[1].height);
        add_text_row (store, "Top Right Corner Size", tmp);
        g_free (tmp);

        tmp = g_strdup_printf ("%.2f x %.2f", clip->corner[2].width, clip->corner[2].height);
        add_text_row (store, "Bottom Right Corner Size", tmp);
        g_free (tmp);

        tmp = g_strdup_printf ("%.2f x %.2f", clip->corner[3].width, clip->corner[3].height);
        add_text_row (store, "Bottom Left Corner Size", tmp);
        g_free (tmp);
      }
      break;

    case GSK_CONTAINER_NODE:
      tmp = g_strdup_printf ("%d", gsk_container_node_get_n_children (node));
      add_text_row (store, "Children", tmp);
      g_free (tmp);
      break;

    case GSK_DEBUG_NODE:
      add_text_row (store, "Message", gsk_debug_node_get_message (node));
      break;

    case GSK_SHADOW_NODE:
      {
        int i;

        for (i = 0; i < gsk_shadow_node_get_n_shadows (node); i++)
          {
            char *label;
            char *value;
            const GskShadow *shadow = gsk_shadow_node_peek_shadow (node, i);

            label = g_strdup_printf ("Color %d", i);
            add_color_row (store, label, &shadow->color);
            g_free (label);

            label = g_strdup_printf ("Offset %d", i);
            value = g_strdup_printf ("%.2f %.2f", shadow->dx, shadow->dy);
            add_text_row (store, label, value);
            g_free (value);
            g_free (label);

            label = g_strdup_printf ("Radius %d", i);
            add_float_row (store, label, shadow->radius);
            g_free (label);
          }
      }
      break;

    case GSK_OFFSET_NODE:
      add_float_row (store, "x offset", gsk_offset_node_get_x_offset (node));
      add_float_row (store, "y offset", gsk_offset_node_get_y_offset (node));
      break;

    case GSK_NOT_A_RENDER_NODE:
    case GSK_TRANSFORM_NODE:
    default:
      break;
    }
}

static void
render_node_list_selection_changed (GtkTreeSelection     *selection,
                                    GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  GskRenderNode *node;
  GtkTreeIter iter;

  if (!gtk_tree_selection_get_selected (selection, NULL, &iter))
    {
      gtk_widget_set_sensitive (priv->render_node_save_button, FALSE);
      return;
    }

  gtk_widget_set_sensitive (priv->render_node_save_button, TRUE);
  node = gtk_tree_model_render_node_get_node_from_iter (GTK_TREE_MODEL_RENDER_NODE (priv->render_node_model), &iter);
  gtk_render_node_view_set_render_node (GTK_RENDER_NODE_VIEW (priv->render_node_view), node);

  populate_render_node_properties (GTK_LIST_STORE (priv->render_node_properties), node);
}

static void
render_node_save_response (GtkWidget     *dialog,
                           gint           response,
                           GskRenderNode *node)
{
  gtk_widget_hide (dialog);

  if (response == GTK_RESPONSE_ACCEPT)
    {
      GBytes *bytes = gsk_render_node_serialize (node);
      GError *error = NULL;

      if (!g_file_replace_contents (gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog)),
                                    g_bytes_get_data (bytes, NULL),
                                    g_bytes_get_size (bytes),
                                    NULL,
                                    FALSE,
                                    0,
                                    NULL,
                                    NULL,
                                    &error))
        {
          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,
                                                   _("Saving RenderNode failed"));
          gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (message_dialog),
                                                    "%s", error->message);
          g_signal_connect (message_dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
          gtk_widget_show (message_dialog);
          g_error_free (error);
        }

      g_bytes_unref (bytes);
    }

  gtk_widget_destroy (dialog);
}

static void
render_node_save (GtkButton            *button,
                  GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  GskRenderNode *node;
  GtkTreeIter iter;
  GtkWidget *dialog;
  char *filename, *nodename;

  if (!gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->render_node_tree)), NULL, &iter))
    return;

  node = gtk_tree_model_render_node_get_node_from_iter (GTK_TREE_MODEL_RENDER_NODE (priv->render_node_model), &iter);

  dialog = gtk_file_chooser_dialog_new ("",
                                        GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (recorder))),
                                        GTK_FILE_CHOOSER_ACTION_SAVE,
                                        _("_Cancel"), GTK_RESPONSE_CANCEL,
                                        _("_Save"), GTK_RESPONSE_ACCEPT,
                                        NULL);
  nodename = node_name (node);
  filename = g_strdup_printf ("%s.node", nodename);
  g_free (nodename);
  gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), filename);
  g_free (filename);
  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
  g_signal_connect (dialog, "response", G_CALLBACK (render_node_save_response), node);
  gtk_widget_show (dialog);
}

static char *
format_timespan (gint64 timespan)
{
  if (ABS (timespan) < G_TIME_SPAN_MILLISECOND)
    return g_strdup_printf ("%fus", (double) timespan);
  else if (ABS (timespan) < 10 * G_TIME_SPAN_MILLISECOND)
    return g_strdup_printf ("%.1fs", (double) timespan / G_TIME_SPAN_MILLISECOND);
  else if (ABS (timespan) < G_TIME_SPAN_SECOND)
    return g_strdup_printf ("%.0fms", (double) timespan / G_TIME_SPAN_MILLISECOND);
  else if (ABS (timespan) < 10 * G_TIME_SPAN_SECOND)
    return g_strdup_printf ("%.1fs", (double) timespan / G_TIME_SPAN_SECOND);
  else
    return g_strdup_printf ("%.0fs", (double) timespan / G_TIME_SPAN_SECOND);
}

static GtkWidget *
gtk_inspector_recorder_recordings_list_create_widget (gpointer item,
                                                      gpointer user_data)
{
  GtkInspectorRecorder *recorder = GTK_INSPECTOR_RECORDER (user_data);
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  GtkInspectorRecording *recording = GTK_INSPECTOR_RECORDING (item);
  GtkWidget *widget;

  if (GTK_INSPECTOR_IS_RENDER_RECORDING (recording))
    {
      GtkInspectorRecording *previous = NULL;
      char *time_str, *str;
      const char *render_str;
      cairo_region_t *region;
      GtkWidget *hbox, *label, *button;
      guint i;

      widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);

      hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
      gtk_box_pack_start (GTK_BOX (widget), hbox);

      for (i = 0; i < g_list_model_get_n_items (priv->recordings); i++)
        {
          GtkInspectorRecording *r = g_list_model_get_item (priv->recordings, i);

          g_object_unref (r);

          if (r == recording)
            break;

          if (GTK_INSPECTOR_IS_RENDER_RECORDING (r))
            previous = r;
          else if (GTK_INSPECTOR_IS_START_RECORDING (r))
            previous = NULL;
        }

      region = cairo_region_create_rectangle (
                   gtk_inspector_render_recording_get_area (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      cairo_region_subtract (region,
                             gtk_inspector_render_recording_get_clip_region (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      if (cairo_region_is_empty (region))
        render_str = "Full Render";
      else
        render_str = "Partial Render";
      cairo_region_destroy (region);

      if (previous)
        {
          time_str = format_timespan (gtk_inspector_recording_get_timestamp (recording) -
                                      gtk_inspector_recording_get_timestamp (previous));
          str = g_strdup_printf ("<b>%s</b>\n+%s", render_str, time_str);
          g_free (time_str);
        }
      else
        {
          str = g_strdup_printf ("<b>%s</b>\n", render_str);
        }
      label = gtk_label_new (str);
      gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
      g_free (str);
      gtk_box_pack_start (GTK_BOX (hbox), label);

      button = gtk_toggle_button_new ();
      gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
      gtk_button_set_icon_name (GTK_BUTTON (button), "view-more-symbolic");

      gtk_box_pack_end (GTK_BOX (hbox), button);

      label = gtk_label_new (gtk_inspector_render_recording_get_profiler_info (GTK_INSPECTOR_RENDER_RECORDING (recording)));
      gtk_widget_hide (label);
      gtk_box_pack_end (GTK_BOX (widget), label);
      g_object_bind_property (button, "active", label, "visible", 0);
    }
  else
    {
      widget = gtk_label_new ("<b>Start of Recording</b>");
      gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
    }

  g_object_set (widget, "margin", 6, NULL); /* Seriously? g_object_set() needed for that? */

  return widget;
}

static void
node_property_activated (GtkTreeView *tv,
                         GtkTreePath *path,
                         GtkTreeViewColumn *col,
                         GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  GtkTreeIter iter;
  GdkRectangle rect;
  GdkTexture *texture;
  gboolean visible;
  GtkWidget *popover;
  GtkWidget *image;

  gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->render_node_properties), &iter, path);
  gtk_tree_model_get (GTK_TREE_MODEL (priv->render_node_properties), &iter,
                      2, &visible,
                      3, &texture,
                      -1);
  gtk_tree_view_get_cell_area (tv, path, col, &rect);
  gtk_tree_view_convert_bin_window_to_widget_coords (tv, rect.x, rect.y, &rect.x, &rect.y);

  if (texture == NULL || visible)
    return;

  popover = gtk_popover_new (GTK_WIDGET (tv));
  gtk_popover_set_pointing_to (GTK_POPOVER (popover), &rect);

  image = gtk_image_new_from_paintable (GDK_PAINTABLE (texture));
  g_object_set (image, "margin", 20, NULL);
  gtk_container_add (GTK_CONTAINER (popover), image);
  gtk_popover_popup (GTK_POPOVER (popover));

  g_signal_connect (popover, "unmap", G_CALLBACK (gtk_widget_destroy), NULL);

  g_object_unref (texture);
}

static void
gtk_inspector_recorder_get_property (GObject    *object,
                                     guint       param_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  GtkInspectorRecorder *recorder = GTK_INSPECTOR_RECORDER (object);
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);

  switch (param_id)
    {
    case PROP_RECORDING:
      g_value_set_boolean (value, priv->recording != NULL);
      break;

    case PROP_DEBUG_NODES:
      g_value_set_boolean (value, priv->debug_nodes);
      break;

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

static void
gtk_inspector_recorder_set_property (GObject      *object,
                                     guint         param_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  GtkInspectorRecorder *recorder = GTK_INSPECTOR_RECORDER (object);

  switch (param_id)
    {
    case PROP_RECORDING:
      gtk_inspector_recorder_set_recording (recorder, g_value_get_boolean (value));
      break;

    case PROP_DEBUG_NODES:
      gtk_inspector_recorder_set_debug_nodes (recorder, g_value_get_boolean (value));
      break;

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

static void
gtk_inspector_recorder_class_init (GtkInspectorRecorderClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->get_property = gtk_inspector_recorder_get_property;
  object_class->set_property = gtk_inspector_recorder_set_property;

  props[PROP_RECORDING] =
    g_param_spec_boolean ("recording",
                          "Recording",
                          "Whether the recorder is currently recording",
                          FALSE,
                          G_PARAM_READWRITE);
  props[PROP_DEBUG_NODES] =
    g_param_spec_boolean ("debug-nodes",
                          "Debug nodes",
                          "Whether to insert extra debug nodes in the tree",
                          FALSE,
                          G_PARAM_READWRITE);

  g_object_class_install_properties (object_class, LAST_PROP, props);

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/recorder.ui");

  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, recordings);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, recordings_list);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, render_node_view);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, render_node_tree);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, render_node_save_button);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorRecorder, node_property_tree);

  gtk_widget_class_bind_template_callback (widget_class, recordings_clear_all);
  gtk_widget_class_bind_template_callback (widget_class, recordings_list_row_selected);
  gtk_widget_class_bind_template_callback (widget_class, render_node_list_selection_changed);
  gtk_widget_class_bind_template_callback (widget_class, render_node_save);
  gtk_widget_class_bind_template_callback (widget_class, node_property_activated);
}

static void
gtk_inspector_recorder_init (GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);

  gtk_widget_init_template (GTK_WIDGET (recorder));

  gtk_list_box_bind_model (GTK_LIST_BOX (priv->recordings_list),
                           priv->recordings,
                           gtk_inspector_recorder_recordings_list_create_widget,
                           recorder,
                           NULL);

  priv->render_node_model = gtk_tree_model_render_node_new (render_node_list_get_value,
                                                            N_NODE_COLUMNS,
                                                            G_TYPE_STRING);
  gtk_tree_view_set_model (GTK_TREE_VIEW (priv->render_node_tree), priv->render_node_model);
  g_object_unref (priv->render_node_model);

  priv->render_node_properties = GTK_TREE_MODEL (gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, GDK_TYPE_TEXTURE));
  gtk_tree_view_set_model (GTK_TREE_VIEW (priv->node_property_tree), priv->render_node_properties);
  g_object_unref (priv->render_node_properties);
}

static void
gtk_inspector_recorder_add_recording (GtkInspectorRecorder  *recorder,
                                      GtkInspectorRecording *recording)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  guint count;
  GtkListBoxRow *selected_row;
  gboolean should_select_new_row;

  count = g_list_model_get_n_items (priv->recordings);
  selected_row = gtk_list_box_get_selected_row (GTK_LIST_BOX (priv->recordings_list));
  if (count == 0 || selected_row == NULL)
    should_select_new_row = TRUE;
  else
    should_select_new_row = (gtk_list_box_row_get_index (selected_row) == count - 1);

  g_list_store_append (G_LIST_STORE (priv->recordings), recording);

  if (should_select_new_row)
    {
      gtk_list_box_select_row (GTK_LIST_BOX (priv->recordings_list),
                               gtk_list_box_get_row_at_index (GTK_LIST_BOX (priv->recordings_list), count));
    }
}

void
gtk_inspector_recorder_set_recording (GtkInspectorRecorder *recorder,
                                      gboolean              recording)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);

  if (gtk_inspector_recorder_is_recording (recorder) == recording)
    return;

  if (recording)
    {
      priv->recording = gtk_inspector_start_recording_new ();
      gtk_inspector_recorder_add_recording (recorder, priv->recording);
    }
  else
    {
      g_clear_object (&priv->recording);
    }

  g_object_notify_by_pspec (G_OBJECT (recorder), props[PROP_RECORDING]);
}

gboolean
gtk_inspector_recorder_is_recording (GtkInspectorRecorder *recorder)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);

  return priv->recording != NULL;
}

void
gtk_inspector_recorder_record_render (GtkInspectorRecorder *recorder,
                                      GtkWidget            *widget,
                                      GskRenderer          *renderer,
                                      GdkSurface           *surface,
                                      const cairo_region_t *region,
                                      GskRenderNode        *node)
{
  GtkInspectorRecording *recording;
  GdkFrameClock *frame_clock;

  if (!gtk_inspector_recorder_is_recording (recorder))
    return;

  frame_clock = gtk_widget_get_frame_clock (widget);

  recording = gtk_inspector_render_recording_new (gdk_frame_clock_get_frame_time (frame_clock),
                                                  gsk_renderer_get_profiler (renderer),
                                                  &(GdkRectangle) { 0, 0,
                                                    gdk_surface_get_width (surface),
                                                    gdk_surface_get_height (surface) },
                                                  region,
                                                  node);
  gtk_inspector_recorder_add_recording (recorder, recording);
  g_object_unref (recording);
}

void
gtk_inspector_recorder_set_debug_nodes (GtkInspectorRecorder *recorder,
                                        gboolean              debug_nodes)
{
  GtkInspectorRecorderPrivate *priv = gtk_inspector_recorder_get_instance_private (recorder);
  guint flags;

  if (priv->debug_nodes == debug_nodes)
    return;

  priv->debug_nodes = debug_nodes;

  flags = gtk_get_debug_flags ();

  if (debug_nodes)
    flags |= GTK_DEBUG_SNAPSHOT;
  else
    flags &= ~GTK_DEBUG_SNAPSHOT;

  gtk_set_debug_flags (flags);

  g_object_notify_by_pspec (G_OBJECT (recorder), props[PROP_DEBUG_NODES]);
}

// vim: set et sw=2 ts=2:
