/* Filename: rdfGraph.js
 * This file is part of the gRaDF Semantic Web browsing tool.
 * Copyright (C) 2006  Marcus Cobden
 * 
 * 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.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 * (or see the file gpl.txt)
 * 
 * 	Description:
 * This file contains classes to present RDF triples as a normal node & edge layout to the graph based system.
 * It also contains interactive functionality on the nodes, and a utility function to split predicates.
 * 
 *	Classes:
 * URIMngr
 * NodeView
 * 
 * 	Version information:
 * $HeadURL: svn+ssh://mc1204@svn.ugforge.ecs.soton.ac.uk/projects/grdf/code/branches/serverside/js/rdfGraph.js $
 * $Date: 2007-06-30 08:07:34 +0100 (Sat, 30 Jun 2007) $
 * $Author: mc1204 $
 * $Rev: 139 $
 */
/* Begin URIMngr */

/**
 * URIMnger - Manages the URIs that the graph based view is subscribed to, and handles
 * the abstraction from triples to nodes and edges. Also is responsible for querying the
 * Store and passing changes to the Graph plotter.
 */
function URIMngr(graphID, graphOptions, kBase, docMngr) {
	this.graphID = graphID;
	this.graphOptions = graphOptions;
	this.docMngr = docMngr;
	this.kBase = kBase;
	
	this.nodeCount = 0;
	this.subscriptions = [];

	this.edgeCount = 0;
	this.edges = {start: [], end: []};
	this.currentEdges = {};
	
	// hover label
	this.nodeHover = null;
	// context menu
	this.nodeContext = {};
	
	this.autoAddRules = [];
	this.graphModeAuto = null;

	this.predMenuURI = null;
	
	this.nodeEvents = {
		contextmenu:function (event) { return URIMngr.nodeContextMenu(event); },
		dblclick:	this.nodeDblClickCallback,
		mouseover:	function (event) { URIMngr.labelShow(event); },
		mouseout:	function (event) { URIMngr.labelHide(event); }
	};
	
	this.setGraphMode('low', true);
	this.setGraphModeAuto(true);
	
	document.getElementById('menu:body:MainMenu:graphMode:auto').addEventListener('change', this.graphModeCallback, false);
	document.getElementById('menu:body:MainMenu:graphMode:low').addEventListener('change', this.graphModeCallback, false);
	document.getElementById('menu:body:MainMenu:graphMode:med').addEventListener('change', this.graphModeCallback, false);
	document.getElementById('menu:body:MainMenu:graphMode:high').addEventListener('change', this.graphModeCallback, false);
	document.getElementById('menu:body:MainMenu:graphMode:auto').checked = true;
	
	document.getElementById('predMenu:moveRight').addEventListener('click', this.predMenuMove, false);
	document.getElementById('predMenu:moveLeft').addEventListener('click', 	this.predMenuMove, false);
	document.getElementById('predMenu:ok').addEventListener('click', 		this.predMenuOk, false);
	document.getElementById('predMenu:cancel').addEventListener('click', 	this.predMenuCancel, false);
	
	kBase.addURIListener(this, 'notifyChanged');
	docMngr.addDocStatusListener(this, 'documentStatus');
	
	return this;
}

URIMngr.prototype.type = 'URI Manager';

/**
 * Subscribe to a particular URI
 */
URIMngr.prototype.add = function(uri, refURI, update)
{
	if(this.subscriptions[uri] != null){
		gLOG('Unable to re-add subscribed node: ' + uri, this.type);
		return false;
	}
	var type;
	if (this.checkResolved(uri))
	 	type = 'node';
	else
		type = 'node-unresolved';
	
	var node = new GraphNode(uri, this.nodeEvents, type);
	
	this.subscriptions[uri] = {node: node, uri: uri, resolved: false};
	gLOG('Added uri \'' + uri + '\' to subscription.', this.type);
	var refNode = null;
	if (refURI != null){
		if (this.subscriptions[refURI] != null)
			refNode = this.subscriptions[refURI].node;
	}
	this.nodeCount++;
	
	gPlotter.addNode(this.subscriptions[uri].node, refNode);

	this.autoGraphMode();
	if (update == null || update == true)
		this.computeEdges(new Array(uri));
	this.updateMenu();
};

