/*----------------------------------------------------------*/
/* This is the witica.js client library to access items     */
/* from a witica WebTarget including base classes and       */
/* common function to write your site specific renderers    */
/*----------------------------------------------------------*/

/*-----------------------------------------*/
/* Witica: Namespace                       */
/*-----------------------------------------*/

var Witica = Witica || {};
Witica.util = Witica.util || {};

/*-----------------------------------------*/
/* Witica: globals                         */
/*-----------------------------------------*/

Witica.VERSION = "0.9.1"
Witica.CACHESIZE = 100;

Witica.itemcache = new Array();
Witica._knownHashes = new Array();
Witica.currentItemId = null;
Witica.registeredRenderers = new Array();
Witica.mainView = null;
Witica.defaultItemId = "";
Witica._virtualItemCount = 0;
Witica._prefix = "";

/*-----------------------------------------*/
/* Common extensions                       */
/*-----------------------------------------*/

Array.prototype.remove= function (index) {
  this.splice(index, 1);
};

Array.prototype.insert = function (index, item) {
  this.splice(index, 0, item);
};

Array.prototype.contains = function (item) {
	for (var i = 0; i < this.length; i++) {
		if (this[i] == item) {
			return true;
		}
		return false;
	};
}

/*-----------------------------------------*/
/* Witica.util                             */
/*-----------------------------------------*/

Witica.util.timeUnits = {
	second: 1,
	minute: 60,
	hour: 3600,
	day: 86400,
	week: 604800,
	month: 2592000,
	year: 31536000
};

Witica.util.getHumanReadableDate = function(date) {
	var dateStr, amount,
		current = new Date().getTime(),
		diff = (current - date.getTime()) / 1000;

	if(diff > Witica.util.timeUnits.week) {
		dateStr = date.getFullYear() + "-";
		if (date.getMonth()+1 < 10) 
			dateStr += "0";
		dateStr += (date.getMonth()+1) + "-";
		if (date.getDate() < 10)
			dateStr += "0";
		dateStr += date.getDate();
	}
	else if(diff > Witica.util.timeUnits.day) {
		amount = Math.round(diff/Witica.util.timeUnits.day);
		dateStr = ((amount > 1) ? amount + " " + "days ago":"one day ago");
	} 
	else if(diff > Witica.util.timeUnits.hour) {
		amount = Math.round(diff/Witica.util.timeUnits.hour);
		dateStr = ((amount > 1) ? amount + " " + "hour" + "s":"an " + "hour") + " ago";
	} 
	else if(diff > Witica.util.timeUnits.minute) {
		amount = Math.round(diff/Witica.util.timeUnits.minute);
		dateStr = ((amount > 1) ? amount + " " + "minute" + "s":"a " + "minute") + " ago";
	} 
	else {
		dateStr = "a few seconds ago";
	}

	return dateStr;
};

Witica.util.shorten = function (html, maxLength) {
	plaintext = html.replace(/(<([^>]+)>)/ig,"");

	var shortstr = "";
	var minLength = 0.8*maxLength;

	var sentences = plaintext.split(". ");
	var sentenceNo = 0;
	for (; shortstr.length < minLength && sentenceNo < sentences.length; sentenceNo++) {
		if ((shortstr.length + sentences[sentenceNo].length + ". ".length) <= maxLength) {
			shortstr += sentences[sentenceNo];

			if (sentenceNo < sentences.length-1) {
				shortstr += ". ";
			}
		}
		else {
			var words = sentences[sentenceNo].split(" ");
			var wordNo = 0;
			for (; shortstr.length < minLength && wordNo < words.length; wordNo++) {
				if ((shortstr.length + words[wordNo].length + " ".length) <= maxLength-3) {
					shortstr += words[wordNo] + " ";
				}
				else {
					shortstr = plaintext.slice(0,maxLength-2);
				}
			};
			shortstr = shortstr.slice(0,shortstr.length-1) + "...";
		}
	}

	return shortstr;
};

//Finds y value of given object
Witica.util.getYCoord = function(element) {
	return element.offsetTop + (element.parentElement ? Witica.util.getYCoord(element.parentElement) : 0);
};

Witica.util.Event = function () {
	this._listeners = new Array();
};

Witica.util.Event.prototype = {
	constructor: Witica.util.Event,

	addListener: function(context, callable) {
		var listener = {};
		listener.context = context;
		listener.callable = callable;

		this._listeners.push(listener);
	},

	removeListener: function(context, callable) {
		for (var i=0; i < this._listeners.length; i++){
			if (this._listeners[i].context == context && this._listeners[i].callable == callable) {
				this._listeners.remove(i);
				i--;
			}
		}
	},

	fire: function(argument) {
		for (var i=0; i < this._listeners.length; i++){
			this._listeners[i].callable.call(this._listeners[i].context,argument);
		}
	},

	getNumberOfListeners: function () {
		return this._listeners.length;	
	}
}

