/*
 * File-Roller
 *
 * Copyright (C) 2005 Free Software Foundation, Inc.
 * Author: Paolo Bacchilega
 */

/* eggtreemultidnd.c
 * Copyright (C) 2001  Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */


#include <string.h>
#include <math.h>
#include <gtk/gtk.h>
#include "eggtreemultidnd.h"

#define EGG_TREE_MULTI_DND_STRING "EggTreeMultiDndString"

static GtkTargetEntry target_table[] = {
        { "XdndDirectSave0", 0, 0 },
        { "XdndFileRoller0", 0, 1 }
};

typedef struct
{
  guint   pressed_button;
  gint    x;
  gint    y;
  guint   motion_notify_handler;
  guint   drag_data_get_handler;
  GSList *event_list;
  gboolean pending_event;
  gboolean button_released;
} EggTreeMultiDndData;


G_DEFINE_INTERFACE (EggTreeMultiDragSource, egg_tree_multi_drag_source, 0)


static void
egg_tree_multi_drag_source_default_init (EggTreeMultiDragSourceInterface *iface)
{
        /* void */
}


/**
 * egg_tree_multi_drag_source_row_draggable:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row on which user is initiating a drag
 *
 * Asks the #EggTreeMultiDragSource whether a particular row can be used as
 * the source of a DND operation. If the source doesn't implement
 * this interface, the row is assumed draggable.
 *
 * Return value: %TRUE if the row can be dragged
 **/
gboolean
egg_tree_multi_drag_source_row_draggable (EggTreeMultiDragSource *drag_source,
					  GList                  *path_list)
{
  EggTreeMultiDragSourceInterface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

  g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
  g_return_val_if_fail (iface->row_draggable != NULL, FALSE);
  g_return_val_if_fail (path_list != NULL, FALSE);

  if (iface->row_draggable)
    return (* iface->row_draggable) (drag_source, path_list);
  else
    return TRUE;
}


/**
 * egg_tree_multi_drag_source_drag_data_delete:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row that was being dragged
 *
 * Asks the #EggTreeMultiDragSource to delete the row at @path, because
 * it was moved somewhere else via drag-and-drop. Returns %FALSE
 * if the deletion fails because @path no longer exists, or for
 * some model-specific reason. Should robustly handle a @path no
 * longer found in the model!
 *
 * Return value: %TRUE if the row was successfully deleted
 **/
gboolean
egg_tree_multi_drag_source_drag_data_delete (EggTreeMultiDragSource *drag_source,
					     GList                  *path_list)
{
  EggTreeMultiDragSourceInterface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

  g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
  g_return_val_if_fail (iface->drag_data_delete != NULL, FALSE);
  g_return_val_if_fail (path_list != NULL, FALSE);

  return (* iface->drag_data_delete) (drag_source, path_list);
}


/**
 * egg_tree_multi_drag_source_drag_data_get:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row that was dragged
 * @selection_data: a #EggSelectionData to fill with data from the dragged row
 *
 * Asks the #EggTreeMultiDragSource to fill in @selection_data with a
 * representation of the row at @path. @selection_data->target gives
 * the required type of the data.  Should robustly handle a @path no
 * longer found in the model!
 *
 * Return value: %TRUE if data of the required type was provided
 **/
gboolean
egg_tree_multi_drag_source_drag_data_get (EggTreeMultiDragSource *drag_source,
					  GdkDragContext         *context,
					  GtkSelectionData       *selection_data,
					  GList                  *path_list)
{
  EggTreeMultiDragSourceInterface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

  g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
  g_return_val_if_fail (iface->drag_data_get != NULL, FALSE);
  g_return_val_if_fail (path_list != NULL, FALSE);
  g_return_val_if_fail (selection_data != NULL, FALSE);

  return (* iface->drag_data_get) (drag_source, context, selection_data, path_list);
}


