/*
 * gui_canvas.c - GUI, canvas
 *
 * Written 2009, 2010, 2012 by Werner Almesberger
 * Copyright 2009, 2010, 2012 by Werner Almesberger
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 */


#include <math.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "obj.h"
#include "delete.h"
#include "inst.h"
#include "gui_util.h"
#include "gui_inst.h"
#include "gui_style.h"
#include "gui_status.h"
#include "gui_tool.h"
#include "gui.h"
#include "gui_frame_drag.h"
#include "gui_canvas.h"


#if 0
#define	DPRINTF(fmt, ...)	fprintf(stderr, fmt "\n", ##__VA_ARGS__)
#else
#define	DPRINTF(fmt, ...)
#endif


void (*highlight)(void) = NULL;

static struct coord curr_pos; /* canvas coordinates ! */
static struct coord user_origin = { 0, 0 };

static int dragging = 0;
static int drag_escaped = 0; /* 1 once we've made it out of the drag radius */
static struct coord drag_start;
static struct inst *selected_before_drag;
    /* instance selected before dragging. we use it to do the click-to-select
       routine in case we later find out the drag was really just a click. */


/* ----- status display ---------------------------------------------------- */


static void update_zoom(void)
{
	status_set_zoom("Zoom factor", "x%d", draw_ctx.scale);
}


static void update_pos(struct coord pos)
{
	struct coord user;
	unit_type diag;

	set_with_units(status_set_sys_x, "X ", pos.x, "Absolute X position");
	set_with_units(status_set_sys_y, "Y ", pos.y, "Absolute Y position");

	user.x = pos.x-user_origin.x;
	user.y = pos.y-user_origin.y;
	set_with_units(status_set_user_x, "x ", user.x,
	    "User X position. Press SPACE to zero.");
	set_with_units(status_set_user_y, "y ", user.y,
	    "User Y position. Press SPACE to zero.");

	if (!selected_inst) {
		diag = hypot(user.x, user.y);
		set_with_units(status_set_r, "r = ", diag,
		    "Distance from user origin");
		status_set_angle_xy("Angle from user origin", user);
	}
}


void refresh_pos(void)
{
	update_pos(canvas_to_coord(curr_pos.x, curr_pos.y));
}


/* ----- coordinate system ------------------------------------------------- */


static void center(const struct bbox *this_bbox)
{
	struct bbox bbox;

	bbox = this_bbox ? *this_bbox : inst_get_bbox(NULL);
	draw_ctx.center.x = (bbox.min.x+bbox.max.x)/2;
	draw_ctx.center.y = (bbox.min.y+bbox.max.y)/2;
}


static void auto_scale(const struct bbox *this_bbox)
{
	struct bbox bbox;
	unit_type h, w;
	int sx, sy;
	float aw, ah;

	bbox = this_bbox ? *this_bbox : inst_get_bbox(NULL);
	aw = draw_ctx.widget->allocation.width;
	ah = draw_ctx.widget->allocation.height;
	h = bbox.max.x-bbox.min.x;
	w = bbox.max.y-bbox.min.y;
	aw -= 2*CANVAS_CLEARANCE;
	ah -= 2*CANVAS_CLEARANCE;
	if (aw < 1)
		aw = 1;
	if (ah < 1)
		ah = 1;
	sx = ceil(h/aw);
	sy = ceil(w/ah);
	draw_ctx.scale = sx > sy ? sx : sy > 0 ? sy : 1;

	update_zoom();
}


/* ----- drawing ----------------------------------------------------------- */


void redraw(void)
{
	float aw, ah;

	aw = draw_ctx.widget->allocation.width;
	ah = draw_ctx.widget->allocation.height;
	gdk_draw_rectangle(draw_ctx.widget->window,
	    instantiation_error ? gc_bg_error : gc_bg, TRUE, 0, 0, aw, ah);

	DPRINTF("--- redraw: inst_draw ---");
	inst_draw();
	if (highlight)
		highlight();
	DPRINTF("--- redraw: tool_redraw ---");
	tool_redraw();
	DPRINTF("--- redraw: done ---");
}


/* ----- drag -------------------------------------------------------------- */


static void drag_left(struct coord pos)
{
	if (!dragging)
		return;
	if (!drag_escaped &&
	    hypot(pos.x-drag_start.x, pos.y-drag_start.y)/draw_ctx.scale <
	    DRAG_MIN_R)
		return;
	drag_escaped = 1;
	tool_drag(pos);
}


static void drag_middle(struct coord pos)
{
}


static gboolean motion_notify_event(GtkWidget *widget, GdkEventMotion *event,
    gpointer data)
{
	struct coord pos = canvas_to_coord(event->x, event->y);

	DPRINTF("--- motion ---");
	curr_pos.x = event->x;
	curr_pos.y = event->y;
	tool_hover(pos);
	if (event->state & GDK_BUTTON1_MASK)
		drag_left(pos);
	if (event->state & GDK_BUTTON2_MASK)
		drag_middle(pos);
	update_pos(pos);
	return FALSE;
}