/**
 * Unsubscribe from a particular URI
 */
URIMngr.prototype.remove = function(uri)
{
	if(! this.subscriptions[uri]){
		gWARN('Unable to remove unsubscribed node: ' +uri, this.type);
		return false;
	}
	
	var droppedEdges = gPlotter.removeNode(this.subscriptions[uri].node);

	for (i in droppedEdges){
		delete this.currentEdges[droppedEdges[i].id];
	}
	
	delete this.subscriptions[uri];
	gLOG('Removed uri \'' + uri + '\' from subscription.', this.type);
	
	this.updateMenu();
};

/**
 * Check whether a uri has been subscribed to
 */
URIMngr.prototype.check = function(uri) {
	return (this.subscriptions[uri] != null);
};

URIMngr.prototype.checkResolved = function(uri) {
	if (uri.indexOf('bNode-') == 0)
		return true;
		
	return (dMngr.check(uri) != null);
};

URIMngr.prototype.updateMenu = function() {
	MenuHandler.uriMenu.update(this.subscriptions);
	nodeView.updateGraphList(this.subscriptions);
};

/**
 * Will be called with a list of URI's for which the number of statements has changed
 * NB: will be called to notify when URI's have been removed completely, 
 * i.e. no statements left
 */
URIMngr.prototype.notifyChanged = function(list)
{
	var changed = [];
	
	for (uri1 in list){	
		for(uri2 in this.subscriptions){
			if(uri1 == uri2){
				changed.push(uri1);
			}
		}
	}
	
	var func = function (item){
		var stmts = this.kBase.getByURI(item);
		var matches = {};
		var stmt;
		for (i in this.autoAddRules) {
			for (j in stmts) {
				stmt = stmts[j];
				if (this.autoAddRules[i].prop == null || stmt.predicate.value == this.autoAddRules[i].prop){
					if (stmt.subject.value == item){
						if (matches[stmt.object.value] == null)
							matches[stmt.object.value] = [];
						matches[stmt.object.value].push(this.autoAddRules[i]);						
					} else {
						if (matches[stmt.subject.value] == null)
							matches[stmt.subject.value] = [];
						matches[stmt.subject.value].push(this.autoAddRules[i]);						
					}
				}
			}
		}
		var added = {};
		for (uri in matches) {
			stmts = this.kBase.getStmtsMatching(uri);
			for (i in this.autoAddRules) {
				for (j in stmts) {
					stmt = stmts[j];
					if ((this.autoAddRules[i].cond == null || stmt.predicate.value == this.autoAddRules[i].cond) &&
						(this.autoAddRules[i].value == null || stmt.object.value == this.autoAddRules[i].value)){
						
						added[uri] = true;
						this.add(uri, item, false);
						break;
					}
				}
				if (added[uri] != null)
					break;
			}
		}
		
	};
	changed.map(func, this);
	
	this.computeEdges(changed);
};

URIMngr.prototype.documentStatus = function(type, doc) {
	if (type != 'update')
		return;
	
	var record = this.subscriptions[doc.uri];
	if (record == null)
		return;
	
	var node = record.node;
	var resolved = doc.isResolved();
	
	node.nodeType = resolved ? 'node' : 'node-unresolved';
	if (record.resolved == null || record.resolved != resolved) {
		record.resolved = resolved;
		gPlotter.updateNode(node);
	}
};

URIMngr.prototype.computeEdges = function(list) {
	var addEdges = [];
	var removeEdges = [];
	var statements = [];
	
	var temp;
	// Remove all edges which might possibly have been affected
	for(id in list){
		if (this.edges.start[list[id]] != null){
			temp = this.edges.start[list[id]];
			for(end in temp){
				for(type in temp[end]){
					if (this.currentEdges[temp[end][type].id] != null)
						removeEdges[temp[end][type].id] = temp[end][type];
				}
			}
		}
		if (this.edges.end[list[id]] != null){
			temp = this.edges.end[list[id]];
			for(start in temp){
				for(type in temp[start]){
					if (this.currentEdges[temp[start][type].id] != null)
						removeEdges[temp[start][type].id] = temp[start][type];
				}
			}
		}
		
		// And get the statements from the kb while we're at it
		statements = statements.concat(kBase.getByURI(list[id]));
	}
	
	for (index in statements){
		if(statements[index].subject.value == statements[index].object.value)
			continue;
			
		var sub = false; var obj = false;
		for (uri in this.subscriptions){
			if(statements[index].subject.value == uri){
				sub = true;
			} else if(statements[index].object.value == uri){
				obj = true;
			}
		}
		
		if (sub == true && obj == true) {
			var edge = 
				this.getEdge(statements[index].subject.value,
				statements[index].object.value,
				statements[index].predicate.value);
				
			if(removeEdges[edge.id] != null){
				delete removeEdges[edge.id];
			} else if (this.currentEdges[edge.id] == null){
				addEdges[edge.id] = edge;
				this.currentEdges[edge.id] = edge;
			}
		}
	}
	
	for (i in removeEdges){
		delete this.currentEdges[removeEdges[i].id];
	}
	
	gPlotter.removeEdges(removeEdges);
	gPlotter.addEdges(addEdges);
};

