/*************************************************************************
 * Copyright (c) 2011 AT&T Intellectual Property 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
 *
 * Contributors: Details at https://graphviz.org
 *************************************************************************/

#include "config.h"

#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <math.h>

#include <gvc/gvplugin_layout.h>
#include <gvc/gvcint.h>
#include <gvc/gvcproc.h>
#include <common/utils.h>
#include <util/gv_fopen.h>

extern void emit_graph(GVJ_t * job, graph_t * g);
extern int gvLayout(GVC_t *gvc, graph_t *g, const char *engine);
extern int gvRenderFilename(GVC_t *gvc, graph_t *g, const char *format, const char *filename);
extern void graph_cleanup(graph_t *g);

#define PANFACTOR 10
#define ZOOMFACTOR 1.1
#define EPSILON .0001

static char *s_tooltip = "tooltip";
static char *s_href = "href";
static char *s_URL = "URL";

static void gv_graph_state(GVJ_t *job, graph_t *g)
{
    Agsym_t *a = agfindgraphattr(g, s_href);
    if (!a)
	a = agfindgraphattr(g, s_URL);
    if (a)
	job->selected_href = strdup_and_subst_obj(agxget(g, a), g);
}

static void gv_node_state(GVJ_t *job, node_t *n)
{
    Agsym_t *a = agfindnodeattr(agraphof(n), s_href);
    if (!a)
        a = agfindnodeattr(agraphof(n), s_URL);
    if (a)
	job->selected_href = strdup_and_subst_obj(agxget(n, a), n);
}

static void gv_edge_state(GVJ_t *job, edge_t *e)
{
    Agsym_t *a = agfindedgeattr(agraphof(aghead(e)), s_href);
    if (!a)
	a = agfindedgeattr(agraphof(aghead(e)), s_URL);
    if (a)
	job->selected_href = strdup_and_subst_obj(agxget(e, a), e);
}

static void gvevent_refresh(GVJ_t * job)
{
    graph_t *g = job->gvc->g;

    if (!job->selected_obj) {
	job->selected_obj = g;
	GD_gui_state(g) |= GUI_STATE_SELECTED;
	gv_graph_state(job, g);
    }
    emit_graph(job, g);
    job->has_been_rendered = true;
}

/* recursively find innermost cluster containing the point */
static graph_t *gvevent_find_cluster(graph_t *g, boxf b)
{
    int i;
    graph_t *sg;
    boxf bb;

    for (i = 1; i <= GD_n_cluster(g); i++) {
	sg = gvevent_find_cluster(GD_clust(g)[i], b);
	if (sg)
	    return sg;
    }
    B2BF(GD_bb(g), bb);
    if (OVERLAP(b, bb))
	return g;
    return NULL;
}

static void * gvevent_find_obj(graph_t *g, boxf b)
{
    graph_t *sg;
    node_t *n;
    edge_t *e;

    /* edges might overlap nodes, so search them first */
    for (n = agfstnode(g); n; n = agnxtnode(g, n))
	for (e = agfstout(g, n); e; e = agnxtout(g, e))
	    if (overlap_edge(e, b))
	        return e;
    /* search graph backwards to get topmost node, in case of overlap */
    for (n = aglstnode(g); n; n = agprvnode(g, n))
	if (overlap_node(n, b))
	    return n;
    /* search for innermost cluster */
    sg = gvevent_find_cluster(g, b);
    if (sg)
	return sg;

    /* otherwise - we're always in the graph */
    return g;
}

static void gvevent_leave_obj(GVJ_t * job)
{
    void *obj = job->current_obj;

    if (obj) {
        switch (agobjkind(obj)) {
        case AGRAPH:
	    GD_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
	    break;
        case AGNODE:
	    ND_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
	    break;
        case AGEDGE:
	    ED_gui_state(obj) &= (unsigned char)~GUI_STATE_ACTIVE;
	    break;
        }
    }
    job->active_tooltip = NULL;
}