/* ----- drag and drop (frame to canvas) ----------------------------------- */


void canvas_frame_begin(struct frame *frame)
{
	inst_deselect(); /* don't drag away bits of the selected object */
	redraw();
	tool_push_frame(frame);
}


int canvas_frame_motion(struct frame *frame, int x, int y)
{
	struct coord pos = canvas_to_coord(x, y);

	return tool_hover(pos);
}


void canvas_frame_end(void)
{
	tool_dehover();
	tool_pop_frame();
}


int canvas_frame_drop(struct frame *frame, int x, int y)
{
	struct coord pos = canvas_to_coord(x, y);

	if (!tool_place_frame(frame, pos))
		return FALSE;
	change_world();
	return TRUE;
}


/* ----- button press and release ------------------------------------------ */


static void click_to_select(struct coord pos)
{
	const struct inst *prev;

	tool_reset();
	prev = selected_inst;
	inst_select(pos);
	if (prev != selected_inst)
		redraw();
}


static gboolean button_press_event(GtkWidget *widget, GdkEventButton *event,
    gpointer data)
{
	struct coord pos = canvas_to_coord(event->x, event->y);
	int res;

	DPRINTF("--- button press ---");
	gtk_widget_grab_focus(widget);
	switch (event->button) {
	case 1:
		if (dragging) {
			fprintf(stderr, "HUH ?!?\n");
			tool_cancel_drag();
			dragging = 0;
		}
		res = tool_consider_drag(pos);
		/* tool doesn't do drag */
		if (res < 0) {
			change_world();
			inst_deselect();
			break;
		}
		if (res) {
			selected_before_drag = selected_inst;
			inst_deselect();
			redraw();
			dragging = 1;
			drag_escaped = 0;
			drag_start = pos;
			break;
		}
		click_to_select(pos);
		break;
	case 2:
		tool_dehover();
		draw_ctx.center = pos;
		redraw();
		tool_hover(canvas_to_coord(event->x, event->y));
		break;
	}
	return TRUE;
}


static gboolean button_release_event(GtkWidget *widget, GdkEventButton *event,
    gpointer data)
{
	struct coord pos = canvas_to_coord(event->x, event->y);

	DPRINTF("--- button release ---");
	switch (event->button) {
	case 1:
		if (is_dragging_anything())
			return FALSE;
		if (!dragging)
			break;
		drag_left(pos);
		dragging = 0;
		if (!drag_escaped) {
			tool_cancel_drag();
			selected_inst = selected_before_drag;
			click_to_select(pos);
			break;
		}
		if (tool_end_drag(pos))
			change_world();
		break;
	}
	return TRUE;
}


/* ----- zoom control ------------------------------------------------------ */


static void zoom_in(struct coord pos)
{
	if (draw_ctx.scale < 2)
		return;
	tool_dehover();
	draw_ctx.scale /= 2;
	draw_ctx.center.x = (draw_ctx.center.x+pos.x)/2;
	draw_ctx.center.y = (draw_ctx.center.y+pos.y)/2;
	update_zoom();
	redraw();
	tool_hover(pos);
}


static void zoom_out(struct coord pos)
{
	struct bbox bbox;

	bbox = inst_get_bbox(NULL);
	bbox.min = translate(bbox.min);
	bbox.max = translate(bbox.max);
	if (bbox.min.x >= ZOOM_STOP_BORDER &&
	    bbox.max.y >= ZOOM_STOP_BORDER &&
	    bbox.max.x < draw_ctx.widget->allocation.width-ZOOM_STOP_BORDER &&
	    bbox.min.y < draw_ctx.widget->allocation.height-ZOOM_STOP_BORDER)
		return;
	tool_dehover();
	draw_ctx.scale *= 2;
	draw_ctx.center.x = 2*draw_ctx.center.x-pos.x;
	draw_ctx.center.y = 2*draw_ctx.center.y-pos.y;
	update_zoom();
	redraw();
	tool_hover(pos);
}


void zoom_in_center(void)
{
	zoom_in(draw_ctx.center);
}


void zoom_out_center(void)
{
	zoom_out(draw_ctx.center);
}


void zoom_to_frame(void)
{
	tool_dehover();
	center(&active_frame_bbox);
	auto_scale(&active_frame_bbox);
	redraw();
	tool_hover(canvas_to_coord(curr_pos.x, curr_pos.y));
}


void zoom_to_extents(void)
{
	tool_dehover();
	center(NULL);
	auto_scale(NULL);
	redraw();
	tool_hover(canvas_to_coord(curr_pos.x, curr_pos.y));
}


static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *event,
    gpointer data)
{
	struct coord pos = canvas_to_coord(event->x, event->y);

	gtk_widget_grab_focus(widget);
	switch (event->direction) {
	case GDK_SCROLL_UP:
		zoom_in(pos);
		break;
	case GDK_SCROLL_DOWN:
		zoom_out(pos);
		break;
	default:
		/* ignore */;
	}
	return TRUE;
}