static void
stop_drag_check (GtkWidget *widget)
{
  EggTreeMultiDndData *priv_data;
  GSList *l;

  priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);

  for (l = priv_data->event_list; l != NULL; l = l->next)
    gdk_event_free (l->data);

  g_slist_free (priv_data->event_list);
  priv_data->event_list = NULL;
  priv_data->pending_event = FALSE;

  if (priv_data->motion_notify_handler) {
    g_signal_handler_disconnect (widget, priv_data->motion_notify_handler);
    priv_data->motion_notify_handler = 0;
  }
}


static gboolean
egg_tree_multi_drag_button_release_event (GtkWidget      *widget,
					  GdkEventButton *event,
					  gpointer        data)
{
  EggTreeMultiDndData *priv_data;
  GSList *l;

  priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);
  if (priv_data == NULL)
    return FALSE;

  priv_data->button_released = TRUE;

  for (l = priv_data->event_list; l != NULL; l = l->next)
    gtk_propagate_event (widget, l->data);

  stop_drag_check (widget);

  return FALSE;
}


static void
selection_foreach (GtkTreeModel *model,
		   GtkTreePath  *path,
		   GtkTreeIter  *iter,
		   gpointer      data)
{
  GList **list_ptr;

  list_ptr = (GList **) data;

  *list_ptr = g_list_prepend (*list_ptr, gtk_tree_row_reference_new (model, path));
}


static void
path_list_free (GList *path_list)
{
  g_list_foreach (path_list, (GFunc) gtk_tree_row_reference_free, NULL);
  g_list_free (path_list);
}


static void
set_context_data (GdkDragContext *context,
		  GList          *path_list)
{
  g_object_set_data_full (G_OBJECT (context),
                          "egg-tree-view-multi-source-row",
                          path_list,
                          (GDestroyNotify) path_list_free);
}


static GList *
get_context_data (GdkDragContext *context)
{
  return g_object_get_data (G_OBJECT (context),
			    "egg-tree-view-multi-source-row");
}


static gboolean
egg_tree_multi_drag_drag_data_get (GtkWidget        *widget,
				   GdkDragContext   *context,
				   GtkSelectionData *selection_data,
				   guint             info,
				   guint             time)
{
  GtkTreeView  *tree_view;
  GtkTreeModel *model;
  GList        *path_list;

  tree_view = GTK_TREE_VIEW (widget);
  model = gtk_tree_view_get_model (tree_view);
  if (model == NULL)
    return FALSE;

  path_list = get_context_data (context);
  if (path_list == NULL)
    return FALSE;

  /* We can implement the GTK_TREE_MODEL_ROW target generically for
   * any model; for DragSource models there are some other targets
   * we also support.
   */

  if (! EGG_IS_TREE_MULTI_DRAG_SOURCE (model))
    return FALSE;

  return egg_tree_multi_drag_source_drag_data_get (EGG_TREE_MULTI_DRAG_SOURCE (model),
      						   context,
						   selection_data,
						   path_list);
}


static gboolean
_gtk_cairo_surface_extents (cairo_surface_t *surface,
                            GdkRectangle *extents)
{
  double x1, x2, y1, y2;
  cairo_t *cr;

  g_return_val_if_fail (surface != NULL, FALSE);
  g_return_val_if_fail (extents != NULL, FALSE);

  cr = cairo_create (surface);
  cairo_clip_extents (cr, &x1, &y1, &x2, &y2);

  x1 = floor (x1);
  y1 = floor (y1);
  x2 = ceil (x2);
  y2 = ceil (y2);
  x2 -= x1;
  y2 -= y1;

  if (x1 < G_MININT || x1 > G_MAXINT ||
      y1 < G_MININT || y1 > G_MAXINT ||
      x2 > G_MAXINT || y2 > G_MAXINT)
    {
      extents->x = extents->y = extents->width = extents->height = 0;
      return FALSE;
    }

  extents->x = x1;
  extents->y = y1;
  extents->width = x2;
  extents->height = y2;

  return TRUE;
}


