/*
 * ***** BEGIN LICENSE BLOCK *****
 * Version: ZAPL 1.1
 * 
 * The contents of this file are subject to the Zimbra AJAX Public
 * License Version 1.1 ("License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.zimbra.com/license
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is: Zimbra AJAX Toolkit.
 * 
 * The Initial Developer of the Original Code is Zimbra, Inc.
 * Portions created by Zimbra are Copyright (C) 2005 Zimbra, Inc.
 * All Rights Reserved.
 * 
 * Contributor(s):
 * 
 * ***** END LICENSE BLOCK *****
 */

var _MODEL_ = "model";
var _INSTANCE_ = "instance";
var _INHERIT_ = "inherit";
var _MODELITEM_ = "modelitem";


function XModel(attributes) {
	// get a unique id for this form
	XFG.assignUniqueId(this, "_Model_");

	// copy any attributes passed in directly into this object
	if (attributes) {
		for (var prop in attributes) {
			this[prop] = attributes[prop];	
		}
	}
	
	if (this.items == null) this.items = [];
	
	this._pathIndex = {};
	this._pathGetters = {};
	this._parentGetters = {};
	this._itemsAreInitialized = false;
	this._errorMessages = {};

	if (this.getDeferInit() == false) {
		this.initializeItems();
	}
}
XModel.toString = function() {	return "[Class XModel]";	}
XModel.prototype.toString = function() {	return "[XModel " + this.__id + "]";	}

XModel.prototype.pathDelimiter = "/";
XModel.prototype.getterScope = _INSTANCE_;
XModel.prototype.setterScope = _INSTANCE_;

// set deferInit to false to initialize all modelItems when the model is created
//	NOTE: this is generally a bad idea, and all XForms are smart enough
//			to tell their models to init before they need them...
XModel.prototype.deferInit = true;
XModel.prototype.getDeferInit = function () {	return this.deferInit	}


XModel.prototype.initializeItems = function() {
	if (this._itemsAreInitialized) return;
	
	var t0 = new Date().getTime();

	this.__nestedItemCount = 0;	//DEBUG

	// initialize the items for the form
	this.items = this.initItemList(this.items, null);

	this._itemsAreInitialized = true;

	var t1 = new Date().getTime();
	//DBG.println(this,".initializeItems(): w/ ", this.__nestedItemCount," items took ", (t1 - t0), " msec");
}



XModel.prototype.initItemList = function(itemAttrs, parentItem) {
	var items = [];
	for (var i = 0; i < itemAttrs.length; i++) {
		items[i] = this.initItem(itemAttrs[i], parentItem);
	}
	this.__nestedItemCount += itemAttrs.length;		//DEBUG
	return items;
}


XModel.prototype.initItem = function(itemAttr, parentItem) {
	// if we already have a form item, assume it's been initialized already!
	if (itemAttr.__isXModelItem) return itemAttr;

	// create the XFormItem subclass from the item attributes passed in
	//	(also links to the model)
	var item = XModelItemFactory.createItem(itemAttr, parentItem, this);
	
	
	
	// have the item initialize it's sub-items, if necessary (may be recursive)
	item.initializeItems();

	return item;
}

XModel.prototype.addItem = function(item, parentItem) {
	if (!item.__isXModelItem) item = this.initItem(item, parentItem);
	if (parentItem == null) {
		this.items.push(item);
	} else {
		parentItem.addItem(item);
	}
}

// add an item to our index, so we can find it easily later
XModel.prototype.indexItem = function(item, path) {
	this._pathIndex[path] = item;
}






//
// getting modelItems, parent items, their paths, etc
//

XModel.prototype.getItem = function(path, createIfNecessary) {
	// try to find the item by the path, return if we found it
	var item = this._pathIndex[path];
	if (item != null) return this._pathIndex[path];

	// if we didn't find it, try normalizing the path
	var normalizedPath = this.normalizePath(path);
	//	convert any "#1", etc to just "#"
	for (var i = 0; i < normalizedPath.length; i++) {
		if (normalizedPath[i].charAt(0) == "#") normalizedPath[i] = "#";
	}
	// and if we find it, save that item under the original path and return it
	item = this._pathIndex[normalizedPath.join(this.pathDelimiter)];
	if (item != null) {
		this._pathIndex[path] = item;
		return item;
	}

	if (createIfNecessary != true) return null;

	// get each parent item (creating if necessary) until we get to the end
	var parentItem = null;
	for (var p = 0; p < normalizedPath.length; p++) {
		var itemPath = normalizedPath.slice(0, p+1).join(this.pathDelimiter);
		var item = this.getItem(itemPath, false);
		if (item == null) {
			//DBG.println("making modelItem for ", itemPath);
			item = XModelItemFactory.createItem({id:normalizedPath[p]}, parentItem, this);
		}
		parentItem = item;
	}
	return item;
}



