/*
 * Copyright (c) 2021 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 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 <glib/gi18n-lib.h>

#include "clipboard.h"
#include "gtkdataviewer.h"
#include "window.h"

#include "gtkbinlayout.h"
#include "gtkbox.h"
#include "gtkdebug.h"
#include "gtkdropcontrollermotion.h"
#include "gtklabel.h"
#include "gtklistbox.h"
#include "gtktogglebutton.h"

struct _GtkInspectorClipboard
{
  GtkWidget parent;

  GdkDisplay *display;

  GtkWidget *swin;

  GtkWidget *dnd_formats;
  GtkWidget *dnd_info;

  GtkWidget *clipboard_formats;
  GtkWidget *clipboard_info;

  GtkWidget *primary_formats;
  GtkWidget *primary_info;
};

typedef struct _GtkInspectorClipboardClass
{
  GtkWidgetClass parent_class;
} GtkInspectorClipboardClass;

G_DEFINE_TYPE (GtkInspectorClipboard, gtk_inspector_clipboard, GTK_TYPE_WIDGET)

static void
load_gtype_value (GObject      *source,
                  GAsyncResult *res,
                  gpointer      data)
{
  GtkDataViewer *viewer = data;
  const GValue *value;
  GError *error = NULL;

  if (GDK_IS_CLIPBOARD (source))
    value = gdk_clipboard_read_value_finish (GDK_CLIPBOARD (source), res, &error);
  else if (GDK_IS_DROP (source))
    value = gdk_drop_read_value_finish (GDK_DROP (source), res, &error);
  else
    g_assert_not_reached ();

  if (value == NULL)
    gtk_data_viewer_load_error (viewer, error);
  else
    gtk_data_viewer_load_value (viewer, value);

  g_object_unref (viewer);
}

static gboolean
load_gtype (GtkDataViewer *viewer,
            GCancellable  *cancellable,
            gpointer       gtype)
{
  GObject *data_source = g_object_get_data (G_OBJECT (viewer), "data-source");

  if (GDK_IS_CLIPBOARD (data_source))
    {
      gdk_clipboard_read_value_async (GDK_CLIPBOARD (data_source),
                                      GPOINTER_TO_SIZE (gtype),
                                      G_PRIORITY_DEFAULT,
                                      cancellable,
                                      load_gtype_value,
                                      g_object_ref (viewer));
    }
  else if (GDK_IS_DROP (data_source))
    {
      gdk_drop_read_value_async (GDK_DROP (data_source),
                                 GPOINTER_TO_SIZE (gtype),
                                 G_PRIORITY_DEFAULT,
                                 cancellable,
                                 load_gtype_value,
                                 g_object_ref (viewer));
    }
  else
    {
      g_assert_not_reached ();
    }

  return TRUE;
}

static void
load_mime_type_stream (GObject      *source,
                       GAsyncResult *res,
                       gpointer      data)
{
  GtkDataViewer *viewer = data;
  GInputStream *stream;
  GError *error = NULL;
  const char *mime_type;

  if (GDK_IS_CLIPBOARD (source))
    stream = gdk_clipboard_read_finish (GDK_CLIPBOARD (source), res, &mime_type, &error);
  else if (GDK_IS_DROP (source))
    stream = gdk_drop_read_finish (GDK_DROP (source), res, &mime_type, &error);
  else
    g_assert_not_reached ();

  if (stream == NULL)
    gtk_data_viewer_load_error (viewer, error);
  else
    gtk_data_viewer_load_stream (viewer, stream, mime_type);

  g_object_unref (viewer);
}

static gboolean
load_mime_type (GtkDataViewer *viewer,
                GCancellable  *cancellable,
                gpointer       mime_type)
{
  GObject *data_source = g_object_get_data (G_OBJECT (viewer), "data-source");

  if (GDK_IS_CLIPBOARD (data_source))
    {
      gdk_clipboard_read_async (GDK_CLIPBOARD (data_source),
                                (const char *[2]) { mime_type, NULL },
                                G_PRIORITY_DEFAULT,
                                cancellable,
                                load_mime_type_stream,
                                g_object_ref (viewer));
    }
  else if (GDK_IS_DROP (data_source))
    {
      gdk_drop_read_async (GDK_DROP (data_source),
                           (const char *[2]) { mime_type, NULL },
                           G_PRIORITY_DEFAULT,
                           cancellable,
                           load_mime_type_stream,
                           g_object_ref (viewer));
    }
  else
    {
      g_assert_not_reached ();
    }

  return TRUE;
}

static void
on_drop_row_enter (GtkDropControllerMotion *motion,
                   double                   x,
                   double                   y,
                   GtkWidget               *viewer)
{
  gtk_widget_set_visible (viewer, TRUE);
}

static void
add_content_type_row (GtkInspectorClipboard *self,
                      GtkListBox            *list,
                      const char            *type_name,
                      GObject               *data_source,
                      GCallback              load_func,
                      gpointer               load_func_data)
{
  GtkWidget *row, *vbox, *hbox, *label, *viewer, *button;

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);

  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 40);
  gtk_box_append (GTK_BOX (vbox), hbox);

  label = gtk_label_new (type_name);
  gtk_widget_set_halign (label, GTK_ALIGN_START);
  gtk_widget_set_valign (label, GTK_ALIGN_BASELINE_FILL);
  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
  gtk_widget_set_hexpand (label, TRUE);
  gtk_box_append (GTK_BOX (hbox), label);

  viewer = gtk_data_viewer_new ();
  g_signal_connect (viewer, "load", load_func, load_func_data);
  g_object_set_data (G_OBJECT (viewer), "data-source", data_source);
  gtk_box_append (GTK_BOX (vbox), viewer);

  if (GDK_IS_CLIPBOARD (data_source))
    {
      button = gtk_toggle_button_new_with_label (_("Show"));
      gtk_widget_set_halign (button, GTK_ALIGN_END);
      gtk_widget_set_valign (button, GTK_ALIGN_BASELINE_FILL);
      gtk_box_append (GTK_BOX (hbox), button);

      g_object_bind_property (G_OBJECT (button), "active",
                              G_OBJECT (viewer), "visible",
                              G_BINDING_SYNC_CREATE);
    }
  else
    {
      GtkEventController *controller = gtk_drop_controller_motion_new ();
      g_signal_connect (controller, "enter", G_CALLBACK (on_drop_row_enter), viewer);
      gtk_widget_add_controller (vbox, controller);

      gtk_widget_set_visible (viewer, FALSE);

      label = gtk_label_new (_("Hover to load"));
      g_object_bind_property (G_OBJECT (viewer), "visible",
                              G_OBJECT (label), "visible",
                              G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
      gtk_widget_set_halign (label, GTK_ALIGN_END);
      gtk_widget_set_valign (label, GTK_ALIGN_BASELINE_FILL);
      gtk_box_append (GTK_BOX (hbox), label);
    }

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

  gtk_list_box_insert (list, row, -1);
}

static void
clear_formats (GtkInspectorClipboard *self,
               GtkListBox            *list)
{
  GtkListBoxRow *row;

  while ((row = gtk_list_box_get_row_at_index (list, 1)))
    gtk_list_box_remove (list, GTK_WIDGET (row));
}

static void
init_formats (GtkInspectorClipboard *self,
              GtkListBox            *list,
              GdkContentFormats     *formats,
              GObject               *data_source)
{
  const char * const *mime_types;
  const GType *gtypes;
  gsize i, n;

  clear_formats (self, list);

  gtypes = gdk_content_formats_get_gtypes (formats, &n);
  for (i = 0; i < n; i++)
    add_content_type_row (self, list, g_type_name (gtypes[i]), data_source, G_CALLBACK (load_gtype), GSIZE_TO_POINTER (gtypes[i]));

  mime_types = gdk_content_formats_get_mime_types (formats, &n);
  for (i = 0; i < n; i++)
    add_content_type_row (self, list, mime_types[i], data_source, G_CALLBACK (load_mime_type), (gpointer) mime_types[i]);
}

static void
init_info (GtkInspectorClipboard *self,
           GtkLabel              *label,
           GdkClipboard          *clipboard)
{
  GdkContentFormats *formats;

  formats = gdk_clipboard_get_formats (clipboard);
  if (gdk_content_formats_get_gtypes (formats, NULL) == NULL &&
      gdk_content_formats_get_mime_types (formats, NULL) == NULL)
    {
      gtk_label_set_text (label, C_("clipboard", "empty"));
      return;
    }

  if (gdk_clipboard_is_local (clipboard))
    gtk_label_set_text (label, C_("clipboard", "local"));
  else
    gtk_label_set_text (label, C_("clipboard", "remote"));
}

static void
clipboard_notify (GdkClipboard          *clipboard,
                  GParamSpec            *pspec,
                  GtkInspectorClipboard *self)
{
  if (g_str_equal (pspec->name, "formats"))
    {
      init_formats (self, GTK_LIST_BOX (self->clipboard_formats), gdk_clipboard_get_formats (clipboard), G_OBJECT (clipboard));
    }

  init_info (self, GTK_LABEL (self->clipboard_info), clipboard);
}

static void
primary_notify (GdkClipboard          *clipboard,
                GParamSpec            *pspec,
                GtkInspectorClipboard *self)
{
  if (g_str_equal (pspec->name, "formats"))
    {
      init_formats (self, GTK_LIST_BOX (self->primary_formats), gdk_clipboard_get_formats (clipboard), G_OBJECT (clipboard));
    }

  init_info (self, GTK_LABEL (self->primary_info), clipboard);
}

static void
drop_done (gpointer data,
           GObject *object)
{
  GtkInspectorClipboard *self = data;

  clear_formats (self, GTK_LIST_BOX (self->dnd_formats));
}

static void
on_drop_enter (GtkDropControllerMotion *motion,
               double                   x,
               double                   y,
               GtkInspectorClipboard   *self)
{
  GdkDrop *drop = gtk_drop_controller_motion_get_drop (motion);

  g_object_weak_ref (G_OBJECT (drop), drop_done, self);

  init_formats (self, GTK_LIST_BOX (self->dnd_formats), gdk_drop_get_formats (drop), G_OBJECT (drop));

  if (gdk_drop_get_drag (drop))
    gtk_label_set_text (GTK_LABEL (self->dnd_info), C_("clipboard", "local"));
  else
    gtk_label_set_text (GTK_LABEL (self->dnd_info), C_("clipboard", "remote"));
}

static void
gtk_inspector_clipboard_unset_display (GtkInspectorClipboard *self)
{
  GdkClipboard *clipboard;

  if (self->display == NULL)
    return;

  clipboard = gdk_display_get_clipboard (self->display);
  g_signal_handlers_disconnect_by_func (clipboard, clipboard_notify, self);

  clipboard = gdk_display_get_primary_clipboard (self->display);
  g_signal_handlers_disconnect_by_func (clipboard, primary_notify, self);
}

static void
gtk_inspector_clipboard_init (GtkInspectorClipboard *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));
}

static void
gtk_inspector_clipboard_dispose (GObject *object)
{
  GtkInspectorClipboard *self = GTK_INSPECTOR_CLIPBOARD (object);

  gtk_inspector_clipboard_unset_display (self);

  gtk_widget_dispose_template (GTK_WIDGET (self), GTK_TYPE_INSPECTOR_CLIPBOARD);

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

static void
gtk_inspector_clipboard_class_init (GtkInspectorClipboardClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose = gtk_inspector_clipboard_dispose;

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/clipboard.ui");
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, swin);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, dnd_formats);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, dnd_info);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, clipboard_formats);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, clipboard_info);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, primary_formats);
  gtk_widget_class_bind_template_child (widget_class, GtkInspectorClipboard, primary_info);

  gtk_widget_class_bind_template_callback (widget_class, on_drop_enter);

  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
}

void
gtk_inspector_clipboard_set_display (GtkInspectorClipboard *self,
                                     GdkDisplay            *display)
{
  GdkClipboard *clipboard;

  gtk_inspector_clipboard_unset_display (self);

  self->display = display;

  if (display == NULL)
    return;

  clipboard = gdk_display_get_clipboard (display);
  g_signal_connect (clipboard, "notify", G_CALLBACK (clipboard_notify), self);
  init_formats (self, GTK_LIST_BOX (self->clipboard_formats), gdk_clipboard_get_formats (clipboard), G_OBJECT (clipboard));
  init_info (self, GTK_LABEL (self->clipboard_info), clipboard);

  clipboard = gdk_display_get_primary_clipboard (display);
  g_signal_connect (clipboard, "notify", G_CALLBACK (primary_notify), self);
  init_formats (self, GTK_LIST_BOX (self->primary_formats), gdk_clipboard_get_formats (clipboard), G_OBJECT (clipboard));
  init_info (self, GTK_LABEL (self->primary_info), clipboard);
}