URIMngr.prototype.getEdge = function(startID, endID, type) {
	var start, end; // NB: ID == URI in this case
	if(startID < endID){
		start = startID;
		end = endID;
	} else {
		end = startID;
		start = endID;
	}
	
	var edge;
	if (this.edges.start[start] == null)
		this.edges.start[start] = [];
	if (this.edges.end[end] == null)
		this.edges.end[end] = [];

	if (this.edges.start[start][end] == null)
		this.edges.start[start][end] = [];
	if (this.edges.end[end][start] == null)
		this.edges.end[end][start] = [];
			
	if (this.edges.start[start][end][type] == null) {
		edge = new GraphEdge(this.edgeCount,
				this.subscriptions[start].node, 
				this.subscriptions[end].node,
				type);
		this.edges.start[start][end][type] = edge;
		this.edges.end[end][start][type] = edge;
		this.edgeCount++;
	} else {
		edge = this.edges.start[start][end][type];
	}
	
	return edge;
};

URIMngr.prototype.graphModeCallback = function(event) {
	var id = event.currentTarget.id;
	id = id.substr(id.lastIndexOf(':') + 1);
	if (id == 'auto'){
		URIMngr.setGraphModeAuto(event.currentTarget.checked);
	} else if ((id == 'low') || (id == 'med') || (id == 'high')) {
		document.getElementById('menu:body:MainMenu:graphMode:low').removeAttribute('checked');
		document.getElementById('menu:body:MainMenu:graphMode:med').removeAttribute('checked');
		document.getElementById('menu:body:MainMenu:graphMode:high').removeAttribute('checked');
		URIMngr.setGraphMode(id, true);
	}
};

URIMngr.prototype.setGraphMode = function(mode, changeView) {
	var opt = this.graphOptions;
	if (mode == 'low') {
		opt.edge.def.l = 50;
		opt.edge.def.k = 0.2;
		opt.nonedge.def = 2000;
		opt.nonedge.maxDist = 300;
		opt.nodeAddRadius.min = 40;
		opt.nodeAddRadius.max = 80;
	} else if (mode == 'med') {
		opt.edge.def.l = 40;
		opt.edge.def.k = 0.1;
		opt.nonedge.def = 1000;
		opt.nonedge.maxDist = 200;
		opt.nodeAddRadius.min = 50;
		opt.nodeAddRadius.max = 110;
	} else if (mode == 'high') {
		opt.edge.def.l = 35;
		opt.edge.def.k = 0.03;
		opt.nonedge.def = 300;
		opt.nonedge.maxDist = 75;
		opt.nodeAddRadius.min = 60;
		opt.nodeAddRadius.max = 150;
	} else {
		return;
	}
	if (changeView){
		var elem = document.getElementById('menu:body:MainMenu:graphMode:' + mode);
		elem.checked = true;
		elem.setAttribute('checked', true);
	}	
};

URIMngr.prototype.setGraphModeAuto = function(state) {
	this.graphModeAuto = state;
	if (state == true) {
		document.getElementById('menu:body:MainMenu:graphMode:low').setAttribute('disabled', true);
		document.getElementById('menu:body:MainMenu:graphMode:med').setAttribute('disabled', true);
		document.getElementById('menu:body:MainMenu:graphMode:high').setAttribute('disabled', true);
		this.autoGraphMode();
	} else {
		document.getElementById('menu:body:MainMenu:graphMode:low').removeAttribute('disabled');
		document.getElementById('menu:body:MainMenu:graphMode:med').removeAttribute('disabled');
		document.getElementById('menu:body:MainMenu:graphMode:high').removeAttribute('disabled');
	}
};