// "normalize" a path and return it split on the itemDelimiter for this model
XModel.prototype.normalizePath = function (path) {
	if (path.indexOf("[") > -1) {
		path = path.split("[").join("/#");
		path = path.split("]").join("");
	}
	if (path.indexOf(".") > -1) {
		path = path.split(this.pathDelimiter);
		var outputPath = [];
		for (var i = 0; i < path.length; i++) {
			var step = path[i];
			if (step == "..") {
				outputPath.pop();
			} else if (step != ".") {
				outputPath.push(step);
			}
		}
		return outputPath;
	}
	return path.split(this.pathDelimiter);
}


XModel.prototype.getParentPath = function (path) {
	path = this.normalizePath(path);
	return path.slice(0, path.length - 1);
}

XModel.prototype.getLeafPath = function (path) {
	path = this.normalizePath(path);
	return path[path.length - 1];
}






XModel.prototype.getInstanceValue = function (instance, path) {
	var getter = this._getPathGetter(path);
//DBG.println("getInstanceValue(",path,"):" + (typeof path) + ":" + (typeof getter));
	return getter.call(this, instance);
}

XModel.prototype.getParentInstanceValue = function (instance, path) {
	var getter = this._getParentPathGetter(path);
//DBG.println("getParentInstanceValue(",path,"):" + (typeof path) + ":" + (typeof getter));
	return getter.call(this, instance);
}


XModel.prototype.setInstanceValue = function (instance, path, value) {
//DBG.println("setInstanceValue(",path,"): ", value, " (",typeof value,")");
	var parentValue = this.getParentInstanceValue(instance, path);
	if (parentValue == null) {
		parentValue = this.setParentInstanceValues(instance, path);
	}
	var modelItem = this.getItem(path, true);
	var leafPath = this.getLeafPath(path);
	var ref = modelItem.ref;
	if (leafPath.charAt(0) == "#") ref = parseInt(leafPath.substr(1));
	
	if (modelItem.setter) {
		// convert "/" to "." in the ref
		if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join(".");

		var setter = modelItem.setter;
		var scope = modelItem.setterScope;
		if (scope == _INHERIT_) scope = this.setterScope;
		if (scope == _INSTANCE_) {
			instance[setter](value, parentValue, ref);
		} else if (scope == _MODEL_) {
			this[setter](value, instance, parentValue, ref);		
		} else {
			modelItem[setter](value, instance, parentValue, ref);
		}
	} else {
		if (typeof ref == "string" && ref.indexOf(this.pathDelimiter) > -1) {
			ref = ref.split(this.pathDelimiter);
			for (var i = 0; i < ref.length - 1; i++) {
				parentValue = parentValue[ref[i]];
			}
			ref = ref.pop();
		}
		parentValue[ref] = value;
	}
	return value;
}


XModel.prototype.setParentInstanceValues = function (instance, path) {
	var pathList = this.getParentPath(path);
	for (var i = 0; i < pathList.length; i++) {
		var itemPath = pathList.slice(0, i+1).join(this.pathDelimiter);
		var itemValue = this.getInstanceValue(instance, itemPath);
		if (itemValue == null) {
			var modelItem = this.getItem(itemPath, true);
			var defaultValue = modelItem.getDefaultValue();
			itemValue = this.setInstanceValue(instance, itemPath, defaultValue);
		}
	}
	return itemValue;
}







//NOTE: model.getInstance() gets count of PARENT
// "modelItem" is a pointer to a modelItem, or an path as a string
XModel.prototype.getInstanceCount = function (instance, path) {
	var list = this.getParentInstanceValue(instance, path);
	if (list != null && list.length) return list.length;
	return 0;
}


// "path" is a path of id's
XModel.prototype.addRowAfter = function (instance, path, afterRow) {
	var newInstance = null;	
	
	var modelItem = this.getItem(path);
	if (modelItem) {
		newInstance = this.getNewListItemInstance(modelItem);
	} else {
		newInstance = "";
	}
	var list = this.getInstanceValue(instance, path);
	if (list == null) {
		// create a list and install it!
		list = [];
		this.setInstanceValue(instance, path, list);
	}

	list.splice(afterRow+1, 0, newInstance);
}


XModel.prototype.getNewListItemInstance = function (modelItem) {
	var listItem = modelItem.listItem;
	if (listItem == null) return "";
	return this.getNewInstance(listItem);
}

XModel.prototype.getNewInstance = function (modelItem) {
	if (modelItem.defaultValue != null) return modelItem.defaultValue;
	
	var type = modelItem.type;
	switch (type) {
		case _STRING_:
			return "";

		case _NUMBER_:
			return 0;
			
		case _OBJECT_:
			var output = {};
			if (modelItem.items) {
				for (var i = 0; i < modelItem.items.length; i++) {
					var subItem = modelItem.items[i];
					if (subItem.ref) {
						output[subItem.ref] = this.getNewInstance(subItem);
					} else if (subItem.id) {
						output[subItem.id] = this.getNewInstance(subItem);
					}
				}
			
			}
			return output;
			
		case _LIST_:
			return [];

		case _DATE_:
		case _TIME_:
		case _DATETIME_:
			return new Date();
			
		default:
			return "";
	}
}



