/*
    This software may only be used by you under license from AT&T Corp.
    ("AT&T").  A copy of AT&T's Source Code Agreement is available at
    AT&T's Internet website having the URL:
    <http://www.research.att.com/sw/tools/graphviz/license/source.html>
    If you received this software without first entering into a license
    with AT&T, you have an infringing copy of this software and cannot use
    it without violating AT&T's intellectual property rights.
*/

#include <aghdr.h>

#ifdef DMALLOC
#include "dmalloc.h"
#endif

static char DRName[] = "_AG_pending";

typedef struct symlist_s {Agsym_t *sym; struct symlist_s *link;} symlist_t;

/* this record describes one pending callback on one object */
typedef struct {
	Dtlink_t	link;
	unsigned long	key;		/* universal key for main or sub-object */
	Agobj_t		*obj;		/* may be a subgraph obj */ 
					/* i'm sure there are more elegant approaches,but*/
	Agraph_t	*g_alloc;	/* save this, because object may become dead */
	symlist_t	*symlist;	/* attributes involved */
} pending_cb_t;

typedef struct {
	Agrec_t		h;
	struct {Dict_t *g,*n,*e;} ins,mod,del;
} pendingset_t;

static void free_symlist(pending_cb_t *pcb)
{
	symlist_t		*s,*t;

	for (s = pcb->symlist; s; s = t) {
		t = s->link;
		agfree(pcb->g_alloc,s);
	}
}

static void freef(Dict_t *dict, void *ptr, Dtdisc_t *disc)
{
	pending_cb_t	*pcb;

	NOTUSED(dict);
	NOTUSED(disc);
	pcb = ptr;
	free_symlist(pcb);
	agfree(pcb->g_alloc,pcb);
}

static Dtdisc_t Disc = {
	offsetof(pending_cb_t,key),	/* sort by 'key' */
	sizeof(unsigned long),
	0, /* link offset */
	NIL(Dtmake_f),
	freef,
	NIL(Dtcompar_f),
	NIL(Dthash_f)
};

static Dict_t *dictof(pendingset_t *ds, Agobj_t *obj, int kind)
{
	Dict_t	**dict_ref = NIL(Dict_t**);

	dict_ref = 0;
	switch(AGTYPE(obj)) {
		case AGRAPH:
			switch (kind) {
				case CB_INITIALIZE: dict_ref = &(ds->ins.g); break;
				case CB_UPDATE    : dict_ref = &(ds->mod.g); break;
				case CB_DELETION    : dict_ref = &(ds->del.g); break;
				default: break;
			}
			break;
		case AGNODE:
			switch (kind) {
				case CB_INITIALIZE: dict_ref = &(ds->ins.n); break;
				case CB_UPDATE    : dict_ref = &(ds->mod.n); break;
				case CB_DELETION    : dict_ref = &(ds->del.n); break;
				default: break;
			}
			break;
		case AGEDGE:
			switch (kind) {
				case CB_INITIALIZE: dict_ref = &(ds->ins.e); break;
				case CB_UPDATE    : dict_ref = &(ds->mod.e); break;
				case CB_DELETION    : dict_ref = &(ds->del.e); break;
				default: break;
			}
			break;
		default : break;
	}

	if (dict_ref == 0) agerror(AGERROR_BADOBJ,"pend dictof");
	if (*dict_ref == NIL(Dict_t*))
		*dict_ref = agdtopen(agraphof(obj),&Disc,Dttree);
	return *dict_ref;
}

static unsigned long genkey(Agobj_t *obj)
{
	return obj->tag.id;
}

static pending_cb_t *lookup(Dict_t *dict, Agobj_t *obj)
{
	pending_cb_t	key, *rv;

	key.key = genkey(obj);
	rv = (pending_cb_t*) dtsearch(dict,&key);
	return rv; 
}

static void record_sym(Agobj_t *obj, pending_cb_t *handle, Agsym_t *optsym)
{
	symlist_t	*sym,*nsym,*psym;

	psym = NIL(symlist_t*);
	for (sym = handle->symlist; sym; psym = sym, sym = sym->link) {
		if (sym->sym == optsym) break;
		if (sym == NIL(symlist_t*)) {
			nsym = agalloc(agraphof(obj),sizeof(symlist_t));
			nsym->sym = optsym;
			if (psym) psym->link = nsym;
			else handle->symlist = nsym;
		}
		/* else we already have a callback registered */
	}
}