/*-----------------------------------------*/
/*	Witica: Item cache                     */
/*-----------------------------------------*/

Witica.Item = function (itemId, virtual) {
	this.isLoaded = false;
	this.itemId = itemId;
	this.metadata = null;
	this.contents = new Array();
	this.hash = null;
	this.lastUpdate = null;
	this.virtual = virtual;
	this.loadFinished = new Witica.util.Event();
	if (this.virtual) {
		this.isLoaded = true;
	}
};

Witica.Item.prototype.toString = function () {
  return this.itemId;
};

Witica.Item.prototype._loadMeta = function(hash) {
	var http_request = new XMLHttpRequest();
	http_request.open("GET", Witica._prefix + this.itemId + ".item" + "?bustCache=" + Math.random(), true);
	http_request.onreadystatechange = function () {
		var done = 4, ok = 200;
		if (http_request.readyState == done) {
			if (http_request.status == ok) {
				var metadata = JSON.parse(http_request.responseText);
				this.contents = [];
				if (metadata["witica:contentfiles"]) {
					for (var i = 0; i < metadata["witica:contentfiles"].length; i++) {
						var content = new Witica.Content(this, metadata["witica:contentfiles"][i]);
						this.contents.push(content);
					}
				}
				this.metadata = this._processMetadata(metadata);
			}
			this.isLoaded = true;
			this.hash = hash;
			this.loadFinished.fire(this);
		}
	}.bind(this);
	http_request.send(null);
};

Witica.Item.prototype._processMetadata = function (metadata) {
	if (typeof metadata == 'string' || metadata instanceof String) {
		if (/^!(?:.\/)?(?!meta\/)[^\n@.]+$/.test(metadata)) {
			var item_id = metadata.substring(1);
			return Witica.getItem(item_id);
		}
		else {
			return metadata;
		}
	}
	else if (metadata instanceof Array) {
		var list = [];
		for (var i = 0; i < metadata.length; i++) {
			list.push(this._processMetadata(metadata[i]));
		}
		return list;
	}
	else if (metadata instanceof Object) {
		var obj = {};
		for (key in metadata) {
			obj[key] = this._processMetadata(metadata[key]);
		}
		return obj;
	}
	else {
		return metadata;
	}
};

Witica.Item.prototype.update = function () {
	if (this.virtual) {
		throw new Error("Virutal items cannot be updated.");
	}

	var http_request = new XMLHttpRequest();
	http_request.open("GET", Witica._prefix + this.itemId + ".itemhash" + "?bustCache=" + Math.random(), true);
	http_request.onreadystatechange = function () {
		var done = 4, ok = 200, notfound = 404;
		if (http_request.readyState == done) {
			this.lastUpdate = new Date();
			if (http_request.status == ok) {
				var newHash = http_request.responseText;
				if (this.hash != newHash) {
					this._loadMeta(newHash);
				}
			}
			//treat as non existing if item couldn't be loaded for the first time or was deleted, but not if it is in cache and only a network error occured during update
			else if ((this.hash != "" && http_request.readyState == notfound) || (this.hash == null)) {
				this.metadata = null;
				this.isLoaded = true;
				this.hash = "";
				this.loadFinished.fire(this);
			}
		}
	}.bind(this);
	http_request.send(null);
};

Witica.Item.prototype.exists = function () {
	return !(this.metadata == null);
};

Witica.Item.prototype.getContent = function(extension) {
	var extlist = [];

	if (typeof extension == "string") {
		extlist.push(extension);
	}
	else if (extension instanceof Array) {
		extlist = extension;
	}

	var contentlist = [];
	for (var i = 0; i < this.contents.length; i++) {
		var ext = this.contents[i].filename.substring(this.contents[i].filename.lastIndexOf(".") + 1);
		if (extlist.indexOf(ext) > -1) {
			contentlist.push(this.contents[i]);
		}	
	};

	if (typeof extension == "string") {
		return contentlist[0];
	}
	else if (extension instanceof Array) {
		return contentlist;
	}
	return null;
};