// "modelItem" is a pointer to a modelItem, or an path as a string
XModel.prototype.removeRow = function (instance, path, instanceNum) {
	var list = this.getInstanceValue(instance, path);
	if (list == null) return;

	// WHAT IF LIST IS A STRING?
	list.splice(instanceNum, 1);
}






// for speed, we create optimized functions to traverse paths in the instance
//	to actually return values for an instance.  Make them here.
//
XModel.prototype._getPathGetter = function (path) {
//DBG.println("_getPathGetter(",path,")");
	var getter = this._pathGetters[path];
	if (getter != null) return getter;

	getter = this._makePathGetter(path);
//DBG.println("assigning path getter for ", path, " to ", getter);
	this._pathGetters[path] = getter;
	return getter;
}

XModel.prototype._getParentPathGetter = function (path) {
//DBG.println("_getParentPathGetter(",path,")");
	var getter = this._parentGetters[path];
	if (getter != null) return getter;
	
	var parentPath = this.getParentPath(path).join(this.pathDelimiter);
	getter = this._getPathGetter(parentPath);
	this._parentGetters[path] = getter;
	this._pathGetters[parentPath] = getter;
	return getter;
}


XModel.prototype._makePathGetter = function (path) {
	if (path == null) return new Function("return null");
	
	// normalizePath() converts to an array, fixes all "." and ".." items, and changes [x]  to #x
	var pathList = this.normalizePath(path);

	// forget any leading slashes
	if (pathList[0] == "") pathList = pathList.slice(1);
	
//	DBG.println("_makePathGetter(", path, "): ", pathList);
	var methodSteps = [];
	var pathToStep = "";
	
	for (var i = 0; i < pathList.length; i++) {
		var	pathStep = pathList[0, i];
		
		if (pathStep.charAt(0) == "#") {
			pathStep = pathStep.substr(1);
			pathToStep = pathToStep + "#";
		} else {
			pathToStep = pathToStep + pathStep;

		}
		var	modelItem = this.getItem(pathToStep, true);
		var ref = modelItem.ref;

		// convert "/" to "." in the ref
		if (ref.indexOf(this.pathDelimiter) > -1) ref = ref.split(this.pathDelimiter).join(".");

		if (modelItem.getter) {
			var getter = modelItem.getter;
			var scope = modelItem.getterScope;
			if (scope == _INHERIT_) {
				scope = this.getterScope;
			}
			
			if (scope == _INSTANCE_) {
				methodSteps.push("if(instance) {");	
				methodSteps.push("	current = instance."+ getter+ "(current, '"+ref+"');");
				methodSteps.push("}");							
			} else if (scope == _MODEL_) {
				methodSteps.push("current = this."+ getter+ "(instance, current, '"+ref+"');");
			} else {
				methodSteps.push("current = this.getItem(\""+ pathToStep+ "\")."+ getter+ "(instance, current, '"+ref+"');");			
			}
			
		} else if (ref == "#") {
			methodSteps.push("if(current) {");	
			methodSteps.push("	current = current[" + pathStep + "];");
			methodSteps.push("}");					
		} else {
			methodSteps.push("if(current) {");		
			methodSteps.push("	current = current." + ref + ";");
			methodSteps.push("}");					
		}
		pathToStep += this.pathDelimiter;
	}

	var methodBody = AjxBuffer.concat(
			"try {\r",
			"var current = instance;\r",
			"\t", methodSteps.join("\r\t"), "\r",
			"} catch (e) {\r ",
			"	DBG.println('Error in getting path for \"", path, "\": ' + e);\r",
			"	current = null;\r",
			"}\r",
			"return current;\r"
		);
	//DBG.println(path,"\r\t", methodSteps.join("\r\t"), "\r");
	var method = new Function("instance", methodBody);

	return method;
}








// error messages
//	NOTE: every call to XModel.prototype.registerError() should be translated!

XModel._errorMessages = {};
XModel.registerErrorMessage = XModel.prototype.registerErrorMessage = function (id, message) {
	this._errorMessages[id] = message;
}
XModel.registerErrorMessage("unknownError", "Unknown error.");
XModel.prototype.defaultErrorMessage = "unknownError";


// set the default error message for the model (it's not a bad idea to override this in your models!)
XModel.prototype.getDefaultErrorMessage = function (modelItem) {
	if (modelItem && modelItem.errorMessage) {
		return modelItem.getDefaultErrorMessage();
	}

	return this.defaultErrorMessage;
}

XModel.prototype.getErrorMessage = function (id, arg0, arg1, arg2, arg3, arg4) {
	var msg = this._errorMessages[id];
	if (msg == null) msg = XModel._errorMessages[id];
	
	if (msg == null) {
		DBG.println("getErrorMessage('", id, "'): message not found.  If this is an actual error message, add it to the XModel error messages so it can be translated.");
		return id;
	}
	if (arg0 !== null) msg = msg.split("$0").join(arg0);
	if (arg1 !== null) msg = msg.split("$1").join(arg1);
	if (arg2 !== null) msg = msg.split("$2").join(arg2);
	if (arg3 !== null) msg = msg.split("$3").join(arg3);
	if (arg4 !== null) msg = msg.split("$4").join(arg4);
	return msg;
}