URIMngr.prototype.autoGraphMode = function() {
	if (this.graphModeAuto == false)
		return;
		
	if (this.nodeCount < 20){
		this.setGraphMode('low', true);
	} else if (this.nodeCount >= 20 && this.nodeCount < 50){
		this.setGraphMode('med', true);		
	} else if (this.nodeCount >= 50) {
		this.setGraphMode('high', true);		
	}
};

URIMngr.prototype.labelShow = function(event) {
	var svgElem = document.getElementById(this.graphID);
	
	if(this.nodeHover == null){	
		this.nodeHover = document.createElementNS('http://www.w3.org/2000/svg', 'text');
		this.nodeHover.appendChild(document.createTextNode(''));
	}
	var title = event.target.temp.node.id;
	
	var stmts = kBase.getStmtsMatching(title, RDFConst.abbr['rdfs:label'], null);
	if (stmts.length != 0)
		title = stmts[0].object.value;
		
	this.nodeHover.firstChild.nodeValue = title;
	
	this.nodeHover.setAttribute('x', event.target.x.baseVal.value);
	this.nodeHover.setAttribute('y', event.target.y.baseVal.value);
	svgElem.appendChild(this.nodeHover);
};

URIMngr.prototype.labelHide = function(event) {
	if(this.nodeHover == null)
		return;
	
	document.getElementById(this.graphID).removeChild(this.nodeHover);
};

URIMngr.prototype.nodeContextMenu = function(event) {
	var menu = this.nodeContext.menu;
	if (menu == null) {
		this.nodeContext.resolve = new ContextMenuItem('Resolve', this.nodeResolveCallback);
		this.nodeContext.stick = new ContextMenuItem('Stick', this.nodeStickCallback, 'default');
		this.nodeContext.unstick = new ContextMenuItem('Unstick', this.nodeStickCallback, 'default');

		menu = new ContextMenu({
			0: new ContextMenuList({
				0: new ContextMenuItem('Inspect', this.nodeInspectCallback),
				1: this.nodeContext.resolve,
				2: this.nodeContext.stick,
				3: this.nodeContext.unstick
				}),
			1: new ContextMenuList({
				0: new ContextMenuItem('Connected Nodes..', null, 'label'),
				1: new ContextMenuNestItem(new ContextMenuList({
						0: new ContextMenuItem('Add', this.nodeAddAllCallback),
						1: new ContextMenuItem('Add by Predicate..', this.nodeAddPredCallback)
					}))
				}),
			2: new ContextMenuList({
				0: new ContextMenuItem('Remove Node', this.nodeRemoveCallback)
				})
			});
			
		this.nodeContext.menu = menu;
	}
	var node = event.currentTarget.temp.node; 
	vis = node.fixed;
	
	this.nodeContext.stick.setVisible(! vis);
	this.nodeContext.unstick.setVisible(vis);
	this.nodeContext.resolve.setVisible(! this.subscriptions[node.id].resolved);
	
	return menu.show(event);
};

URIMngr.prototype.nodeDblClickCallback = function(event) {
	nodeView.showURI(event.currentTarget.temp.node.id);
};

URIMngr.prototype.nodeInspectCallback = function(event) {
	var uri = URIMngr.nodeContext.menu.target.temp.node.id;
	nodeView.showURI(uri);
};

URIMngr.prototype.nodeRemoveCallback = function(event) {
	var uri = URIMngr.nodeContext.menu.target.temp.node.id;
	URIMngr.remove(uri);
};

URIMngr.prototype.nodeStickCallback = function(event) {
	var node = URIMngr.nodeContext.menu.target.temp.node;
	node.fixed = ! node.fixed;
	gPlotter.updateNode(node);
};

URIMngr.prototype.nodeResolveCallback = function(event) {
	var uri = URIMngr.nodeContext.menu.target.temp.node.id;
	MenuHandler.docMenu.loadDocument(uri);
};

