/* Filename: kbase.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 store the information from rdf documents.
 * 
 *	Classes:
 * KBase
 * InfoSource
 * InferredSource
 * 
 * 	Version information:
 * $HeadURL: svn+ssh://mc1204@svn.ugforge.ecs.soton.ac.uk/projects/grdf/code/branches/serverside/js/rdf/kbase.js $
 * $Date: 2007-10-04 04:17:48 -0700 (Thu, 04 Oct 2007) $
 * $Author: mc1204 $
 * $Rev: 140 $
 */

function KBase()
{
	// Store of all statements, in nested array, indexed by URI.
	this.stBySub = [];
	this.stByPre = [];
	this.stByObj = [];
	this.stBySrc = [];
	
	// Prefix store
	this.prefixToURI = [];
	this.URIToPrefix = [];
	
	// Running totals during parsing of documents
	this.accounting = [];
	
	// Transitive Properties
	this.transProps = [];
	// Transitive Triggers
	this.transTriggers = [];
	// Transitive Statements (indexed by transitive property)
	this.transStmts = [];
	
	// URI update Listeners
	this.URIListeners = [];
	
	// Looks after RDF node creation
	this.nodeFactory = new RDFNodeFactory();
	// Looks after Information Source Creation
	this.inferSrcFactory = new InferredSourceFactory();
	// Reason ID
	this.reasonCount = 0;
	
	
	this.rdfParser = new RDFParser(this);
	this.addRDFSInference();
	return this;
}

KBase.prototype.type = "KnowledgeBase";

/**
 * 
 */
KBase.prototype.add = function(subj, pred, obj, why)
{
	if (why == null)
		this._add(subj, pred, obj);
	else
		this._add(subj, pred, obj, why.doc, why);
};

KBase.prototype._add = function(subj, pred, obj, src, why) {
	if (src == null || src.id == null) {
		src = {id: null};
	}


	// Make sure the arrays exist
	if (this.stBySub[subj.value] == null)
		this.stBySub[subj.value] = [];
	if (this.stByPre[pred.value] == null)
		this.stByPre[pred.value] = [];
	if (this.stByObj[obj.value] == null)
		this.stByObj[obj.value] = [];
	if (this.stBySrc[src.id] == null)
		this.stBySrc[src.id] = [];

	
	if(why != null && why.id != null && this.accounting[why.id] != null){
		this.accounting[why.id].triples++;

		// Number of triples on that URI
		if(subj.type != 'RDFLiteral'){
			if(this.accounting[why.id].uris[subj.value] != null)
				this.accounting[why.id].uris[subj.value]++;
			else
				this.accounting[why.id].uris[subj.value] = 1;
		}
		if(obj.type != 'RDFLiteral'){
			if(this.accounting[why.id].uris[obj.value] != null)
				this.accounting[why.id].uris[obj.value]++;
			else
				this.accounting[why.id].uris[obj.value] = 1;
		}
	}
	
	var stmt = this.nodeFactory.getStatement(subj, pred, obj, src);
	
	this.stBySub[subj.value].push(stmt);
	this.stByPre[pred.value].push(stmt);
	this.stByObj[obj.value].push(stmt);
	this.stBySrc[src.id].push(stmt);
	
	if (why == null || why.id == null) {
		var tmp = [];
		tmp.push(subj.value);
		tmp.push(obj.value);
		
		this._notifyURIChanges(tmp);
	}
	
	/** Inference **/
	
	if (this.transProps[pred.value] != null){
		uri = pred.value;
		if (this.transStmts[uri] == null)
			this.transStmts[uri] = [];

		this.transStmts[uri][this.transStmts[uri].length] = stmt;
		// Pred is last, as it's optional. Incase functions handle multiple things.
		this.transProps[uri].call(this, why, src, subj, obj, pred);
	}
	// TODO loops really needed? Isn't it a hashmap?
	if (this.transTriggers[pred.value] != null){
		uri = pred.value;
		// Pred is last, as it's optional. Incase functions handle multiple things.
		for (i in this.transTriggers[uri])
			this.transTriggers[uri][i].call(this, why, src, subj, obj, pred); 
	}
};

/**
 * Remove all triples from the specified document
 * @param doc Doument URI to remove from
 */
KBase.prototype.removeBySrc = function(doc)
{
	if(doc == null){
		gERROR('Remove By Document: No document specified',this.type);
		return;
	}
	gLOG('Removing statements from \'' + doc + '\'.', this.type);
	
	// TODO Fix
	if(this.stByDoc[doc] != null){
		var stmts = this.stByDoc[doc];
		delete this.stByDoc[doc];
		
		for(i in stmts){
			var subj = stmts[i].subject.value;
			var pred = stmts[i].predicate.value;
			var obje = stmts[i].object.value;
			
			var remain = [];
			for(j in this.stBySub[subj]){
				if(this.stBySub[subj][j].doc != doc)
					remain.push(this.stBySub[subj][j]);
			}
			if(remain.length != 0){
				this.stBySub[subj] = remain;
			} else {
				delete this.stBySub[subj];
			}
			
			remain = [];
			for(j in this.stByPre[pred]){
				if(this.stByPre[pred][j].doc != doc)
					remain.push(this.stByPre[pred][j]);
			}
			if(remain.length != 0){
				this.stByPre[pred] = remain;
			} else {
				delete this.stByPre[pred];
			}
			
			remain = [];
			for(j in this.stByObj[obje]){
				if(this.stByObj[obje][j].doc != doc)
					remain.push(this.stByObj[obje][j]);
			}
			if(remain.length != 0){
				this.stByObj[obje] = remain;
			} else {
				delete this.stByObj[obje];
			}
		}
	}
};

