#include <CtrlCore/CtrlCore.h>

#ifdef GUI_GTK

namespace Upp {

#define LLOG(x) // DLOG(x)

// --------------------------------------------------------------------------------------------

Ptr<Ctrl>                          Ctrl::dnd_source;
const VectorMap<String, ClipData> *Ctrl::dnd_source_data;
Vector<String>                     Ctrl::dnd_fmts;
int                                Ctrl::dnd_result;
Image                              Ctrl::dnd_icon;

Ctrl *Ctrl::GetDragAndDropSource()
{
	return dnd_source;
}

cairo_surface_t *CreateCairoSurface(const Image& img);

void Ctrl::GtkDragBegin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
	if(IsNull(dnd_icon))
		gtk_drag_set_icon_default(context);
	else {
		cairo_surface_t *surface = CreateCairoSurface(dnd_icon);
	#if GTK_CHECK_VERSION(3, 10, 0)
		cairo_surface_set_device_scale(surface, SCL(1), SCL(1));
	#endif
		cairo_surface_set_device_offset(surface, DPI(8), DPI(8));
		gtk_drag_set_icon_surface(context, surface);
		cairo_surface_destroy(surface);
	}
	LLOG("GtkDragBegin");
}

void Ctrl::GtkDragDelete(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
	LLOG("GtkDragDelete");
}

void Ctrl::GtkDragGetData(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data,
                          guint info, guint time, gpointer user_data)
{
	LLOG("GtkDragGetData");
	if(!dnd_source)
		return;
	if(info < (guint)dnd_source_data->GetCount())
		GtkSelectionDataSet(data, dnd_source_data->GetKey(info), (*dnd_source_data)[info].Render());
	else {
		info -= dnd_source_data->GetCount();
		if(info < (guint)dnd_fmts.GetCount()) {
			String fmt = dnd_fmts[info];
			GtkSelectionDataSet(data, fmt, dnd_source->GetDropData(fmt));
		}
	}
}

void Ctrl::GtkDragEnd(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
	LLOG("GtkDragEnd");
	dnd_result = DND_NONE;
	GdkDragAction a = gdk_drag_context_get_selected_action(context);
	dnd_result = a == GDK_ACTION_MOVE ? DND_MOVE : a == GDK_ACTION_COPY ? DND_COPY : DND_NONE;
	dnd_source = NULL;
}
                                                       
gboolean Ctrl::GtkDragFailed(GtkWidget *widget, GdkDragContext *context, GtkDragResult   result,
                             gpointer user_data)
{
	LLOG("GtkDragFailed");
	dnd_source = NULL;
	return FALSE;
}

Image MakeDragImage(const Image& arrow, Image sample);

Image MakeDragImage(const Image& arrow, const Image& arrow98, Image sample)
{
	return arrow98;
}

int Ctrl::DoDragAndDrop(const char *fmts, const Image& sample, dword actions,
                        const VectorMap<String, ClipData>& data)
{
	LLOG("------------------------------");
	LLOG("DoDragAndDrop " << fmts);
	TopWindow *w = GetTopWindow();
	if(!w || !w->top)
		return DND_NONE;
	int gdk_actions = 0;
	if(actions & DND_MOVE)
		gdk_actions |= GDK_ACTION_MOVE;
	if(actions & DND_COPY)
		gdk_actions |= GDK_ACTION_COPY;
	GtkTargetList *list = CreateTargetList(data);
	dnd_fmts.Clear();
	for(int i = 0; i < data.GetCount(); i++)
		AddFmt(list, data.GetKey(i), i);
	Vector<String> f = Split(fmts, ';');
	for(int i = 0; i < f.GetCount(); i++) {
		AddFmt(list, f[i], data.GetCount() + i);
		dnd_fmts.Add(f[i]);
	}
	dnd_source_data = &data;
	dnd_result = DND_NONE;
	dnd_source = this;
	if(IsNull(sample))
		dnd_icon = Null;
	else {
		Size sz = sample.GetSize();
		int maxsize = DPI(128);
		if(sz.cx > maxsize)
			sz.cx = maxsize;
		if(sz.cy > maxsize)
			sz.cy = maxsize;
		sz += 2;
		ImageDraw iw(sz);
		iw.DrawRect(sz, White());
		DrawFrame(iw, sz, Black());
		iw.DrawImage(1, 1, sz.cx, sz.cy, sample);
		dnd_icon = iw;
	}
	Top *top = w->GetTop();
	if(top)
	#if GTK_CHECK_VERSION(3, 10, 0)
		gtk_drag_begin_with_coordinates(top->window, list, GdkDragAction(gdk_actions),
		                                GetMouseLeft() ? 1 : GetMouseMiddle() ? 2 : 3,
		                                CurrentEvent.event, -1, -1);
	#else
		gtk_drag_begin(top->window, list, GdkDragAction(gdk_actions),
		               GetMouseLeft() ? 1 : GetMouseMiddle() ? 2 : 3, CurrentEvent.event);
	#endif
	while(dnd_source && GetTopCtrls().GetCount())
		ProcessEvents();
	dnd_source_data = NULL;
	gtk_target_list_unref (list);
	LLOG("-------- DoDragAndDrop");
	return dnd_result;
}