/* ----- keys -------------------------------------------------------------- */


static gboolean key_press_event(GtkWidget *widget, GdkEventKey *event,
    gpointer data)
{
	struct coord pos = canvas_to_coord(curr_pos.x, curr_pos.y);

	DPRINTF("--- key press ---");
	switch (event->keyval) {
	case ' ':
		user_origin = pos;
		update_pos(pos);
		break;
	case '+':
	case '=':
		zoom_in(pos);
		break;
	case '-':
		zoom_out(pos);
		break;
	case '*':
		zoom_to_extents();
		break;
	case '#':
		zoom_to_frame();
		break;
	case '.':
		tool_dehover();
		draw_ctx.center = pos;
		redraw();
		tool_hover(canvas_to_coord(curr_pos.x, curr_pos.y));
		break;
	case GDK_BackSpace:
	case GDK_Delete:
#if 0
	case GDK_KP_Delete:
		if (selected_inst) {
			inst_delete(selected_inst);
			change_world();
		}
		break;
#endif
	case 'u':
		if (undelete())
			change_world();
		break;
	case '/':
{
/* @@@ find a better place for this */
extern int show_vars;
		show_vars = !show_vars;
change_world();
}
	}
	return TRUE;
}


/* ----- expose event ------------------------------------------------------ */


static gboolean expose_event(GtkWidget *widget, GdkEventExpose *event,
    gpointer data)
{
	static int first = 1;

	DPRINTF("--- expose ---");
	if (first) {
		init_canvas();
		first = 0;
	}
	tool_dehover();
	redraw();
	return TRUE;
}


/* ----- enter/leave ------------------------------------------------------- */


static gboolean enter_notify_event(GtkWidget *widget, GdkEventCrossing *event,
    gpointer data)
{
	DPRINTF("--- enter ---");
	gtk_widget_grab_focus(widget);
	return FALSE;
}


static gboolean leave_notify_event(GtkWidget *widget, GdkEventCrossing *event,
    gpointer data)
{
	DPRINTF("--- leave ---");
	if (dragging)
		tool_cancel_drag();
	tool_dehover();
	dragging = 0;
	return FALSE;
}


/* ----- tooltip ----------------------------------------------------------- */


static gboolean canvas_tooltip(GtkWidget *widget, gint x, gint y,
    gboolean keyboard_mode, GtkTooltip *tooltip, gpointer user_data)
{
	struct coord pos = canvas_to_coord(x, y);
	const char *res;

	res = tool_tip(pos);
	if (!res)
		return FALSE;
	gtk_tooltip_set_markup(tooltip, res);
	return TRUE;
}


/* ----- canvas setup ------------------------------------------------------ */


/*
 * Note that we call init_canvas twice: first to make sure we'll make it safely
 * through select_frame, and the second time to set the geometry for the actual
 * screen.
 */

void init_canvas(void)
{
	center(NULL);
	auto_scale(NULL);
}


GtkWidget *make_canvas(void)
{
	GtkWidget *canvas;
	GdkColor black = { 0, 0, 0, 0 };

	/* Canvas */

	canvas = gtk_drawing_area_new();
	gtk_widget_modify_bg(canvas, GTK_STATE_NORMAL, &black);

	g_signal_connect(G_OBJECT(canvas), "motion_notify_event",
	    G_CALLBACK(motion_notify_event), NULL);
	g_signal_connect(G_OBJECT(canvas), "button_press_event",
	    G_CALLBACK(button_press_event), NULL);
	g_signal_connect(G_OBJECT(canvas), "button_release_event",
	    G_CALLBACK(button_release_event), NULL);
	g_signal_connect(G_OBJECT(canvas), "scroll_event",
	    G_CALLBACK(scroll_event), NULL);

	GTK_WIDGET_SET_FLAGS(canvas, GTK_CAN_FOCUS);

	g_signal_connect(G_OBJECT(canvas), "key_press_event",
	    G_CALLBACK(key_press_event), NULL);

	g_signal_connect(G_OBJECT(canvas), "expose_event",
	    G_CALLBACK(expose_event), NULL);
	g_signal_connect(G_OBJECT(canvas), "enter_notify_event",
	    G_CALLBACK(enter_notify_event), NULL);
	g_signal_connect(G_OBJECT(canvas), "leave_notify_event",
	    G_CALLBACK(leave_notify_event), NULL);

	gtk_widget_set(canvas, "has-tooltip", TRUE, NULL);
	g_signal_connect(G_OBJECT(canvas), "query_tooltip",
	    G_CALLBACK(canvas_tooltip), NULL);

	gtk_widget_set_events(canvas,
	    GDK_EXPOSE | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK |
	    GDK_KEY_PRESS_MASK |
	    GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
	    GDK_SCROLL |
	    GDK_POINTER_MOTION_MASK);

	gtk_widget_set_double_buffered(canvas, FALSE);

	setup_canvas_drag(canvas);

	draw_ctx.widget = canvas;

	return canvas;
}
