"wedia.wxm.dataloader".namespace((function($){
	// cache interne
	var __cache__ = {
		structures: {},
		instances: {}
	};
	
	// renvoie toujours true
	function internal_acceptAlways() {
		return true;
	};
	
	function isUndefined(val) {
		return typeof val === "undefined";
	};
	
	/**
	 * Normalise un tableau d'ID :
	 * qu'on recoive une chaine d'id séparés par des virgules,
	 * un tableau d'id, un tableau de chaines séparées par des virgules,
	 * on renvoie un tableau d'id.
	 * les valeurs vides ou non numériques sont exclues
	 * exemple:
	 *  <code>
	 *  	internal_normalizeFormIds('1'); 
	 *  	//=> ['1'] 
	 *  	internal_normalizeFormIds('1,2,3'); 
	 *  	//=> ['1','2','3']
	 *  	internal_normalizeFormIds(['1,2,3','4']); 
	 *  	//=> ['1','2','3','4']
	 *  	internal_normalizeFormIds(['1,,2,foo,3','4','bar']); 
	 *  	//=> ['1','2','3','4']
	 *  	internal_normalizeFormIds(['1,,2,foo,3','4','bar',[5,6,"7,8,9"],"10"]); 
	 *  	//=> ['1','2','3','4','5','6','7','8','9','10']
	 *  </code>
	 *  il est par ailleurs possible d'appliquer des filtres supplémentaires au 
	 *  moyen de la fonction filter. La fonction filter prend en paramêtre une 
	 *  valeur à priori ajoutée, et renvoie true si cette valeur doit 
	 *  effectivement être ajoutée.
	 *  exemple:
	 *  <code>
	 *    function isEvenNum(value) {
	 *    	return parseInt(value) % 2 == 0;
	 *    }
	 *    internal_normalizeFormIds(['1,,2,foo,3','4','bar',[5,6,"7,8,9"],"10"], isEvenNum); 
	 *  	//=> ['2','4','6','8','10']
	 *  </code>
	 *   
	 */
	function internal_normalizeFormIds(form_ids, filter) {
		// on s'assure de partir d'un tableau
		form_ids = $.isArray(form_ids) ? form_ids : [form_ids];
		// par defaut le filtre renvoie toujours true
		filter = $.isFunction(filter) ? filter : internal_acceptAlways;
		var tmpFormId = ",";
		for(var i=0, len=form_ids.length; i<len; i++) {
			var item = form_ids[i];
			if($.isArray(item)) {
				item = internal_normalizeFormIds(item, filter).join(",");
			}
			tmpFormId += item + ",";
		}
		tmpFormId = tmpFormId.split(",");
		var res = [];
		$.each(tmpFormId, function(index, elem){
			if( elem.length > 0 && ! isNaN(elem) ) {
				if( filter(elem) ) {
					res.push(elem);
				}
			}
		});
		return res;
	};
	
	/**
	 * vérifie dans une instance que l'ensemble des champs demandés
	 * est bien disponible.
	 * Si fields n'est pas renseigné, on vérifie avec id et name
	 */
	function internal_missesFields(instance, fields) {
		fields = (fields || "id,name").split(",");
		var missingFields = false;
		$.each(fields, function(index, val) {
			if( isUndefined(instance.properties[val]) ) {
				missingFields = true;
			}
		});
		return missingFields;
	};
	
	function internal_missesFieldsAsObjects(mainInstance, fieldsAsObjects) {
		fieldsAsObjects = (fieldsAsObjects || "").split(",");
		var missingFieldAsObject = false;
		$.each(fieldsAsObjects, function(index, field){
			if(field !== "") {
				if( isUndefined(mainInstance.getPropertyAsObject(field)) ) {
					missingFieldAsObject = true;
				}
			}
		});
		
		return missingFieldAsObject;
	};
	
	/**
	 * Adapte le paramètre de configuration au cache en cours.
	 * Garde une copie de la configuration d'origine dans config.
	 * config peut en outre avoir à l'issue du traitement
	 * un attribut cancel qui signifie que tout dans le cache permet de 
	 * répondre à la demande. Il est donc inutile de faire la requete
	 */
	function internal_adaptConfigToCache(config) {
		
		// si on a form_id
		if( ! isUndefined(config.form_id) ) {
			config.form_id = internal_normalizeFormIds(config.form_id);
			// si on a deja chargé des instances de config.form_object
			if( ! isUndefined(__cache__.instances[config.form_object]) ) {
				// on normalise les IDs à charger avec un filtre supplémentaire
				// qui force le rechargement d'un objet si au moins 1 des 
				// critères suivant est vrai :
				//	* la config force un rechargement du cache
				//	* le cache ne contient pas l'instance demandée
				//	* l'instance actuellement en cache n'a pas toutes les champs
				//		demandés
				config.form_id = internal_normalizeFormIds(config.form_id, function(elem){
					return ( config.refresh 
							// si l'instance n'est pas en cache on sort
						|| ( isUndefined(__cache__.instances[config.form_object]["" + elem]) )
							// l'instance est forcément en cache, s'il nous manque un champ on sort
						|| internal_missesFields(__cache__.instances[config.form_object]["" + elem], config.fields)
							// l'instance est forcément en cache, on a forcément la propriété, si on n'a pas l'objet équivalent en cache, on sort
						|| internal_missesFieldsAsObjects(__cache__.instances[config.form_object]["" + elem], config.fieldsAsObjects)
					);
				});
				// si après cette normalisation on n'a plus de form_id
				if(config.form_id.length == 0) {
					// on supprime ce critère de la config
					delete config.form_id;
				} else {
					// sinon, on fait un join
					config.form_id = config.form_id.join(",");
				}
			}
			// si on n'a plus de form_id
			if( isUndefined(config.form_id) ) {
				// si le report de la structure n'a pas été demandé 
				if(! config.reportStruct) {
					// on annule la requete AJAX
					config.cancel = true;
				// sinon (la structure est demandée), si la structure est en cache
				} else if( ! isUndefined(__cache__.structures[config.form_object]) ){
					// on annule le rechargement
					config.cancel = true;
				}
			} else {
				config.form_id = config.form_id.join(",");
			}
		// sinon (seule la structure est demandée)
		} else {
			// si elle est en cache et qu'on ne force pas un rechargement
			if( ! isUndefined(__cache__.structures[config.form_object]) && ! config.refresh) {
				// on annule le rechargement
				config.cancel = true;
			}
		}
		// on renvoie cette nouvelle config
		return config;
	};
	
	function internal_adaptConfig(config) {
		// on sauvegarde la config courante
		config.originalConfig = $.extend(true, {}, config);
		config = $.extend({
			ts: $.now(),
			form_object: "activated",
			reportStruct: false,
			refresh: false
		}, config);
		if(! isUndefined(config.preparedWhere) ) {
			delete config.preparedWhere;
		}
		// si on n'a pas la structure, il faudra la reporter
		if( isUndefined(__cache__.structures[config.form_object]) ) {
			config.reportStruct = true;
		}
		
		if( ! isUndefined(config.fields) ) {
			if(! $.isArray(config.fields) ) {
				config.fields = config.fields.split(",");
			}
		}
		if( ! isUndefined(config.fieldsAsObjects) ) {
			if(! $.isArray(config.fieldsAsObjects) ) {
				config.fieldsAsObjects = config.fieldsAsObjects.split(",");
			}
			config.fields = config.fields || [];
			$.each(config.fieldsAsObjects, function(index, val) {
				if(val == "_directparent" || val == "_superparent") {
					val = "parent";
				}
				if(config.fields.indexOf(val) < 0) {
					config.fields.push(val);
				}
			});
		}
		if( ! isUndefined(config.fields) ) {
			config.fields = config.fields.join(",");
		}
		if( ! isUndefined(config.fieldsAsObjects) ) {
			config.fieldsAsObjects = config.fieldsAsObjects.join(",");
		}
		return config;
	};
	
	/**
	 * fonction de chargement de structure et/ou instances.
	 * @param callback à appeler une fois les données obtenues (via cache ou 
	 * requete AJAX). Ce callback prend en paramêtre un objet avec la structure 
	 * suivante :
	 * {
	 *   "instances" : {
	 *   	"idInstance" : CTObject
	 *   },
	 *   "structure" : CTObjectStructure
	 * }
	 * @param config : la configuration de la requete :
	 * {
	 *   form_object: string structure à charger (par defaut 'activated'),
	 *   refresh: forcer un rafraichissement même si présent dans le cache 
	 *   	(par defaut false),
	 *   fields: string liste des champs à renvoyer (par defaut: 'id,name')
	 *   form_id: id des instances à charger, sous forme de tableau, de chaine 
	 *   	d'id séparés par , ou de tout ca mélangé si ce paramètre est ommis, 
	 *   	alors reportStructure sera toujours considéré comme true
	 * }
	 * 
	 */
	function internal_load(config, callback) {
		callback = callback || $.noop;
		config = internal_adaptConfig(config);
		// is ajax to be done ?
		config = internal_adaptConfigToCache(config);
		if( ! isUndefined(config.cancel) ) {
			internal_dispatchCallbackFromCache(callback, config.originalConfig);
		} else {
			var originalConfig = config.originalConfig;
			delete config.originalConfig;
			$.post(boState.getContextPath() + "/bov3/common/actions/loadjson.jspz", config, function(data){
				internal_feedCache(config, data);
				internal_dispatchCallbackFromCache(callback, originalConfig);
			}, "json");
		}
	};
	
	function internal_search(config, callback) {
		callback = callback || $.noop;
		config.preparedWhereJSON = $.toJSON(config.preparedWhere);
		config = internal_adaptConfig(config);
		
		var originalConfig = config.originalConfig;
		delete config.originalConfig;

		$.post(boState.getContextPath() + "/bov3/common/actions/loadjson.jspz", config, function(data){
			if( isUndefined(config.form_id) ) {
				config.form_id = data.loaded_ids.join(",");
				originalConfig.tmp_form_id = data.loaded_ids.join(",");
			}
			internal_feedCache(config, data);
			internal_dispatchCallbackFromCache(callback, originalConfig, data.pagination);
		}, "json");
		
	};
	
	/**
	 * alimente le cache
	 */
	function internal_feedCache(config, data) {
		// on rempli la structure
		if(data.structures) {
			for(var structureKey in data.structures) {
				__cache__.structures[structureKey] = new CTObjectStructure(data.structures[structureKey]);
			}
		}
		// on rempli les instances demandés par form_id
		if(data.instances) {
			__cache__.instances[config.form_object] = __cache__.instances[config.form_object] || {};
			var searchedIDs = config.form_id.split(",");
			for(var i=0, len=searchedIDs.length; i<len; i++) {
				if(searchedIDs[i] != "" && ! isNaN(searchedIDs[i])) {
					var currID = searchedIDs[i];
					if( isUndefined(data.instances[currID]) ) {
						__cache__.instances[config.form_object][currID] = null;
					} else {
						if( isUndefined(__cache__.instances[config.form_object][currID]) ) {
							__cache__.instances[config.form_object][currID] = new CTObject(config.form_object, data.instances[currID]);
						} else {
							internal_CTObject_updateProperties.call(__cache__.instances[config.form_object][currID], data.instances[currID]);
						}
						internal_CTObject_simulateMissingProperties.call(__cache__.instances[config.form_object][currID], (config.fields || "").split(","));
					}
				}
			}
		}
		// on rempli les instances liées (via fieldsAsObjects)
		if(data.relatedInstances) {
			for(var relatedInstanceName in data.relatedInstances) {
				__cache__.instances[relatedInstanceName] = __cache__.instances[relatedInstanceName] || {};
				for(var relatedInstanceID in data.relatedInstances[relatedInstanceName]) {
					if( isUndefined( __cache__.instances[relatedInstanceName][relatedInstanceID]) ) {
						__cache__.instances[relatedInstanceName][relatedInstanceID] = new CTObject(relatedInstanceName, data.relatedInstances[relatedInstanceName][relatedInstanceID]);
					} else {
						internal_CTObject_updateProperties.call(__cache__.instances[relatedInstanceName][relatedInstanceID], data.relatedInstances[relatedInstanceName][relatedInstanceID]);
					}
				}
			}
		}
		// On complète les instances liées non trouvées à null
		var struct = __cache__.structures[config.form_object];
		var fieldsAsObjects = (config.fieldsAsObjects || "").split(",");
		for(var instanceKey in data.instances) {
			var obj = __cache__.instances[config.form_object][instanceKey];
			$.each(fieldsAsObjects, function(index, field) {
				if(field != "") {
					if(field == "_directparent" || field == "_superparent") {
						field = "parent";
					}
					switch(struct.fields[field].type) {
					case "child":
						__cache__.instances[struct.fields[field].nature] = __cache__.instances[struct.fields[field].nature] || {};
						if( isUndefined(__cache__.instances[struct.fields[field].nature][obj.properties[field]]) ) {
							__cache__.instances[struct.fields[field].nature][obj.properties[field]] = null;
						}
						break;
					case "childmulti":
					case "childmultilng":
					case "childmultilngdb":
						var ids = obj.properties[field].split(",");
						$.each(ids, function(index, val) {
							if(val != "") {
								if( isUndefined(__cache__.instances[struct.fields[field].nature][val]) ) {
									__cache__.instances[struct.fields[field].nature][val] = null;
								}
							}
						});
						break;
					default: 
						break;
					}
				}
			});
		}
	};
	
	/**
	 * appel du callback user
	 */
	function internal_dispatchCallbackFromCache(callback, config, pagination) {
		var res = {};
		if(config.reportStruct || ( isUndefined(config.form_id) &&  isUndefined(config.tmp_form_id))) {
			res["structure"] = __cache__.structures[config.form_object];
		}
		if( ! isUndefined(config.form_id) || ! isUndefined(config.tmp_form_id) ) {
			var form_ids = internal_normalizeFormIds(config.tmp_form_id || config.form_id);
			
			res["instances"] = {};
			for(var i=0, len=form_ids.length; i<len; i++) {
				res["instances"]["" + form_ids[i]] = __cache__.instances[config.form_object]["" + form_ids[i]];
			}
			if(! isUndefined(config.tmp_form_id)){
				delete config.tmp_form_id;
			}
		}
		callback(config, res, pagination);
	};
	
	var STR_DATE_FORMAT = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/;
	function internal_parseStrDateFormat(val) {
		var res = STR_DATE_FORMAT.exec(val);
		var dt = new Date(parseInt(res[1], 10), parseInt(res[2], 10) - 1, parseInt(res[3], 10), parseInt(res[4], 10), parseInt(res[5], 10), parseInt(res[6], 10));
		return dt;
	};

	var DATE_FORMAT = /(\d{2})\/(\d{2})\/(\d{4})( (\d{2}):(\d{2}):(\d{2}))?/;
	function internal_parseDateFormat(val) {
		var res = DATE_FORMAT.exec(val);
		var dt = new Date(parseInt(res[3], 10), parseInt(res[2], 10) - 1, parseInt(res[1], 10), parseInt(res[5] || "0", 10), parseInt(res[6] || "0", 10), parseInt(res[7] || "0", 10));
		return dt;
	};

	function CTObjectStructure(fields) {
		this.fields = fields;
	};
	
	function CTObject(form_object, properties) {
		this.form_object = form_object;
		this.properties = properties;
	};
	
	// pour les propriétés non trouvées, on force null pour ne pas les recharger à chaque fois
	function internal_CTObject_simulateMissingProperties(fields) {
		for(var i=0, len=fields.length; i<len;i++) {
			var field = fields[i];
			if(field != "") {
				if( isUndefined(this.properties[field]) ) {
					this.properties[field] = null;
				}
			}
		}
	};
	
	function internal_CTObject_updateProperties(properties) {
		for(var prop in properties) {
			this.properties[prop] = properties[prop];
		}
	};
	
	
	CTObject.prototype.getProperty=function(propertyName) {
		return this.properties[propertyName];
	};
	
	CTObject.prototype.getPropertyI18n=function(propertyName) {
		var rawVal = this.getProperty(propertyName);
		var keyVal = (this.form_object + "." + rawVal).toLowerCase();
		if(this.getCTObjectStructure() == null || this.getCTObjectStructure().fields[propertyName].type == "data") {
			var args = ["/bov3/wcmcaption", keyVal];
			for(var i=1, len=arguments.length; i<len; i++) {
				args.push(arguments[i]);
			}
			var localizedVal = boi18n.getMessage.apply(boi18n, args);
			if(localizedVal != keyVal) {
				return localizedVal;
			}
		}
		return rawVal;
	};
	
	CTObject.prototype.getPropertyAsObject=function(propertyName) {
		switch(propertyName) {
		case "_directparent" : 
			var parents = this.getProperty("parent").split("_").reverse();
			if(parents.length >= 2) {
				if( ! isUndefined(__cache__.instances[parents[1]]) ) {
					return __cache__.instances[parents[1]][parents[0]];
				}
			}
			break;
		case "_superparent" :
			var parents = this.getProperty("parent").split("_");
			if(parents.length >= 2) {
				if( ! isUndefined(__cache__.instances[parents[0]]) ) {
					return __cache__.instances[parents[0]][parents[1]];
				}
			}
			break;
		default:
			var field = this.getCTObjectStructure().fields[propertyName] || null;
			if(field == null) {
				return null;
			} else {
				switch(field.type) {
				case "integer":
					return parseInt(this.getProperty(field.colField), 10);
				case "decimal":
				case "money":
					return parseFloat(this.getProperty(field.colField)); 
				case "child":
					if( ! isUndefined(__cache__.instances[field.nature]) ) {
						return __cache__.instances[field.nature][this.getProperty(field.colField)] || null;
					}
					break;
				case "childmulti":
				case "childmultilng":
				case "childmultilngdb":
					if( ! isUndefined(__cache__.instances[field.nature]) ) {
						var res = [];
						var ids= this.getProperty(field.colField).split(",");
						$.each(ids, function(index, val){
							var elem = __cache__.instances[field.nature][val]; 
							if( ! isUndefined(elem) ) {
								res.push(elem);
							}
						});
						return res;
					}
					break;
				case "date":
					return internal_parseStrDateFormat(this.getProperty(field.colField));
				case "dater":
					return internal_parseDateFormat(this.getProperty(field.colField));
					break;
				case "datetime":
					return internal_parseDateFormat(this.getProperty(field.colField));
					break;
				}
			}
		}

	};

	CTObject.prototype.getProperties=function() {
		return this.properties;
	};
	
	CTObject.prototype.getCTObjectStructure=function() {
		return __cache__.structures[this.form_object];
	};
	
	function PreparedWhere(baseWhere) {
		this.bases = [];
		for(var i=0, len=arguments.length; i<len; i++) {
			this.bases.push(arguments[i]);
		}
		this.criterias = [];
		this.andOrWhere = []; 
	};
		
	PreparedWhere.prototype.addCriteriaStr = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "string",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: constraintValue
		});
		return this;
	};
	
	PreparedWhere.prototype.addCriteriaInt = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "int",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: constraintValue
		});
		return this;
	};

	PreparedWhere.prototype.addCriteriaLong = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "long",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: constraintValue
		});
		return this;
	};

	PreparedWhere.prototype.addCriteriaDate = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "date",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: constraintValue
		});
		return this;
	};
	
	PreparedWhere.prototype.addCriteriaStrArray = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "array.string",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: $.isArray(constraintValue) ? constraintValue.join(",") : constraintValue
		});
		return this;
	};

	PreparedWhere.prototype.addCriteriaIntArray = function(fieldName, constraintType, constraintValue) {
		this.criterias.push({
			type: "array.int",
			fieldName: fieldName,
			constraintType: constraintType,
			constraintValue: $.isArray(constraintValue) ? constraintValue.join(",") : constraintValue
		});
		return this;
	};
	
	PreparedWhere.EQUALS			= "eq";
	PreparedWhere.NOT_EQUALS		= "ne";
	PreparedWhere.GREATER_THAN		= "gt";
	PreparedWhere.GREATER_OR_EQUAL	= "ge";
	PreparedWhere.LOWER_THAN		= "lt";
	PreparedWhere.LOWER_OR_EQUAL	= "le";
	PreparedWhere.IS_NULL			= "empty";
	PreparedWhere.IS_NOT_NULL		= "notempty";
	PreparedWhere.IN				= "in";
	PreparedWhere.NOT_IN			= "notin";
	PreparedWhere.LIKE				= "like";
	PreparedWhere.NOT_LIKE			= "notlike";

	PreparedWhere.prototype.orWhere = function(preparedWhere) {
		this.andOrWhere.push({or: preparedWhere});
		return this;
	};

	PreparedWhere.prototype.andWhere = function(preparedWhere) {
		this.andOrWhere.push({and: preparedWhere});
		return this;
	};

	return {
		load : internal_load,
		search : internal_search,
		PreparedWhere: PreparedWhere
	};
})(jQuery));
