#include <gtk/gtk.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#elif defined (G_OS_WIN32)
#include <io.h>
#endif

static GdkTexture *
render_paintable_to_texture (GdkPaintable *paintable)
{
  GtkSnapshot *snapshot;
  GskRenderNode *node;
  int width, height;
  cairo_surface_t *surface;
  cairo_t *cr;
  GdkTexture *texture;
  GBytes *bytes;

  width = gdk_paintable_get_intrinsic_width (paintable);
  if (width == 0)
    width = 32;

  height = gdk_paintable_get_intrinsic_height (paintable);
  if (height == 0)
    height = 32;

  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);

  snapshot = gtk_snapshot_new ();
  gdk_paintable_snapshot (paintable, snapshot, width, height);
  node = gtk_snapshot_free_to_node (snapshot);

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

  gsk_render_node_unref (node);

  bytes = g_bytes_new_with_free_func (cairo_image_surface_get_data (surface),
                                      cairo_image_surface_get_height (surface)
                                      * cairo_image_surface_get_stride (surface),
                                      (GDestroyNotify) cairo_surface_destroy,
                                      cairo_surface_reference (surface));
  texture = gdk_memory_texture_new (cairo_image_surface_get_width (surface),
                                    cairo_image_surface_get_height (surface),
                                    GDK_MEMORY_DEFAULT,
                                    bytes,
                                    cairo_image_surface_get_stride (surface));
  g_bytes_unref (bytes);
  cairo_surface_destroy (surface);

  return texture;
}

static GdkTexture *
get_image_texture (GtkImage *image)
{
  GtkIconTheme *icon_theme;
  const char *icon_name;
  int width = 48;
  GdkPaintable *paintable = NULL;
  GdkTexture *texture = NULL;
  GtkIconPaintable *icon;

  switch (gtk_image_get_storage_type (image))
    {
    case GTK_IMAGE_PAINTABLE:
      paintable = gtk_image_get_paintable (image);
      break;
    case GTK_IMAGE_ICON_NAME:
      icon_name = gtk_image_get_icon_name (image);
      icon_theme = gtk_icon_theme_get_for_display (gtk_widget_get_display (GTK_WIDGET (image)));
      icon = gtk_icon_theme_lookup_icon (icon_theme,
                                         icon_name,
                                         NULL,
                                         width, 1,
                                         gtk_widget_get_direction (GTK_WIDGET (image)),
                                         0);
      paintable = GDK_PAINTABLE (icon);
      break;
    case GTK_IMAGE_GICON:
    case GTK_IMAGE_EMPTY:
    default:
      g_warning ("Image storage type %d not handled",
                 gtk_image_get_storage_type (image));
    }

  if (paintable)
    texture = render_paintable_to_texture (paintable);

  g_object_unref (paintable);

  return texture;
}

enum {
  TOP_LEFT,
  CENTER,
  BOTTOM_RIGHT
};

static void
got_texture (GObject *source,
             GAsyncResult *result,
             gpointer data)
{
  GdkDrop *drop = GDK_DROP (source);
  GtkWidget *image = data;
  const GValue *value;
  GError *error = NULL;

  value = gdk_drop_read_value_finish (drop, result, &error);
  if (value)
    {
      GdkTexture *texture = g_value_get_object (value);
      gtk_image_set_from_paintable (GTK_IMAGE (image), GDK_PAINTABLE (texture));
      gdk_drop_finish (drop, GDK_ACTION_COPY);
    }
  else
    {
      g_error_free (error);
      gdk_drop_finish (drop, 0);
    }

  g_object_set_data (G_OBJECT (image), "drop", NULL);
}

static void
perform_drop (GdkDrop   *drop,
              GtkWidget *image)
{
  if (gdk_content_formats_contain_gtype (gdk_drop_get_formats (drop), GDK_TYPE_TEXTURE))
    gdk_drop_read_value_async (drop, GDK_TYPE_TEXTURE, G_PRIORITY_DEFAULT, NULL, got_texture, image);
  else
    {
      gdk_drop_finish (drop, 0);
      g_object_set_data (G_OBJECT (image), "drop", NULL);
    }
}

static void
do_copy (GtkWidget *button)
{
  GtkWidget *popover = gtk_widget_get_ancestor (button, GTK_TYPE_POPOVER);
  GtkWidget *image = gtk_widget_get_parent (popover);
  GdkDrop *drop = GDK_DROP (g_object_get_data (G_OBJECT (image), "drop"));

  gtk_popover_popdown (GTK_POPOVER (popover));
  perform_drop (drop, image);
}

static void
do_cancel (GtkWidget *button)
{
  GtkWidget *popover = gtk_widget_get_ancestor (button, GTK_TYPE_POPOVER);
  GtkWidget *image = gtk_widget_get_parent (popover);
  GdkDrop *drop = GDK_DROP (g_object_get_data (G_OBJECT (image), "drop"));

  gtk_popover_popdown (GTK_POPOVER (popover));
  gdk_drop_finish (drop, 0);

  g_object_set_data (G_OBJECT (image), "drop", NULL);
}