#define DRAG_ICON_MAX_ROWS 3
#define DRAG_ICON_OFFSET 5


static gboolean
egg_tree_multi_drag_motion_event (GtkWidget      *widget,
				  GdkEventMotion *event,
				  gpointer        data)
{
  EggTreeMultiDndData *priv_data;

  priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);

  if (! priv_data->pending_event)
    return FALSE;

  if (gtk_drag_check_threshold (widget,
				priv_data->x,
				priv_data->y,
				event->x,
				event->y))
    {
      GList            *path_list = NULL;
      GtkTreeSelection *selection;
      GtkTreeModel     *model;
      GdkDragContext   *context;

      stop_drag_check (widget);

      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
      gtk_tree_selection_selected_foreach (selection, selection_foreach, &path_list);
      if (path_list == NULL)
	      return FALSE;

      path_list = g_list_reverse (path_list);

      model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
      if (egg_tree_multi_drag_source_row_draggable (EGG_TREE_MULTI_DRAG_SOURCE (model), path_list))
	{
	  GtkTargetList *target_list;
	  GtkTreePath   *tree_path;
	  int            cell_x;
	  int            cell_y;

	  target_list = gtk_target_list_new (target_table, G_N_ELEMENTS (target_table));
	  context = gtk_drag_begin_with_coordinates (widget,
			  	  	  	     target_list,
			  	  	  	     GDK_ACTION_COPY,
			  	  	  	     priv_data->pressed_button,
			  	  	  	     (GdkEvent*) event,
			  	  	  	     event->x,
			  	  	  	     event->y);
	  set_context_data (context, path_list);

	  if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (widget),
					     priv_data->x,
					     priv_data->y,
					     &tree_path,
					     NULL,
					     &cell_x,
					     &cell_y))
	  {
		  cairo_surface_t *drag_icon;

		  drag_icon = gtk_tree_view_create_row_drag_icon (GTK_TREE_VIEW (widget), tree_path);

		  if (path_list->next != NULL) {

			  /* create a multi row drag icon */

			  const int        icon_offset = DRAG_ICON_OFFSET;
			  GdkRectangle     icon_extents;
			  cairo_surface_t *multi_drag_icon;
			  cairo_t         *cr;
			  int              n_icons, i, offset;

			  n_icons = MIN (DRAG_ICON_MAX_ROWS, g_list_length (path_list));
			  _gtk_cairo_surface_extents (drag_icon, &icon_extents);

			  multi_drag_icon = gdk_window_create_similar_surface (gtk_tree_view_get_bin_window (GTK_TREE_VIEW (widget)),
									       CAIRO_CONTENT_COLOR_ALPHA,
									       icon_extents.width + (icon_offset * (n_icons - 1)),
									       icon_extents.height + (icon_offset * (n_icons - 1)));

			  cr = cairo_create (multi_drag_icon);

			  cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.0);
			  cairo_rectangle(cr, 0, 0, icon_extents.width + icon_offset, icon_extents.height + icon_offset);
			  cairo_fill (cr);

			  offset = icon_offset * (n_icons - 1);
			  for (i = 0; i < n_icons; i++) {
				  cairo_set_source_surface (cr, drag_icon, -icon_extents.x + offset, -icon_extents.y + offset);
				  cairo_rectangle (cr, offset, offset, icon_extents.width, icon_extents.height);
				  cairo_fill (cr);
				  offset -= icon_offset;
			  }

			  cairo_destroy (cr);

			  cairo_surface_set_device_offset (multi_drag_icon, - (cell_x + 1), - (cell_y + 1));
			  gtk_drag_set_icon_surface (context, multi_drag_icon);

			  cairo_surface_destroy (multi_drag_icon);
		  }
		  else {
			  cairo_surface_set_device_offset (drag_icon, - (cell_x + 1), - (cell_y + 1));
			  gtk_drag_set_icon_surface (context, drag_icon);
		  }

		  cairo_surface_destroy (drag_icon);
		  gtk_tree_path_free (tree_path);
	  }
	  else
		  gtk_drag_set_icon_default (context);

	  gtk_target_list_unref (target_list);
	}
      else
	{
	  path_list_free (path_list);
	}
    }

  return TRUE;
}


