/* Filename: docs.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 retrieve from rdf documents.
 * 
 *	Classes:
 * DMngr
 * Document
 * 
 * 	Version information:
 * $HeadURL: svn+ssh://mc1204@svn.ugforge.ecs.soton.ac.uk/projects/grdf/code/branches/serverside/js/rdf/docs.js $
 * $Date: 2007-10-04 04:17:48 -0700 (Thu, 04 Oct 2007) $
 * $Author: mc1204 $
 * $Rev: 140 $
 */

function DMngr (kBase) {
	this.kBase = kBase;
	this.req = null;	// XMLHTTPRequest Object
	this.documents = [];
	
	this.uriListeners = [];
	this.statusListeners = [];

	this.docQueue = [];
	this.docQueueActive = false;
	
	window.setTimeout(this._processQueue, 500, this);
	
	return this;
}

DMngr.prototype.type = 'DocumentManager';

/**
 * Add and retrieve a document
 * @param uri URI of the document to add
 */
DMngr.prototype.add = function(uri) {
	// Make sure we have a global URI going on.
	uri = Util.uri.globalURL(uri);
	// Make sure hash namespaces cause only one doc retrieval
	var docpart = Util.uri.docpart(uri);
	
	var doc = this._getDoc(docpart);
	if (doc == null) {
		doc = this._addDoc(docpart);
		this._queueDoc(doc);
	}
	
};

/**
 * Remove a document from the system
 * @param uri URI of the document to remove
 */
DMngr.prototype.remove = function(uri) {
	this._removeDoc(uri);
};

/**
 * Check for the existance of a document
 * @param uri URI of the document to add
 * @return Document object or null
 */
DMngr.prototype.check = function(uri) {
	return this._getDoc(uri);
};

/**
 * Add a listener to this class
 *
 */
DMngr.prototype.addDocStatusListener = function(nosy, method) {
	this.statusListeners.push({obj: nosy, meth: method});
};

/**
 * Add a document to the store
 * @param uri URI of the document
 * @return Document object
 */
DMngr.prototype._addDoc = function(uri, srcDoc) {
	var doc = new Document(uri, srcDoc);
	this.documents[uri] = doc;
	
	this._notifyStatus('add', doc);
	return doc;
};

/**
 * Retrieve a document from the store
 * @param uri URI of the document
 * @return Document object
 */
DMngr.prototype._getDoc = function(uri) {
	return this.documents[uri];
};

DMngr.prototype._removeDoc = function(uri) {
	var doc = this.documents[uri];
	
	if (doc == null)
		return;
	
	delete this.documents[uri];
	// TODO Remove from queue also
	// TODO remove it from links in redirection
	
	this._notifyStatus('remove', doc);
	kBase.removeBySrc(doc);
};

/**
 * Add a document to the download queue
 * @param doc Document to queue
 */
DMngr.prototype._queueDoc = function(doc) {
	doc.status = '0-1';
	this.docQueue.push(doc);
	
	this._notifyStatus('update', doc);
};

/**
 * Check and process the front element of the queue
 * NB: called from a window timeout, so no valid this object set. Instead passed as a parameter.
 */
DMngr.prototype._processQueue = function(me) {
	if (me.docQueueActive != false)
		return;
	
	me.docQueueActive = true;
	
	if (me.docQueue.length == 0){
		me.docQueueActive = false;
		window.setTimeout(me._processQueue, 500, me);
		return;
	}
	
	var doc = me.docQueue.shift();

	if (doc.headers == null)
		me._AJAXRequest(doc,'headers');
	else
		me._AJAXRequest(doc,'content');
};

/**
 * Fire an XMLHTTPRequest to retrieve a document
 * @param doc Document to retrieve
 * @param method What to request from the server. Either 'content' or 'headers'. 
 */
DMngr.prototype._AJAXRequest = function(doc, method) {
	
	// Set the status, and update the menu
	doc.status = '1-0';
	this._notifyStatus('update',doc);
	
	var request;
	if (this.req == null || this.req.count >= 20){
		request = false;
		request = new XMLHttpRequest();
	
		if (! request) {
			gERROR('Cannot create XMLHTTP instance.', this.type);
		
			doc.status = '4-E1';
			this.updateStatus(doc);
			return false;
		}
		
		request.callback = this;
		request.count = 1;
		
		this.req = request;
	} else {
		request = this.req;
		request.count++;
	}
	
	request.doc = doc;
	request.method = method;

	// Event based callback doesn't seem to work. Resorting to the old method.
	//request.addEventListener('readyStateChange', this.progressCallback, false);
	request.onreadystatechange = function(){request.callback._progressCallback(request);};
	
	request.addEventListener('error', this._errorCallback, false);

	request.open('GET', 'temp/moo.php?mode=' + method + '&url=' + doc.uri, true);

	try {
		request.send(null);
	} catch (e) {
		gERROR('Could not send Request. ' + e, this.type);
		
		doc.status = '4-E2';
		return false;
	}
};

/**
 * Callback function for status changes during the XMLHTTPRequest
 * 
 */