static void
ask_actions (GdkDrop *drop,
             GtkWidget *image)
{
  GtkWidget *popover, *box, *button;

  popover = g_object_get_data (G_OBJECT (image), "popover");
  if (!popover)
    {
      popover = gtk_popover_new ();
      gtk_widget_set_parent (popover, image);
      g_object_set_data (G_OBJECT (image), "popover", popover);

      box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
      gtk_popover_set_child (GTK_POPOVER (popover), box);
      button = gtk_button_new_with_label ("Copy");
      g_signal_connect (button, "clicked", G_CALLBACK (do_copy), NULL);
      gtk_box_append (GTK_BOX (box), button);
      button = gtk_button_new_with_label ("Move");
      g_signal_connect (button, "clicked", G_CALLBACK (do_copy), NULL);
      gtk_box_append (GTK_BOX (box), button);
      button = gtk_button_new_with_label ("Cancel");
      g_signal_connect (button, "clicked", G_CALLBACK (do_cancel), NULL);
      gtk_box_append (GTK_BOX (box), button);
    }
  gtk_popover_popup (GTK_POPOVER (popover));
}

static gboolean
delayed_deny (gpointer data)
{
  GtkDropTargetAsync *dest = data;
  GtkWidget *image = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (dest));
  GdkDrop *drop = GDK_DROP (g_object_get_data (G_OBJECT (image), "drop"));

  if (drop)
    {
      g_print ("denying drop, late\n");
      gtk_drop_target_async_reject_drop (dest, drop);
    }

  return G_SOURCE_REMOVE;
}

static gboolean
image_drag_accept (GtkDropTargetAsync *dest,
                   GdkDrop            *drop,
                   gpointer            data)
{
  GtkWidget *image = data;
  g_object_set_data_full (G_OBJECT (image), "drop", g_object_ref (drop), g_object_unref);

  g_print ("accept\n");

  g_timeout_add (1000, delayed_deny, dest);

  return TRUE;
}

static gboolean
image_drag_drop (GtkDropTarget    *dest,
                 GdkDrop          *drop,
                 double            x,
                 double            y,
                 gpointer          data)
{
  GtkWidget *image = data;
  GdkDragAction action = gdk_drop_get_actions (drop);
  const char *name[] = { "copy", "move", "link", "ask" };

  g_object_set_data_full (G_OBJECT (image), "drop", g_object_ref (drop), g_object_unref);

  g_print ("drop, actions: ");
  for (guint i = 0; i < 4; i++)
    {
      if (action & (1 << i))
        {
          if (i > 0)
            g_print (", ");
          g_print ("%s", name[i]);
        }
    }
  g_print ("\n");

  if (!gdk_drag_action_is_unique (action))
    ask_actions (drop, image);
  else
    perform_drop (drop, image);

  return TRUE;
}

static void
update_source_icon (GtkDragSource *source,
                    const char *icon_name,
                    int hotspot)
{
  GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (source));
  GtkIconPaintable *icon;
  int hot_x, hot_y;
  int size = 48;

  if (widget == NULL)
    return;

  icon = gtk_icon_theme_lookup_icon (gtk_icon_theme_get_for_display (gtk_widget_get_display (widget)),
                                     icon_name,
                                     NULL,
                                     size, 1,
                                     gtk_widget_get_direction (widget),
                                     0);
  switch (hotspot)
    {
    default:
    case TOP_LEFT:
      hot_x = 0;
      hot_y = 0;
      break;
    case CENTER:
      hot_x = size / 2;
      hot_y = size / 2;
      break;
    case BOTTOM_RIGHT:
      hot_x = size;
      hot_y = size;
      break;
    }
  gtk_drag_source_set_icon (source, GDK_PAINTABLE (icon), hot_x, hot_y);
  g_object_unref (icon);
}

static GdkContentProvider *
drag_prepare (GtkDragSource *source,
              double         x,
              double         y)
{
  GtkImage *image = GTK_IMAGE (gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (source)));
  GdkTexture *texture;
  GdkContentProvider *content;

  content = gdk_content_provider_new_typed (G_TYPE_STRING, gtk_image_get_icon_name (GTK_IMAGE (image)));

  texture = get_image_texture (image);
  if (texture)
    {
      content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
                                                  gdk_content_provider_new_typed (GDK_TYPE_TEXTURE, texture),
                                                  content,
                                                }, 2);
      g_object_unref (texture);
    }

  return content;
}

static void
drag_begin (GtkDragSource *source)
{
  g_print ("drag begin\n");
}

static void
drag_end (GtkDragSource *source)
{
  g_print ("drag end\n");
}