// --------------------------------------------------------------------------------------------

Index<String>   Ctrl::dnd_targets;
String          Ctrl::dnd_text_target;
String          Ctrl::dnd_image_target;
String          Ctrl::dnd_files_target;
GtkWidget      *Ctrl::dnd_widget;
GdkDragContext *Ctrl::dnd_context;
guint           Ctrl::dnd_time;
String          Ctrl::dnd_data;
String          Ctrl::dnd_data_fmt;
bool            Ctrl::dnd_data_wait;
bool            Ctrl::dnd_events;

Ctrl *Ctrl::DragWnd(gpointer user_data)
{
	if(dnd_data_wait) // Do nothing if waiting for data
		return NULL;
	dnd_events = true; // prevent mouse up messages to be issued
	ProcessEvents0(NULL, false); // process any events that are already fetched
	dnd_events = false;
	return GetTopCtrlFromId(user_data);
}

void ToIndex(GtkTargetList *target_list, Index<String>& ndx)
{
	gint n;
	GtkTargetEntry *t = gtk_target_table_new_from_list(target_list, &n);
	for(int i = 0; i < n; i++)
		ndx.Add(t[i].target);
	gtk_target_table_free(t, n);
	gtk_target_list_unref(target_list);
}

void Ctrl::DndTargets(GdkDragContext *context)
{
	static Index<String> text_targets;
	static Index<String> image_targets;
	static Index<String> files_targets;
	ONCELOCK {
		GtkTargetList *target_list = gtk_target_list_new (NULL, 0);
		gtk_target_list_add_text_targets(target_list, 0);
		ToIndex(target_list, text_targets);
		GtkTargetList *target_list2 = gtk_target_list_new (NULL, 0);
		gtk_target_list_add_image_targets(target_list2, 0, TRUE);
		ToIndex(target_list2, image_targets);
		GtkTargetList *target_list3 = gtk_target_list_new (NULL, 0);
		gtk_target_list_add_uri_targets(target_list3, 0);
		ToIndex(target_list3, files_targets);
	}
	dnd_targets.Clear();
	dnd_text_target.Clear();
	for(GList *list = gdk_drag_context_list_targets(context); list; list = g_list_next (list)) {
		String g = gdk_atom_name((GdkAtom)list->data);
		if(text_targets.Find(g) >= 0) {
			dnd_targets.FindAdd("text");
			if(dnd_text_target.IsEmpty())
				dnd_text_target = g;
		}
		else
		if(image_targets.Find(g) >= 0) {
			dnd_targets.FindAdd("image");
			if(dnd_image_target.IsEmpty())
				dnd_image_target = g;
		}
		else
		if(files_targets.Find(g) >= 0) {
			dnd_targets.FindAdd("files");
			if(dnd_files_target.IsEmpty())
				dnd_files_target = g;
		}
		else
			dnd_targets.FindAdd(g);
	}
}

void Ctrl::GtkDragDataReceived(GtkWidget *widget, GdkDragContext *context,
                               gint x, gint y, GtkSelectionData *data,
                               guint info, guint time, gpointer user_data)
{
	dnd_data_wait = false;
	if(dnd_data_fmt == "text") {
		guchar *s = gtk_selection_data_get_text(data);
		if(s) {
			dnd_data = (const char *)s;
			g_free(s);
		}
		else // seems gtk_selection_data_get_text sometime does not work as expected
		if(findarg(ToUpper(gdk_atom_name(gtk_selection_data_get_data_type(data))), "TEXT", "UTF8_STRING") >= 0)
			dnd_data = GtkDataGet(data);
	}
	else
	if(dnd_data_fmt == "image")
		dnd_data = ImageClipFromPixbufUnref(gtk_selection_data_get_pixbuf(data));
	else
	if(dnd_data_fmt == "files") {
		gchar **uris = gtk_selection_data_get_uris(data);
		if(uris)
			dnd_data = FilesClipFromUrisFree(uris);
		else {
			dnd_data.Clear();
			String s = GtkDataGet(data);
			StringStream ss(s);
			while(!ss.IsEof()) {
				String l = ss.GetLine();
				if(l.StartsWith("file://"))
					dnd_data << l << "\n";
			}
		}
	}
	else
		dnd_data = GtkDataGet(data);
}

bool Ctrl::IsDragAvailable(const char *fmt)
{
	LLOG("IsDragAvailable " << fmt << " " << (Ctrl::dnd_targets.Find(fmt) >= 0));
	return dnd_targets.Find(fmt) >= 0;
}