DMngr.prototype._progressCallback = function(req) {
	/*
	* 0 = uninitialized
	* 1 = open
	* 2 = sent
	* 3 = receiving
	* 4 = loaded
    */
	var me = req.callback;

	if (req.readyState == 4){ // Finished
		var failed = true;
		// W3C spec says req.status must be avaliable on status 4. Firefox thinks otherwise and will throw an exception.
		try{
			failed = (req.status == null);
		} catch (e) {
			// Nothing special to do.
		}
		
		if(! failed){
			if (req.status >= 200 && req.status < 300) { // HTTP 200 OK
				if (req.method == 'headers') {
					req.doc.headers = req.responseText;
					failed = true;
					error4XX = false;
					
					var arrHead = req.doc.headers.split("\r\n\r\n");
					for (var i=0; i < arrHead.length; i++) {
						if (/^HTTP\/1.1 303 See Other\r\n/.test(arrHead[i])) {
							var location = /\r\nLocation:\s*(.+)\s*\r\n/.exec(arrHead[i])[1];
							location = Util.uri.globalURL(location);
							req.doc.redirected = true;
							// TODO existing docs?
							req.doc.redirDest = me._addDoc(location, req.doc);
							req.doc.resolving = false;
							req.doc.resolved = true;
							req.doc.status = '2-?';
							
							me._notifyStatus('update', req.doc);
							
							req.doc = req.doc.redirDest;
							me.documents[location] = req.doc;
							
						} else if (/^HTTP\/[0-9]\.[0-9] 2[0-9]{2}/.test(arrHead[i])) {
							failed = false;

							break;
						} else if (/^HTTP\/[0-9]\.[0-9] 4[0-9]{2}/.test(arrHead[i])) {
							//console.log('4XX', arrHead[i]);
							error4XX = true;
							break;
						}
					}
					
					if (! failed) {
						me.docQueue.unshift(req.doc);
					} else {
						if (error4XX) {
							// HTTP Error
						} else {
							// Network Error
							req.doc.status = "4-ENET";
							me._notifyStatus('update',req.doc);
						}
					}
				} else if (req.method == 'content') {
					
					req.doc.resolving = false;
					req.doc.resolved = true;
					req.doc.status = '2-H' + req.status;
					me._notifyStatus('update', req.doc);
					
					// TODO check mimes.
					var temp = me.kBase.parse(req.responseXML, req.doc);
					
					req.doc.uris = temp.uris;
					req.doc.triples = temp.triples;
					me._notifyStatus('update', req.doc);
				}
			} else {
				gLOG('Request finished: Error retrieving from serverside script ' + req.status, me.type);
			}
		}
		
		me.docQueueActive = false;
		window.setTimeout(me._processQueue, 500, me);
	} else {
		if(req.readyState > 2)
			gLOG('Request state: ' + req.readyState + '. status: ' + req.status,me.type);
		else
			gLOG('Request state: ' + req.readyState, me.type);
	}
};

/**
 * Callback function for errors during the XMLHTTPRequest
 * 
 */
DMngr.prototype._errorCallback = function(event) {
	var req = event.target;
	
	req.doc.status = '4-E?';
	req.callback._notifyStatus('update', req.doc);
};

/**
 * Utility function for notifying listeners of document status changes.
 * @param type Type of the notification. Either 'add', 'remove' or 'update'.
 * @param doc Document object changed (If blank: removed)
 */
DMngr.prototype._notifyStatus = function(type, doc) {
	var tmp = { doc: doc, type: type };
	this.statusListeners.map( function(item){item.obj[item.meth](this.type, this.doc);} , tmp);
};


function Document(uri, srcDoc) {
	this.id = 'doc:(' + uri +')';
	this.uri = uri;
	this.status = '0-0';
	
	this.resolving = false;
	this.resolved = false;
	
	this.redirected = false;
	this.redirDest = null;
	this.redirSrc = [];
	if (srcDoc != null)
		this.redirSrc[srcDoc.uri] = srcDoc;

	this.headers = null;
	
	this.uris = null;
	this.triples = null;
	return this;
}

Document.prototype = new InfoSource();
Document.prototype.constructor = Document;
Document.prototype.type = 'Document';

Document.prototype.isResolved = function() {
	return (this.resolved);
};

Document.prototype.getStatus = function() {
	if (this.statusDescriptions[this.status] == null)
		return this.status;
	return this.statusDescriptions[this.status];
};

Document.prototype.statusDescriptions = {
	'0-0':	'None',
	'0-1':	'Queued',
	'1-0':	'In Progress',
	/*	Complete */
	'2-H200': 	'200 OK',
	'2-H201': 	'201 Created',
	'2-H202': 	'202 Accepted',
	'2-H203': 	'203 Non-Authoritative Information',
	'2-H204': 	'204 No Content',
	'2-H205': 	'205 Reset Content',
	'2-H206': 	'206 Partial Content',
	'2-H207': 	'207 Multi-Status',
	/*	Failure */
	'4-H400': 	'400 Bad Request',
	'4-H401': 	'401 Unauthorized',
	'4-H402': 	'402 Payment Required',
	'4-H403': 	'403 Forbidden',
	'4-H404': 	'404 Not Found',
	'4-H405': 	'405 Method Not Allowed',
	'4-H406': 	'406 Not Acceptable',
	'4-H407': 	'407 Proxy Authentication Required',
	'4-H408': 	'408 Request Timeout',
	'4-H409': 	'409 Conflict',
	'4-H410': 	'410 Gone',
	'4-H411': 	'411 Length Required',
	'4-H412': 	'412 Precondition Failed',
	'4-H413': 	'413 Request Entity Too Large',
	'4-H414': 	'414 Request-URI Too Long',
	'4-H415': 	'415 Unsupported Media Type',
	'4-H416': 	'416 Requested Range Not Satisfiable',
	'4-H417': 	'417 Expectation Failed'
};