static gboolean
egg_tree_multi_drag_button_press_event (GtkWidget      *widget,
					GdkEventButton *event,
					gpointer        data)
{
  GtkTreeView         *tree_view;
  GtkTreePath         *path = NULL;
  GtkTreeViewColumn   *column = NULL;
  gint                 cell_x, cell_y;
  GtkTreeSelection    *selection;
  EggTreeMultiDndData *priv_data;

  if (event->window != gtk_tree_view_get_bin_window (GTK_TREE_VIEW (widget)))
    return FALSE;

  if (event->button == 3)
    return FALSE;

  tree_view = GTK_TREE_VIEW (widget);
  priv_data = g_object_get_data (G_OBJECT (tree_view), EGG_TREE_MULTI_DND_STRING);
  if (priv_data == NULL)
    {
      priv_data = g_new0 (EggTreeMultiDndData, 1);
      priv_data->pending_event = FALSE;
      g_object_set_data (G_OBJECT (tree_view),
			 EGG_TREE_MULTI_DND_STRING,
			 priv_data);
    }

  if (g_slist_find (priv_data->event_list, event))
    return FALSE;

  if (priv_data->pending_event)
    {
      /* save the event to be propagated in order */
      priv_data->event_list = g_slist_append (priv_data->event_list,
					      gdk_event_copy ((GdkEvent*)event));
      return TRUE;
    }

  if (event->type == GDK_2BUTTON_PRESS)
    return FALSE;

  gtk_tree_view_get_path_at_pos (tree_view,
				 event->x, event->y,
				 &path, &column,
				 &cell_x, &cell_y);

  selection = gtk_tree_view_get_selection (tree_view);

  if (path)
    {
      gboolean call_parent = (event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK) ||
			      !gtk_tree_selection_path_is_selected (selection, path) ||
			      event->button != 1);

      /* calling the parent the button_release event could be emitted */
      priv_data->button_released = FALSE;
      if (call_parent)
	(GTK_WIDGET_GET_CLASS (tree_view))->button_press_event (widget, event);

      if (! priv_data->button_released && gtk_tree_selection_path_is_selected (selection, path))
    {
      priv_data->pressed_button = event->button;
      priv_data->x = event->x;
      priv_data->y = event->y;

      priv_data->pending_event = TRUE;
      if (!call_parent)
	priv_data->event_list = g_slist_append (priv_data->event_list,
						gdk_event_copy ((GdkEvent*)event));

      if (priv_data->motion_notify_handler == 0)
        {
          priv_data->motion_notify_handler =
	    g_signal_connect (G_OBJECT (tree_view),
	  		      "motion_notify_event",
			      G_CALLBACK (egg_tree_multi_drag_motion_event),
			      NULL);
        }

      if (priv_data->drag_data_get_handler == 0)
	{
	  priv_data->drag_data_get_handler =
	    g_signal_connect (G_OBJECT (tree_view),
			      "drag_data_get",
			      G_CALLBACK (egg_tree_multi_drag_drag_data_get),
			      NULL);
	}
    }

      priv_data->button_released = FALSE;
      gtk_tree_path_free (path);
      /* We called the default handler so we don't let the default handler run */
      return TRUE;
    }

  return FALSE;
}


void
egg_tree_multi_drag_add_drag_support (GtkTreeView *tree_view)
{
  g_return_if_fail (GTK_IS_TREE_VIEW (tree_view));
  g_signal_connect (G_OBJECT (tree_view),
		    "button_press_event",
		    G_CALLBACK (egg_tree_multi_drag_button_press_event),
		    NULL);
  g_signal_connect (G_OBJECT (tree_view),
		    "button_release_event",
		    G_CALLBACK (egg_tree_multi_drag_button_release_event),
		    NULL);
}