String Ctrl::DragGet(const char *fmt)
{
	LLOG("DragGet " << fmt << " " << (Ctrl::dnd_targets.Find(fmt) >= 0));
	if(Ctrl::dnd_targets.Find(fmt) < 0)
		return Null;
	dnd_data.Clear();
	dnd_data_wait = true;
	dnd_data_fmt = fmt;
	int t0 = msecs();
	gtk_drag_get_data(dnd_widget, dnd_context,
	                  GAtom(strcmp(fmt, "image") == 0 ? ~dnd_image_target :
	                        strcmp(fmt, "text") == 0 ? ~dnd_text_target :
	                        strcmp(fmt, "files") == 0 ? ~dnd_files_target : fmt),
	                  dnd_time);
	while(msecs() - t0 < 1000 && dnd_data_wait)
		FetchEvents(true);
	LLOG("DragGet data length " << dnd_data.GetLength());
	return dnd_data;
}

PasteClip Ctrl::GtkDnd(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
                       guint time, gpointer user_data, bool paste)
{
	DndTargets(context);
	g_object_ref(context); // make sure these always survive the action...
	g_object_ref(widget);
	dnd_context = context;
	dnd_widget = widget;
	dnd_time = time;
	PasteClip clip;
	clip.type = 1;
	clip.paste = paste;
	clip.accepted = false;
	clip.allowed = DND_MOVE|DND_COPY;
	GdkModifierType mod;
	GetMouseInfo(gdk_get_default_root_window(), mod);
	clip.action = mod & GDK_CONTROL_MASK ? DND_COPY : DND_MOVE;
	Ctrl *w = DragWnd(user_data);
	if(w) {
		GetMouseInfo(gdk_get_default_root_window(), mod);
		CurrentState = mod;
		CurrentMousePos = Point(SCL(x), SCL(y));
		int x1, y1;
		gdk_window_get_position(w->gdk(), &x1, &y1);
		CurrentMousePos += Point(SCL(x1), SCL(y1));
		w->DnD(CurrentMousePos, clip);
	}
	gdk_drag_status(context, clip.IsAccepted() ? clip.GetAction() == DND_MOVE ? GDK_ACTION_MOVE
	                                                                          : GDK_ACTION_COPY
	                                           : GdkDragAction(0), time);
	return clip;
}

gboolean Ctrl::GtkDragMotion(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
                             guint time, gpointer user_data)
{
	LLOG("GtkDragMotion");

	PasteClip clip = GtkDnd(widget, context, x, y, time, user_data, false);
	g_object_unref(widget);
	g_object_unref(context);
	return TRUE;
}

void Ctrl::GtkDragLeave(GtkWidget *widget, GdkDragContext *context, guint time, gpointer user_data)
{
	LLOG("GtkDragLeave");
	DnDLeave();
}

gboolean Ctrl::GtkDragDrop(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
                           guint time, gpointer user_data)
{
	LLOG("GtkDragDrop");

	PasteClip clip = GtkDnd(widget, context, x, y, time, user_data, true);
	gtk_drag_finish(context, clip.IsAccepted(), clip.IsAccepted() && clip.GetAction() == DND_MOVE, time);
	g_object_unref(widget);
	g_object_unref(context);
	DnDLeave();
	return TRUE;
}

// -----------------------------------------------------------------------------------------

void Ctrl::DndInit()
{
	Top *top = GetTop();
	if(!top)
		return;
	GtkWidget *w = top->window;
	gpointer id = (gpointer)(uintptr_t)top->id;
	g_signal_connect(w, "drag-begin", G_CALLBACK(GtkDragBegin), id);
	g_signal_connect(w, "drag-data-delete", G_CALLBACK(GtkDragDelete), id);
	g_signal_connect(w, "drag-data-get", G_CALLBACK(GtkDragGetData), id);
	g_signal_connect(w, "drag-data-received", G_CALLBACK(GtkDragDataReceived), id);
	g_signal_connect(w, "drag-drop", G_CALLBACK(GtkDragDrop), id);
	g_signal_connect(w, "drag-end", G_CALLBACK(GtkDragEnd), id);
	g_signal_connect(w, "drag-failed", G_CALLBACK(GtkDragFailed), id);
	g_signal_connect(w, "drag-leave", G_CALLBACK(GtkDragLeave), id);
	g_signal_connect(w, "drag-motion", G_CALLBACK(GtkDragMotion), id);
	gtk_drag_dest_set(w, GtkDestDefaults(0), NULL, 0, GdkDragAction(GDK_ACTION_COPY|GDK_ACTION_MOVE));
}

void Ctrl::DndExit()
{
	Top *top = GetTop();
	if(top)
		gtk_drag_dest_unset(top->window);
}

}

#endif