KBase.prototype.addTransProp = function(uri, func) {
	this.transProps[uri] = func;
};

KBase.prototype.addTransPropTrigger = function(uri, prop, func) {
	if (this.transTriggers[uri] == null)
		this.transTriggers[uri] = [];
	this.transTriggers[uri][prop] = func;
};

KBase.prototype.addURIListener = function(object, methodName) {
	this.URIListeners.push({obj: object, meth: methodName});
};

KBase.prototype._notifyURIChanges = function(list) {
	this.URIListeners.map(function (item){item.obj[item.meth](this);},list);
};

KBase.prototype.parse = function(docXML, doc)
{
	var reason = new Reason(++this.resonCount, doc);

	var rec = {triples: 0, uris: []};
	this.accounting[reason.id] = rec;

	gLOG('Begin parsing \'' + doc.uri + '\'.', this.type);
	try{
		this.rdfParser.parse(docXML, doc.uri, reason); // XML, Doc URI, Why
	} catch (e){
		gERROR('RDFParser Error: '+ e,this.type);
		throw e;
	}
	
	gLOG('Done parsing \'' + doc.uri + '\'. ' + rec.triples + ' triples.', this.type);
	this._notifyURIChanges(rec.uris);
	
	delete this.accounting[reason.id];

	return rec;
};

KBase.prototype.setPrefixForURI = function(prefix, uri) {
	this.prefixToURI[prefix] = uri;
	this.URIToPrefix[uri] = prefix;
};

KBase.prototype.getByURI = function(uri) {
	var result = [];
	
	if(this.stBySub[uri] != null){
		result = result.concat(this.stBySub[uri]);
	}
	
	if(this.stByObj[uri] != null){
		result = result.concat(this.stByObj[uri]);
	}
	// indexedResult - no duplicates
	var iResult = [];
	for (i in result){
		var sub = result[i].subject.value;
		var pre = result[i].predicate.value;
		var obj = result[i].object.value;
		
		if(iResult[sub] == null)
			iResult[sub] = [];
		if(iResult[sub][pre] == null)
			iResult[sub][pre] = [];
		iResult[sub][pre][obj] = result[i];
	}
	result = [];
	
	for (s in iResult){
		for (p in iResult[s]){
			for (o in iResult[s][p])
				result.push(iResult[s][p][o]);
		}
	}
	
	return result;
};

KBase.prototype.getStmtsMatching = function(subj, pred, obj, doc) {
	var candidates = [];
	var all = function(stmt)	{ this.push(stmt); };
	var funSubj = function(stmt)	{ if (subj == stmt.subject.value) this.push(stmt);};
	var funPred = function(stmt)	{ if (pred == stmt.predicate.value) this.push(stmt);};
	var funObj = function(stmt) 	{ if (obj == stmt.object.value) this.push(stmt);};
	var funDoc = function(stmt) 	{ if (doc == stmt.doc) this.push(stmt);};
	
	var temp;
	
	if (subj == null){
		if (obj == null){
			if (pred == null){
				if (doc == null){
					for(i in this.stByDoc)
						this.stByDoc[i].map(all, candidates);
				} else {
					if(this.stByDoc[doc]) this.stByDoc[doc].map(all, candidates);
				}
			} else {
				if(this.stByPre[pred]) this.stByPre[pred].map(all, candidates);
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			}
		} else {
			if(this.stByObj[obj])
				this.stByObj[obj].map(all, candidates);
				
			if (pred == null){
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			} else {
				temp = []; candidates.map(funPred, temp); candidates = temp;
				
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			}
		}
	} else {
		if(this.stBySub[subj])
			this.stBySub[subj].map(all, candidates);
			
		if (obj == null){	
			if (pred == null){
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			} else {
				temp = []; candidates.map(funPred, temp); candidates = temp;
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			}
		} else {
			temp = []; candidates.map(funObj, temp); candidates = temp;
			if (pred == null){
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			} else {
				temp = []; candidates.map(funPred, temp); candidates = temp;
				if (doc != null){
					temp = []; candidates.map(funDoc, temp); candidates = temp;
				}
			}
		}
	}

	return candidates;
};

KBase.prototype.getURIPrefix = function(uri) {
	return this.URIToPrefix[uri];
};

KBase.prototype.getPrefixURI = function(prefix) {
	return this.prefixToURI[prefix];
};