static void gvevent_enter_obj(GVJ_t * job)
{
    void *obj;
    graph_t *g;
    edge_t *e;
    node_t *n;
    Agsym_t *a;

    free(job->active_tooltip);
    job->active_tooltip = NULL;
    obj = job->current_obj;
    if (obj) {
        switch (agobjkind(obj)) {
        case AGRAPH:
	    g = obj;
	    GD_gui_state(g) |= GUI_STATE_ACTIVE;
	    a = agfindgraphattr(g, s_tooltip);
	    if (a)
		job->active_tooltip = strdup_and_subst_obj(agxget(g, a), obj);
	    break;
        case AGNODE:
	    n = obj;
	    ND_gui_state(n) |= GUI_STATE_ACTIVE;
	    a = agfindnodeattr(agraphof(n), s_tooltip);
	    if (a)
		job->active_tooltip = strdup_and_subst_obj(agxget(n, a), obj);
	    break;
        case AGEDGE:
	    e = obj;
	    ED_gui_state(e) |= GUI_STATE_ACTIVE;
	    a = agfindedgeattr(agraphof(aghead(e)), s_tooltip);
	    if (a)
		job->active_tooltip = strdup_and_subst_obj(agxget(e, a), obj);
	    break;
        }
    }
}

static pointf pointer2graph (GVJ_t *job, pointf pointer)
{
    pointf p;

    /* transform position in device units to position in graph units */
    if (job->rotation) {
        p.x = pointer.y / (job->zoom * job->devscale.y) - job->translation.x;
        p.y = -pointer.x / (job->zoom * job->devscale.x) - job->translation.y;
    }
    else {
        p.x = pointer.x / (job->zoom * job->devscale.x) - job->translation.x;
        p.y = pointer.y / (job->zoom * job->devscale.y) - job->translation.y;
    }
    return p;
}

/* CLOSEENOUGH is in 1/72 - probably should be a feature... */
#define CLOSEENOUGH 1

static void gvevent_find_current_obj(GVJ_t * job, pointf pointer)
{
    void *obj;
    boxf b;
    double closeenough;
    pointf p;

    p =  pointer2graph (job, pointer);

    /* convert window point to graph coordinates */
    closeenough = CLOSEENOUGH / job->zoom;

    b.UR.x = p.x + closeenough;
    b.UR.y = p.y + closeenough;
    b.LL.x = p.x - closeenough;
    b.LL.y = p.y - closeenough;

    obj = gvevent_find_obj(job->gvc->g, b);
    if (obj != job->current_obj) {
	gvevent_leave_obj(job);
	job->current_obj = obj;
	gvevent_enter_obj(job);
	job->needs_refresh = true;
    }
}

static void gvevent_select_current_obj(GVJ_t * job)
{
    void *obj;

    obj = job->selected_obj;
    if (obj) {
        switch (agobjkind(obj)) {
        case AGRAPH:
	    GD_gui_state(obj) |= GUI_STATE_VISITED;
	    GD_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
	    break;
        case AGNODE:
	    ND_gui_state(obj) |= GUI_STATE_VISITED;
	    ND_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
	    break;
        case AGEDGE:
	    ED_gui_state(obj) |= GUI_STATE_VISITED;
	    ED_gui_state(obj) &= (unsigned char)~GUI_STATE_SELECTED;
	    break;
        }
    }

    free(job->selected_href);
    job->selected_href = NULL;

    obj = job->selected_obj = job->current_obj;
    if (obj) {
        switch (agobjkind(obj)) {
        case AGRAPH:
	    GD_gui_state(obj) |= GUI_STATE_SELECTED;
	    gv_graph_state(job, obj);
	    break;
        case AGNODE:
	    ND_gui_state(obj) |= GUI_STATE_SELECTED;
	    gv_node_state(job, obj);
	    break;
        case AGEDGE:
	    ED_gui_state(obj) |= GUI_STATE_SELECTED;
	    gv_edge_state(job, obj);
	    break;
        }
    }
}