Witica.Item.prototype.requestLoad = function (update, callback) {
	var requestObj = {};
	requestObj.callback = callback;
	requestObj.item = this;

	requestObj.abort = function () {
		this.item.loadFinished.removeListener(this, this._finished);
	};

	if (this.isLoaded) {
		callback(this, true);
		requestObj._finished = null;
		if (update) {
			requestObj._finished = function () {
				this.callback(this.item, true);
			}
			this.loadFinished.addListener(requestObj, requestObj._finished);
		}
	}
	else {
		requestObj._finished = function () {
			this.callback(this.item, true);
			if (!update) {
				this.item.loadFinished.removeListener(this, this._finished);
			}
		}
		this.loadFinished.addListener(requestObj, requestObj._finished);
	}

	return requestObj;
}

Witica.Item.prototype.toString = function () {
	return this.itemId;
};

Witica.Content = function (item, json) {
	this.item = item;

	this.filename = Witica._prefix + json.filename;
	this.hash = json.hash;
	if (json.variants) {
		this.variants = json.variants;
	}
	else {
		this.variants = [];
	}
}

Witica.Content.prototype.getURL = function(variant) {
	var variant_str = "";
	if (typeof variant == 'undefined') {
		variant_str = "";
	}
	else if (typeof variant == 'number') { //when variant integer, use the next integer variant bigger or equal
		var best_variant = Infinity;
		for (var i = 0; i < this.variants.length; i++) {
			if (!isNaN(parseInt(this.variants[i])) && parseInt(this.variants[i]) >= variant) {
				if (parseInt(this.variants[i]) < best_variant) {
					best_variant = parseInt(this.variants[i]);
				}
			}
		}
		if (best_variant != Infinity) {
			variant_str = best_variant.toString();
		}
	}
	else {
		for (var i = 0; i < this.variants.length; i++) {
			if (this.variants[i] == variant) {
				variant_str = this.variants[i];
				break;
			}
		}
	}

	if (variant_str != "") {
		var ext = this.filename.substring(this.filename.lastIndexOf(".") + 1);
		var filename = this.filename.substring(0,this.filename.lastIndexOf("."));
		return filename + "@" + variant_str + "." + ext;
	}
	else { //use default variant if no matching variant found
		return this.filename;
	}
};

Witica.Content.prototype.downloadVariant = function(variant, callback) {
	var http_request = new XMLHttpRequest();
	http_request.open("GET", this.getURL(variant) + "?bustCache=" + this.hash, true);

	http_request.onreadystatechange = function () {
		var done = 4, ok = 200;
		if (http_request.readyState == done && http_request.status == ok) {
			callback(http_request.responseText, true);
		}
		else if (http_request.readyState == done && http_request.status != ok) {
			callback(null,false);
		}
	};
	http_request.send(null);
	return http_request;
};

Witica.Content.prototype.download = function(callback) {
	return this.downloadVariant(undefined, callback);
};

Witica.createVirtualItem = function (metadata) {
	var itemId = "witica:virtual-" + Witica._virtualItemCount;
	var item = new Witica.Item(itemId, true);
	item.metadata = metadata;
	Witica._virtualItemCount++;
	return item;
};

Witica.updateItemCache = function () {
	currentTime = (new Date()).getTime();
	len = Witica.itemcache.length;
	//console.log("Cached:");
	for (var i = 0; i < len; i++) {
		var item = Witica.itemcache[i];
		//console.log(item.itemId + "(" + item.loadFinished.getNumberOfListeners() + ")");
		if (item.isLoaded) {
			//delete from cache if unused and cache full
			if (len > Witica.CACHESIZE && item.loadFinished.getNumberOfListeners() == 0) {
				Witica.itemcache.remove(i);
				len--;
				i--;
				continue;
			}

			//update item
			var nextUpdate = item.lastUpdate.getTime() + 600000; //if no modification time available, update every 10min
			if (item.exists() && item.metadata.hasOwnProperty("last-modified")) { //update at most every 60 sec, items with older modification date less often
				var interval = Math.round(150*Math.log(0.0001*((currentTime/1000)-item.metadata["last-modified"])+1)+30)*1000;
				if (interval < 1000) { //make sure interval is not negative (in case of wrong modification date)
					interval = 600000;
				}
				nextUpdate = item.lastUpdate.getTime() + interval;
			}
			if (currentTime >= nextUpdate) {
				item.update();
			}
		}	
	}
	setTimeout("Witica.updateItemCache()",10000); //update cache every 10s
};

Witica.getItem = function (itemId) {
	//try to find in cache
	for (var i = Witica.itemcache.length - 1; i >= 0; i--) {
		if (Witica.itemcache[i].itemId == itemId) {
			return Witica.itemcache[i];
		}
	};
	//not in cache: download item add put in cache
	var item = new Witica.Item(itemId, false);
	item.update();
	Witica.itemcache.push(item);
	return item;
};