/** RDFS SUPPORT **/
KBase.prototype.addRDFSInference = function() {
	var rdfsSubClass = function(why, src, subj, obj, pred){
		var from;
		if (pred.value == RDFConst.abbr['rdfs:subClassOf']){
			// If A is a subclass of B, find all instances of A and infer they are of class B
			var stmts = this.getStmtsMatching(null, RDFConst.abbr['rdf:type'], subj.value);

			for (i in stmts) {
				from = this.inferSrcFactory.getSrc(src, stmts[i].src);
				
				this._add(stmts[i].subject, this.sym(RDFConst.abbr['rdf:type']), obj, from, why);
			}
			from = this.inferSrcFactory.getSrc(src, this.rdfSchema);
			this._add(subj, this.sym(RDFConst.abbr['rdf:type']), this.sym(RDFConst.abbr['rdfs:Class']), from, why);
			this._add(obj, this.sym(RDFConst.abbr['rdf:type']), this.sym(RDFConst.abbr['rdfs:Class']), from, why);

		} else if (pred.value == RDFConst.abbr['rdf:type']){
			// If this is a class of something we have a subclass relation for
			var arr = this.transStmts[RDFConst.abbr['rdfs:subClassOf']];
			
			for (i in arr) {
				var stmt = arr[i];
				if (stmt.subject.value == obj.value){
					from = this.inferSrcFactory.getSrc(src, stmt.src);
					this._add(subj, pred, stmt.object, from, why);
				}
			}
		}
	};
	var rdfsSubProp = function(why, src, subj, obj, pred){
		var from;
		if (pred.value == RDFConst.abbr['rdfs:subPropertyOf']) {
			// If A is a subProperty of B, find all statements with A and duplicate with B
			var stmts = this.getStmtsMatching(null, subj.value, null);

			for (i in stmts) {
				from = this.inferSrcFactory.getSrc(src, stmts[i].src);
				this._add(stmts[i].subject, obj, stmts[i].object, from, why);
			}
			this.addTransPropTrigger(subj.value,pred.value,this.transProps[pred.value]);

			from = this.inferSrcFactory.getSrc(src, this.rdfSchema);
			this._add(subj, this.sym(RDFConst.abbr['rdf:type']), this.sym(RDFConst.abbr['rdf:Property']), from, why);
			this._add(obj, this.sym(RDFConst.abbr['rdf:type']), this.sym(RDFConst.abbr['rdf:Property']), from, why);
		} else {
			// if we have a known subproperty assertion for this property
			var arr = this.transStmts[RDFConst.abbr['rdfs:subPropertyOf']];
			for (i in arr) {
				var stmt = arr[i];
				if (stmt.subject.value == pred.value){
					from = this.inferSrcFactory.getSrc(src, stmt.src);
					this._add(subj, stmt.object, obj, from, why);
				}
			}
		}
	};
	
	this.rdfSchema = new InfoSource(RDFConst.ns.rdfs);
	this.addTransProp(RDFConst.abbr['rdfs:subClassOf'], rdfsSubClass);
	this.addTransPropTrigger(RDFConst.abbr['rdf:type'], RDFConst.abbr['rdfs:subClassOf'], rdfsSubClass);
	this.addTransProp(RDFConst.abbr['rdfs:subPropertyOf'], rdfsSubProp);
};

/**
 * RDF Symbol call for the RDFParser
 */
KBase.prototype.sym = function(uri) 
{
	return this.nodeFactory.getSymbol(uri);
};

/**
 * RDF Literal call for the RDFParser
 */
KBase.prototype.literal = function(value, lang, datatype)
{
	return this.nodeFactory.getLiteral(value, lang, datatype);
};

/**
 * RDF Collection call for the RDFParser
 */
KBase.prototype.collection = function (){
	return this.nodeFactory.getCollection();	
};

/**
 * RDF Blank Node call for the RDFParser
 */
KBase.prototype.bnode = function (){
	return this.nodeFactory.getBnode();	
};

/**
 *	InfoSource
 * 
 * An abstract source of information
 */
function InfoSource(id) {
	this.id = id;
	
	return this;	
}
InfoSource.prototype.type = 'InfoSource';

/**
 * Inferred Source
 * @param a First source of the inference
 * @param b Second source of the inference
 */
function InferredSource(a, b) {
	this.src1 = a;
	this.src2 = b; 
	this.id = 'infer:(' + a.id + ' & ' + b.id + ')';
	
	return this;
}

InferredSource.prototype = new InfoSource();
InferredSource.prototype.constructor = InferredSource;
InferredSource.prototype.type = 'InferredSource';

function InferredSourceFactory () {
	this.sources = [];
	
	return this;
}

InferredSourceFactory.prototype.type = 'InferredSourceFactory';

InferredSourceFactory.prototype.getSrc = function(a, b) {
	var src1, src2;
	if (a.id <= b.id){
		src1 = a; src2 = b;
	} else {
		src1 = b; src2 = a;
	}
	
	if (this.sources[src1.id] == null)
		this.sources[src1.id] = [];
	
	if (this.sources[src1.id][src2.id] == null)
		this.sources[src1.id][src2.id] = new InferredSource(src1, src2);
		
	return this.sources[src1.id][src2.id];
};

// TODO
function Reason (id, doc) {
	this.id = id;
	this.doc = doc;
	return this;
}

Reason.prototype.type = 'Reason';