URIMngr.prototype.nodeAddAllCallback = function(event) {
	var self = URIMngr;
	var uri = self.nodeContext.menu.target.temp.node.id;
	var stmts = self.kBase.getByURI(uri);
	var added = [];
	var uris = {};
	for (i in stmts) {
		if (stmts[i].predicate.value == RDFConst.abbr['rdf:type'])
			continue; // TODO a better solution
			
		if (stmts[i].subject.value == uri && stmts[i].object.type != 'RDFLiteral') {
			self.add(stmts[i].object.value, uri, false);
			if (uris[stmt.object.value] == null){
				added.push[stmt.object.value];
				uris[stmt.object.value] = true;
			}
		} else if (stmts[i].object.value == uri && stmts[i].subject.type != 'RDFLiteral') {
			self.add(stmts[i].subject.value, uri, false);
			if (uris[stmt.subject.value] == null){
				added.push[stmt.subject.value];
				uris[stmt.subject.value] = true;
			}
		}
	}
	self.computeEdges(added);
};

URIMngr.prototype.nodeAddPredCallback = function(event) {
	var self = URIMngr;
	var uri = self.nodeContext.menu.target.temp.node.id;
	self.predMenuURI = uri;
	var stmts = self.kBase.getByURI(uri);
	
	var removeChild = function(item) { this.removeChild(item);};
	
	var optionLeft = document.getElementById('predMenu:left');
	var optionRight = document.getElementById('predMenu:right');

	for (i = optionLeft.options.length - 1; i >= 0; i--)
		optionLeft.removeChild(optionLeft.options[i]);
	for (i = optionRight.options.length - 1; i >= 0; i--)
		optionRight.removeChild(optionRight.options[i]);
	var preds = {};
	
	for (i in stmts){
		if ((stmts[i].subject.value == uri && stmts[i].object.type != 'RDFLiteral') ||
			(stmts[i].object.value == uri && stmts[i].subject.type != 'RDFLiteral'))
			preds[stmts[i].predicate.value] = true;
	}
	for (index in preds) {
		var opt = document.createElement('option');
		opt.setAttribute('value',index);
		opt.appendChild(document.createTextNode(index));
		
		optionLeft.appendChild(opt);
	}
	
	document.getElementById('predMenu').style.removeProperty('display');
};

URIMngr.prototype.predMenuMove = function(event){
	var src, dst;
	if (event.currentTarget.id == 'predMenu:moveRight'){
		src = document.getElementById('predMenu:left');
		dst = document.getElementById('predMenu:right');
	} else {
		src = document.getElementById('predMenu:right');
		dst = document.getElementById('predMenu:left');
	}
	for (i = 0; i < src.options.length; i++){
		var item = src.options[i];
		if (item.selected){
			src.removeChild(item);
			dst.appendChild(item);
			i--;
		}
	}
};

URIMngr.prototype.predMenuOk = function(event){
	var self = URIMngr;
	var dst = document.getElementById('predMenu:right');
	var uri = self.predMenuURI;
	
	var found = false;
	var preds = {};
	for (var i = dst.options.length - 1; i >= 0; i--){
		preds[dst.options[i].value] = true;
		found = true;
	};
	
	if (found == false)
		return;
	
	var uris = {};
	var added = [];
	var stmts = self.kBase.getByURI(uri);
	
	for (i in stmts){
		var stmt = stmts[i];
		if (preds[stmt.predicate.value] == null)
			continue;
			
		if (stmt.subject.value == uri && stmt.object.type != 'RDFLiteral') {
			self.add(stmt.object.value, uri, false);
			if (uris[stmt.object.value] == null){
				added.push(stmt.object.value);
				uris[stmt.object.value] = true;
			}
		} else if (stmt.object.value == uri && stmt.subject.type != 'RDFLiteral') {
			self.add(stmt.subject.value, uri, false);
			if (uris[stmt.subject.value] == null){
				added.push(stmt.subject.value);
				uris[stmt.subject.value] = true;
			}
		}
	}
	self.computeEdges(added);
	document.getElementById('predMenu').style.setProperty('display', 'none', null);
};

URIMngr.prototype.predMenuCancel = function(event){
	document.getElementById('predMenu').style.setProperty('display', 'none', null);
};

// URIMngr.prototype.placeholderCallback = function(event) {
// 	alert('This isn\'t implemented yet');
// };

/* End URIMngr */