static gboolean
drag_cancel (GtkDragSource       *source,
             GdkDrag             *drag,
             GdkDragCancelReason  reason)
{
  const char *msg[] = { "no target", "user cancelled", "error" };
  g_print ("drag failed: %s\n", msg[reason]);
  return FALSE;
}

static GtkWidget *
make_image (const char *icon_name, int hotspot)
{
  GtkWidget *image;
  GtkDragSource *source;
  GtkDropTargetAsync *dest;
  GdkContentFormats *formats;
  GdkContentFormatsBuilder *builder;

  image = gtk_image_new_from_icon_name (icon_name);
  gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);

  builder = gdk_content_formats_builder_new ();
  gdk_content_formats_builder_add_gtype (builder, GDK_TYPE_TEXTURE);
  gdk_content_formats_builder_add_gtype (builder, G_TYPE_STRING);
  formats = gdk_content_formats_builder_free_to_formats (builder);

  source = gtk_drag_source_new ();
  gtk_drag_source_set_actions (source, GDK_ACTION_COPY|GDK_ACTION_MOVE|GDK_ACTION_ASK);
  update_source_icon (source, icon_name, hotspot);

  g_signal_connect (source, "prepare", G_CALLBACK (drag_prepare), NULL);
  g_signal_connect (source, "drag-begin", G_CALLBACK (drag_begin), NULL);
  g_signal_connect (source, "drag-end", G_CALLBACK (drag_end), NULL);
  g_signal_connect (source, "drag-cancel", G_CALLBACK (drag_cancel), NULL);
  gtk_widget_add_controller (image, GTK_EVENT_CONTROLLER (source));

  dest = gtk_drop_target_async_new (formats, GDK_ACTION_COPY|GDK_ACTION_MOVE|GDK_ACTION_ASK);
  g_signal_connect (dest, "accept", G_CALLBACK (image_drag_accept), image);
  g_signal_connect (dest, "drop", G_CALLBACK (image_drag_drop), image);
  gtk_widget_add_controller (image, GTK_EVENT_CONTROLLER (dest));

  return image;
}

static void
spinner_drag_begin (GtkDragSource *source,
                    GdkDrag       *drag,
                    GtkWidget     *widget)
{
  GdkPaintable *paintable;

  paintable = gtk_widget_paintable_new (widget);
  gtk_drag_source_set_icon (source, paintable, 0, 0);
  g_object_unref (paintable);
}

static GtkWidget *
make_spinner (void)
{
  GtkWidget *spinner;
  GtkDragSource *source;
  GdkContentProvider *content;

  spinner = gtk_spinner_new ();
  gtk_spinner_start (GTK_SPINNER (spinner));

  content = gdk_content_provider_new_typed (G_TYPE_STRING, "ACTIVE");
  source = gtk_drag_source_new ();
  gtk_drag_source_set_content (source, content);
  g_signal_connect (source, "drag-begin", G_CALLBACK (spinner_drag_begin), spinner);
  gtk_widget_add_controller (spinner, GTK_EVENT_CONTROLLER (source));

  g_object_unref (content);

  return spinner;
}

int
main (int argc, char *Argv[])
{
  GtkWidget *window;
  GtkWidget *grid;
  GtkWidget *entry;

  gtk_init ();

  window = gtk_window_new ();
  gtk_window_set_title (GTK_WINDOW (window), "Drag And Drop");
  gtk_window_set_resizable (GTK_WINDOW (window), FALSE);

  grid = gtk_grid_new ();
  g_object_set (grid,
                "margin-start", 20,
                "margin-end", 20,
                "margin-top", 20,
                "margin-bottom", 20,
                "row-spacing", 20,
                "column-spacing", 20,
                NULL);
  gtk_window_set_child (GTK_WINDOW (window), grid);
  gtk_grid_attach (GTK_GRID (grid), make_image ("dialog-warning", TOP_LEFT), 0, 0, 1, 1);
  gtk_grid_attach (GTK_GRID (grid), make_image ("process-stop", BOTTOM_RIGHT), 1, 0, 1, 1);

  entry = gtk_entry_new ();
  gtk_grid_attach (GTK_GRID (grid), entry, 0, 1, 2, 1);

  gtk_grid_attach (GTK_GRID (grid), make_spinner (), 0, 2, 1, 1);
  gtk_grid_attach (GTK_GRID (grid), make_image ("weather-clear", CENTER), 1, 2, 1, 1);

  gtk_grid_attach (GTK_GRID (grid), make_image ("dialog-question", TOP_LEFT), 0, 3, 1, 1);

  gtk_grid_attach (GTK_GRID (grid), make_image ("dialog-information", CENTER), 1, 3, 1, 1);

  gtk_window_present (GTK_WINDOW (window));
  while (TRUE)
    g_main_context_iteration (NULL, TRUE);

  return 0;
}