Witica.loadItem = function () {
	if (location.hash.indexOf("#!") == 0) { //location begins with #!
		var itemId = location.hash.substring(2).toLowerCase();
		Witica.currentItemId = itemId;
		var item = Witica.getItem(itemId);
		Witica.mainView.showItem(item);
	}
	else {
		Witica.mainView.showItem(Witica.getItem(Witica.defaultItemId));
	}
};

/*-----------------------------------------*/
/*	Witica: Views and renderer             */
/*-----------------------------------------*/

Witica.View = function (element){
	this.element = element;
	this.renderer = null;
	this.subviews = new Array();
	this.item = null;
	this.params = {};
	this._scrollToTopOnNextRenderRequest = false;
	this._itemLoadRequest = null;
};

Witica.View.prototype = {
	showItem: function (item, params) {
		//stop listening for updates of the previous item
		this._itemLoadRequest && this._itemLoadRequest.abort();

		//update hash if main view and item not virtual
		if (this == Witica.mainView && (!item.virtual)) {
			window.onhashchange = null;
			location.hash = "#!" + item.itemId;
			Witica.currentItemId = item.itemId;
			window.onhashchange = Witica.loadItem;
		}

		//show new item
		this.item = item;
		this.params = params;
		this._scrollToTopOnNextRenderRequest = true; //scroll to top when showItem() was called but not on item udpate
		
		this._itemLoadRequest = item.requestLoad(true, this._showLoadedItem.bind(this));
	},

	_showLoadedItem: function () {
		//show error if item doesn't exist
		if (!this.item.exists()) {
			this.showErrorMessage("Item not found", "Sorry, but the item with the ID '" + this.item.itemId + "' was not found.", 404);
			return;
		}

		//set title
		if (this.item.metadata.title) {
			this.setTitle(this.item.metadata.title)
		}
		else {
			this.setTitle("(no title)");
		}

		//find appropriate renderer
		var oldRenderer = this.renderer;
		var newRendererClass = null;

		for (var i = Witica.registeredRenderers.length - 1; i >= 0; i--) {
			try {
				if (Witica.registeredRenderers[i].supports(this.item)) {
					newRendererClass = Witica.registeredRenderers[i].renderer;
					break;
				}
			}
			catch (exception) {
			}
		};
		if (newRendererClass == null) {
			this.showErrorMessage("Not loaded", "Sorry, but the item with the ID '" + this.item.itemId + "' cannot be displayed, because no appropriate renderer was not found. " + '<br/><br/>Try the following: <ul><li>Click <a href="index.html">here</a> to go back to the start page.</li></ul>', 404);
			return;
		}

		//unrender and render
		if (oldRenderer != null && oldRenderer.constructor == newRendererClass) {
			this.renderer.changeItem(this.item, this.params);
		}
		else {
			this.renderer = new newRendererClass();
			if (oldRenderer != null) {
				oldRenderer.stopRendering();
				oldRenderer.unrender(this.item);
				if (typeof oldRenderer.deinit == 'function') {
					oldRenderer.deinit(this.renderer);
				}
			}
			this.renderer.initWithItem(this, this.item, oldRenderer, this.params);
		}
		if (this._scrollToTopOnNextRenderRequest && this == Witica.mainView && document.body.scrollTop > Witica.util.getYCoord(this.element)) {
			window.scrollTo(0,Witica.util.getYCoord(this.element));
		}
		this._scrollToTopOnNextRenderRequest = false;
	},

	loadSubviews: function (element) {
		if (!element) {
			element = this.element;
		}
		var viewElements = element.getElementsByTagName("view");
		for (var i = 0; i < viewElements.length; i++) {
			var view = new Witica.View(viewElements[i]);
			var params = null;
			try {
				 params = JSON.parse(viewElements[i].childNodes[0].textContent);
			}
			catch (e) {
				//ignore
			}
			view.showItem(Witica.getItem(viewElements[i].getAttribute("item")), params);
			this.subviews.push(view);
		};
	},

	destroy: function () {
		this._itemLoadRequest.abort(); //stop listening for updates for current item

		this.destroySubviews();
		if (this.renderer != null) {
			this.renderer.stopRendering();
			this.renderer.unrender(this.item);
			if (typeof this.deinit == 'function') {
				this.deinit(null);
			}
		}
	},

	destroySubviews: function () {
		for (var subview = this.subviews[0]; this.subviews.length > 0; subview = this.subviews[0]) {
			subview.destroy();
			this.subviews.remove(0);
		};
	},

	showErrorMessage: function (title, body, errorcode) {
		var error = {};
		error.type = "error";
		error.title = title;
		error.description = body;
		error.errorcode = errorcode;
		errorItem = Witica.createVirtualItem(error);
		this.showItem(errorItem);
	},

	setTitle: function (title) {
		this._title = title;
		if (this == Witica.mainView) {
			document.title = this._title;
		}
	},

	getTitle: function () {
		if (this._title) {
			return this._title;
		}
		else {
			return "(no title)";
		}	
	},

	toString: function (depth) {
   		depth = typeof depth !== 'undefined' ? depth : 0;
   		var str = "";
   		for (var i = 0; i < depth; i++) {
   			str += "  ";		
   		};
		str += "|-- " + this.getTitle();

		for (var i = 0; i < this.subviews.length; i++) {
			str +=  "\n" + this.subviews[i].toString(depth+1);
		}
		return str;
	}
};