static pending_cb_t *insert(Dict_t *dict, Agobj_t *obj, Agsym_t *optsym)
{
	pending_cb_t	*handle;
	handle = agalloc(agraphof(obj),sizeof(pending_cb_t));
	handle->obj = obj;
	handle->key = genkey(obj);
	handle->g_alloc = agraphof(obj);
	if (optsym) {
		handle->symlist = (symlist_t*)agalloc(handle->g_alloc,sizeof(symlist_t));
		handle->symlist->sym = optsym;
	}
	dtinsert(dict,handle);
	return handle;
}

static void purge(Dict_t *dict, Agobj_t *obj)
{
	pending_cb_t	*handle;

	if ((handle = lookup(dict,obj))) {
		dtdelete(dict,handle);
	}
}

void agrecord_callback(Agobj_t *obj, int kind, Agsym_t *optsym)
{
	pendingset_t	*pending;
	Dict_t			*dict;
	pending_cb_t	*handle;
	Agraph_t		*g;

	g = agraphof(obj);
	pending = (pendingset_t*) agbindrec(g,DRName,sizeof(pendingset_t),FALSE);

	switch (kind) {
		case CB_INITIALIZE:
			assert(lookup(dictof(pending,obj,CB_UPDATE),obj)==0);
			assert(lookup(dictof(pending,obj,CB_DELETION),obj)==0);
			dict = dictof(pending,obj,CB_INITIALIZE);
			handle = lookup(dict,obj);
			if (handle == 0) handle = insert(dict,obj,optsym);
			break;
		case CB_UPDATE:
			if (lookup(dictof(pending,obj,CB_INITIALIZE),obj)) break;
			if (lookup(dictof(pending,obj,CB_DELETION),obj)) break;
			dict = dictof(pending,obj,CB_UPDATE);
			handle = lookup(dict,obj);
			if (handle == 0) handle = insert(dict,obj,optsym);
			record_sym(obj, handle, optsym);
			break;
		case CB_DELETION:
			purge(dictof(pending,obj,CB_INITIALIZE),obj);
			purge(dictof(pending,obj,CB_UPDATE),obj);
			dict = dictof(pending,obj,CB_DELETION);
			handle = lookup(dict,obj);
			if (handle == 0) handle = insert(dict,obj,optsym);
			break;
		default: agerror(AGERROR_BADOBJ,"agrecord_callback");
	}
}

static void cb(Dict_t *dict, int callback_kind)
{
	pending_cb_t	*pcb;
	Agraph_t		*g;
	symlist_t		*psym;
	Agcbstack_t		*stack;

	if (dict) while ((pcb = (pending_cb_t*)dtfirst(dict))) {
		g = pcb->g_alloc;
		stack = g->clos->cb;
		switch(callback_kind) {
		case CB_INITIALIZE:	aginitcb(pcb->obj,stack); break;
		case CB_UPDATE:	
			for (psym = pcb->symlist; psym; psym = psym->link)
				agupdcb(pcb->obj, psym->sym, stack);
			break;
		case CB_DELETION:	agdelcb(pcb->obj,stack); break;
		}
		dtdelete(dict,pcb);
	}
}

static void agrelease_callbacks(Agraph_t *g)
{
	pendingset_t *pending;
	if (NOT(g->clos->callbacks_enabled)) {
		g->clos->callbacks_enabled = TRUE;
		pending = (pendingset_t*) agbindrec(g,DRName,sizeof(pendingset_t),FALSE);
		/* this destroys objects in the opposite of their order of creation */
		cb (pending->ins.g, CB_INITIALIZE);
		cb (pending->ins.n, CB_INITIALIZE);
		cb (pending->ins.e, CB_INITIALIZE);

		cb (pending->mod.g, CB_UPDATE);
		cb (pending->mod.n, CB_UPDATE);
		cb (pending->mod.e, CB_UPDATE);

		cb (pending->del.e, CB_DELETION);
		cb (pending->del.n, CB_DELETION);
		cb (pending->del.g, CB_DELETION);
	}
}

int agcallbacks(Agraph_t *g, int flag)
{
        if (flag && NOT(g->clos->callbacks_enabled))
                agrelease_callbacks(g);
        if (g->clos->callbacks_enabled) {
                g->clos->callbacks_enabled = flag;
                return TRUE;
        }
	g->clos->callbacks_enabled = flag;
	return FALSE;
}