static void gvevent_button_press(GVJ_t * job, int button, pointf pointer)
{
    switch (button) {
    case 1: /* select / create in edit mode */
	gvevent_find_current_obj(job, pointer);
	gvevent_select_current_obj(job);
        job->click = true;
	job->button = (unsigned char)button;
	job->needs_refresh = true;
	break;
    case 2: /* pan */
        job->click = true;
	job->button = (unsigned char)button;
	job->needs_refresh = true;
	break;
    case 3: /* insert node or edge */
	gvevent_find_current_obj(job, pointer);
        job->click = true;
	job->button = (unsigned char)button;
	job->needs_refresh = true;
	break;
    case 4:
        /* scrollwheel zoom in at current mouse x,y */
/* FIXME - should code window 0,0 point as feature with Y_GOES_DOWN */
        job->fit_mode = false;
        if (job->rotation) {
            job->focus.x -= (pointer.y - job->height / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
            job->focus.y += (pointer.x - job->width / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
        }
        else {
            job->focus.x += (pointer.x - job->width / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
            job->focus.y += (pointer.y - job->height / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
        }
        job->zoom *= ZOOMFACTOR;
        job->needs_refresh = true;
        break;
    case 5: /* scrollwheel zoom out at current mouse x,y */
        job->fit_mode = false;
        job->zoom /= ZOOMFACTOR;
        if (job->rotation) {
            job->focus.x += (pointer.y - job->height / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
            job->focus.y -= (pointer.x - job->width / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
        }
        else {
            job->focus.x -= (pointer.x - job->width / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.x);
            job->focus.y -= (pointer.y - job->height / 2.)
                    * (ZOOMFACTOR - 1.) / (job->zoom * job->devscale.y);
        }
        job->needs_refresh = true;
        break;
    }
    job->oldpointer = pointer;
}

static void gvevent_button_release(GVJ_t *job, int button, pointf pointer)
{
    (void)button;
    (void)pointer;

    job->click = false;
    job->button = false;
}

static void gvevent_motion(GVJ_t * job, pointf pointer)
{
    /* dx,dy change in position, in device independent points */
    double dx = (pointer.x - job->oldpointer.x) / job->devscale.x;
    double dy = (pointer.y - job->oldpointer.y) / job->devscale.y;

    if (fabs(dx) < EPSILON && fabs(dy) < EPSILON)  /* ignore motion events with no motion */
	return;

    switch (job->button) {
    case 0: /* drag with no button - */
	gvevent_find_current_obj(job, pointer);
	break;
    case 1: /* drag with button 1 - drag object */
	/* FIXME - to be implemented */
	break;
    case 2: /* drag with button 2 - pan graph */
	if (job->rotation) {
	    job->focus.x -= dy / job->zoom;
	    job->focus.y += dx / job->zoom;
	}
	else {
	    job->focus.x -= dx / job->zoom;
	    job->focus.y -= dy / job->zoom;
	}
	job->needs_refresh = true;
	break;
    case 3: /* drag with button 3 - drag inserted node or uncompleted edge */
	break;
    }
    job->oldpointer = pointer;
}

static int quit_cb(GVJ_t * job)
{
    (void)job;

    return 1;
}

static int left_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->focus.x += PANFACTOR / job->zoom;
    job->needs_refresh = true;
    return 0;
}

static int right_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->focus.x -= PANFACTOR / job->zoom;
    job->needs_refresh = true;
    return 0;
}

static int up_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->focus.y += -(PANFACTOR / job->zoom);
    job->needs_refresh = true;
    return 0;
}

static int down_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->focus.y -= -(PANFACTOR / job->zoom);
    job->needs_refresh = true;
    return 0;
}

static int zoom_in_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->zoom *= ZOOMFACTOR;
    job->needs_refresh = true;
    return 0;
}

static int zoom_out_cb(GVJ_t * job)
{
    job->fit_mode = false;
    job->zoom /= ZOOMFACTOR;
    job->needs_refresh = true;
    return 0;
}

static int toggle_fit_cb(GVJ_t * job)
{
/*FIXME - should allow for margins */
/*      - similar zoom_to_fit code exists in: */
/*      plugin/xlib/gvdevice_xlib.c */
/*      lib/gvc/gvevent.c */

    job->fit_mode = !job->fit_mode;
    if (job->fit_mode) {
	/* FIXME - this code looks wrong */
	int dflt_width, dflt_height;
	dflt_width = job->width;
	dflt_height = job->height;
	job->zoom =
	    MIN((double) job->width / (double) dflt_width,
		(double) job->height / (double) dflt_height);
	job->focus.x = 0.0;
	job->focus.y = 0.0;
	job->needs_refresh = true;
    }
    return 0;
}

static void gvevent_read (GVJ_t * job, const char *filename, const char *layout)
{
    FILE *f;
    GVC_t *gvc;
    Agraph_t *g = NULL;
    gvlayout_engine_t *gvle;

    gvc = job->gvc;
    if (!filename) {
	g = agread(stdin,NULL);  // continue processing stdin
    }
    else {
	f = gv_fopen(filename, "r");
	if (!f)
	   return;   /* FIXME - need some error handling */
	g = agread(f,NULL);
	fclose(f);
    }

    if (!g)
	return;   /* FIXME - need some error handling */

    if (gvc->g) {
	gvle = gvc->layout.engine;
	if (gvle && gvle->cleanup)
	    gvle->cleanup(gvc->g);
	graph_cleanup(gvc->g);
	agclose(gvc->g);
    }

    aginit (g, AGRAPH, "Agraphinfo_t", sizeof(Agraphinfo_t), true);
    aginit (g, AGNODE, "Agnodeinfo_t", sizeof(Agnodeinfo_t), true);
    aginit (g, AGEDGE, "Agedgeinfo_t", sizeof(Agedgeinfo_t), true);
    gvc->g = g;
    GD_gvc(g) = gvc;
    if (gvLayout(gvc, g, layout) == -1)
	return;   /* FIXME - need some error handling */
    job->selected_obj = NULL;
    job->current_obj = NULL;
    job->needs_refresh = true;
}

static void gvevent_layout (GVJ_t * job, const char *layout)
{
    gvLayout(job->gvc, job->gvc->g, layout);
}

static void gvevent_render (GVJ_t * job, const char *format, const char *filename)
{
/* If gvc->jobs is set, a new job for doing the rendering won't be created.
 * If gvc->active_jobs is set, this will be used in a call to gv_end_job.
 * If we assume this function is called by an interactive front-end which
 * actually wants to write a file, the above possibilities can cause problems,
 * with either gvc->job being NULL or the creation of a new window. To avoid 
 * this, we null out these values for rendering the file, and restore them
 * afterwards. John may have a better way around this.
 */
    GVJ_t* save_jobs;
    GVJ_t* save_active;
    if (job->gvc->jobs && (job->gvc->job == NULL)) {
	save_jobs = job->gvc->jobs;
	save_active = job->gvc->active_jobs;
	job->gvc->active_jobs = job->gvc->jobs = NULL;
    }
    else
	save_jobs = NULL;
    gvRenderFilename(job->gvc, job->gvc->g, format, filename);
    if (save_jobs) {
	job->gvc->jobs = save_jobs;
	job->gvc->active_jobs = save_active;
    }
}


gvevent_key_binding_t gvevent_key_binding[] = {
    {"Q", quit_cb},
    {"Left", left_cb},
    {"KP_Left", left_cb},
    {"Right", right_cb},
    {"KP_Right", right_cb},
    {"Up", up_cb},
    {"KP_Up", up_cb},
    {"Down", down_cb},
    {"KP_Down", down_cb},
    {"plus", zoom_in_cb},
    {"KP_Add", zoom_in_cb},
    {"minus", zoom_out_cb},
    {"KP_Subtract", zoom_out_cb},
    {"F", toggle_fit_cb},
};

const size_t gvevent_key_binding_size =
  sizeof(gvevent_key_binding) / sizeof(gvevent_key_binding[0]);

gvdevice_callbacks_t gvdevice_callbacks = {
    gvevent_refresh,
    gvevent_button_press,
    gvevent_button_release,
    gvevent_motion,
    NULL, // modify
    NULL, // del
    gvevent_read,
    gvevent_layout,
    gvevent_render,
};