Witica.Renderer = function (){
	this.view = null;
	this.item = null;
	this.renderRequests = Array();
	this.rendered = false;
	this.params = {};
};

Witica.Renderer.prototype = {
	initWithItem: function (view, item, previousRenderer, params) {
		if (this.rendered) {
			this.view.showErrorMessage("Error", "Renderer is already initialized.", 403);
			return;
		}

		this.view = view;
		if (typeof this.init == 'function') {
			this.init(previousRenderer);
		}

		if (params) {
			this.params = params;
		}
		else {
			this.params = {};
		}

		this.item = item;
		if (this.item.isLoaded) {
			this.render(item);
			this.rendered = true;
		}
		else {
			this.view.showErrorMessage("Not loaded", "Sorry, but the item with the ID '" + this.item.itemId + "' was not loaded.", 404);
		}
	},
	changeItem: function (item, params) {
		if (!this.rendered) {
			this.view.showErrorMessage("Not initialized", "Renderer is not initialized.", 403);
			return;
		}

		if (params) {
			this.params = params;
		}
		else {
			this.params = {};
		}

		this.stopRendering();
		this.item = item;
		if (this.item.isLoaded) {
			this.unrender(item);
			this.render(item);
		}
		else {
			this.view.showErrorMessage("Not loaded", "Sorry, but the item with the ID '" + this.item.itemId + "' was not loaded.", 404);
			return;
		}
	},
	requireContentVariant: function(content, variant, callback) {
		if (typeof content == "string") {
			content = this.item.getContent(content);
		}
		else if (content instanceof Array) {
			var requests = [];
			for (var i = 0; i < content.length; i++) {
				requests.push(this.requireContentVariant(content[i], variant, callback));
			}
			return requests;
		}

		if (content) {
			var request = content.downloadVariant(variant, function (content,success) {
				if (success) {
					callback(content);
				}
			});
			this.addRenderRequest(request);
			return request;
		}
		else {
			return null;
		}

	},
	requireContent: function(content, callback) {
		return this.requireContentVariant(content, undefined, callback);
	},
	requireItem: function(item, callback) {
		var request = item.requestLoad(true, function (item, success) {
			if (success) {
				callback(item);
			}
		});
		this.addRenderRequest(request);
		return request;
	},
	addRenderRequest: function (request) {
		this.renderRequests.push(request);
	},
	stopRendering: function () {
		//stop requests spawned during rendering if necessary
		for (var i = 0; i < this.renderRequests.length; i++) {
			var request = this.renderRequests[i];
			try {
				if(typeof request.abort == 'function') {
					request.abort();
				}
			}
			catch (err) {
				//ignore
			}
		}
		this.renderRequests = [];
	}
};

/*-----------------------------------------*/
/*	Witica: Initialization	               */
/*-----------------------------------------*/

Witica.registerRenderer = function (renderer, supports) {
	supportFunction = null;
	if (typeof supports === "string") {
		supportFunction = function (item) {
			return item.metadata.type == supports;
		}
	}
	else {
		supportFunction = supports;
	}
	renderObj = {};
	renderObj.renderer = renderer;
	renderObj.supports = supportFunction;
	Witica.registeredRenderers.push(renderObj);
};

Witica.initWitica = function (mainView, defaultItemId, prefix) {
	prefix = typeof prefix !== 'undefined' ? prefix + "/" : "";

	Witica._prefix = prefix;
	Witica.mainView = mainView;
	Witica.defaultItemId = defaultItemId;
	window.onhashchange = Witica.loadItem;
	Witica.loadItem();
	Witica.updateItemCache();
}