/*global window, ev */

/**
 * Fonction anonyme de déclaration des classes gérant
 * les chargements RJS.
 * Certaines fonctions ne sont visible que par les
 * classes concernées (déclarées ici).
 * Ce mécanisme de fonction anonyme permet de reproduire
 * un système d'encapsulation digne d'un langage de
 * programmation évolué (comme le Java).
 * Classes et éléments visibles seront stockés dans le
 * namespace 'ev.rjs'.
 */
(function(){
	// Si les namespaces/classes nécessaires ne sont pas chargées : exception
	if(!window.ev){throw new Error("Le namespace 'ev' doit exister");}
	if(!ev.tools){throw new Error("Le namespace 'ev.tools' doit exister");}
	if(!ev.log){throw new Error("Le namespace 'ev.log' doit exister");}
	if(!ev.time){throw new Error("Le namespace 'ev.time' doit exister");}
	if(!ev.time.Timeline){throw new Error("La classe 'ev.time.Timeline' doit exister");}
	if(!ev.Hashtable){throw new Error("La classe 'ev.Hashtable' doit exister");}
	// Si le namespace ev.rjs est déjà déclaré, on sort
	if(ev.rjs){return;}
	ev.rjs={
		/**
		 * Constante globale qui identifie l'URL racine des
		 * fichiers rjs statiques.
		 */
		URL_ROOT: (function(){
			if(new RegExp("http://seoul").test(window.location.href)){
				var port=window.location.href.toString().replace(new RegExp("^http://seoul[^:]*:"), "").replace(new RegExp("/.*$"), "");
//				ev.log.debug('Configuration RJS statiques pour SEOUL! (Développement ; port '+port+')');
				if(port.length>0){
					return "http://seoul.esv.lcom:"+port+"/rjs";
				}
				return "http://seoul.esv.lcom/rjs";
			}
//			ev.log.debug('Configuration RJS statiques pour PRODUCTION!');
			switch(window.lang){
				case "es_ES":
					return "http://www.easyviajar.com/rjs";
				case "it_IT":
					return "http://www.easyviaggio.com/rjs";
				case "en_GB":
					return "http://www.easyvoyage.co.uk/rjs";
				case "de_DE":
					return "http://www.easyvoyage.de/rjs";
				case "fr_FR":
					return "http://www.easyvoyage.com/rjs";
				default:
					return "http://www.easyvoyage.com/rjs";
			}
		})(), // exécution de la procédure ici
		/**
		 * Constante globale qui identifie l'URL racine des fichiers rjs spécifiques
		 * à l'ERA (Easyvoyage Remote API).
		 */
		URL_ERA_ROOT: (function(){
			if(new RegExp("http://seoul").test(window.location.href)){
				var port=window.location.href.toString().replace(new RegExp("^http://seoul[^:]*:"), "").replace(new RegExp("/.*$"), "");
//				ev.log.debug('Configuration RJS de l\'ERA pour SEOUL! (Développement ; port '+port+')');
				if(port.length>0){
					return "http://seoul.era.lfr:"+port;
				}
				return "http://seoul.era.lfr";
			}
			if(new RegExp("http://tokyo").test(window.location.href)){
				var port=window.location.href.toString().replace(new RegExp("^http://tokyo[^:]*:"), "").replace(new RegExp("/.*$"), "");
//				ev.log.debug('Configuration RJS de l\'ERA pour TOKYO! (Développement ; port '+port+')');
				if(port.length>0){
					return "http://tokyo.era.lfr:"+port;
				}
				return "http://tokyo.era.lfr";
			}
			if(new RegExp("http://test").test(window.location.href)){
				return "http://test.era.lfr";
			}
//			ev.log.debug('Configuration RJS de l\'ERA pour PRODUCTION!');
			//on décide de passer sur un unique non de domaine pour l'ERA
			return "http://era.easyvoyage.com/"+window.lang;
			/*switch(window.lang){
				case "es_ES":
					return "http://era.easyviajar.com";
				case "it_IT":
					return "http://era.easyviaggio.com";
				case "en_GB":
					return "http://era.easyvoyage.co.uk";
				case "de_DE":
					return "http://era.easyvoyage.de";
				case "fr_FR":
					if(new RegExp("easyvols.fr").test(window.location.href)){
						return "http://era.easyvols.fr";
					}
					if(new RegExp("easyvols.org").test(window.location.href)){
						return "http://era.easyvols.org";
					}
					return "http://era.easyvoyage.com";
				default:
					return "http://era.easyvoyage.com";
			}*/
		}()), // exécution de la procédure ici
		/**
		 * Multiple resource holder for multiple Locator use.
		 */
		multiple: {}
	};

	ev.log.debug('ev.rjs.js: ev.rjs.URL_ROOT='+ev.rjs.URL_ROOT);
	ev.log.debug('ev.rjs.js: ev.rjs.URL_ERA_ROOT='+ev.rjs.URL_ERA_ROOT);
	
	/**
	 * Instance privée de {@link ev.time.Timeline}.
	 * Cette variable est disponible seulement ici
	 * pour le fonctionnement des RJS d'une page.
	 */
	var TIMELINE=new ev.time.Timeline('RJS', 20);

	/**
	 * Instance privée de {@link ev.Hashtable}.
	 * Cette map agit comme un cache en stockant
	 * les ressources déjà téléchargées. (Table
	 * ne contenant que des ressources)
	 * Cette variable est disponible seulement ici
	 * pour le fonctionnement des RJS d'une page.
	 */
	var MAP=new ev.Hashtable();

	/**
	 * Enumération des différents états existants
	 * d'un localiseur de RJS.
	 * Cette énumération est disponible seulement ici.
	 */
	var LocatorState={
		/**
		 * Etat d'attente d'une ressource à invoquer.
		 */
		WAITING: 0,
		/**
		 * Etat d'invocation d'une ressource.
		 * (en attente d'invocation, de réponse ou du timeout)
		 */
		RUNNING: 1,
		/**
		 * Etat de remise à zéro après invocation d'une ressource.
		 */
		UNLOADING: 2,
		/**
		 * Etat de transition après remise à zéro, avant de revenir
		 * à un état d'attente.
		 */
		UNRUNNING: 3
	};

	/**
	 * FIXME Doc
	 * @param {Object} _resource la ressource téléchargée
	 * @param {Object} _returned la valeur retournée par la ressource
	 * @param {Object} _source le locator ayant créé l'événement
	 */
	ev.rjs.Event=function(_resource, _returned, _source){
	// TODO resource event's type of instance and nullity
		this.getResource=function(){
			return _resource;
		};
		this.getReturned=function(){
			return _returned;
		};
		this.getSource=function(){
			return _source;
		};
		this.toString=function(){
			return "ev.rjs.Event{"+_resource.toString()+", "+_returned+", "+_source.toString()+"}";
		};
	};

	/**
	 * This class holds a resource, i.e. an URL to a RJS class file and a method to 
	 * invoke in the RJS class that will be loaded.
	 * @param _url The URL to the file.
	 * @param _method The method to invoke.
	 */
	ev.rjs.Resource=function(_url) {
		this.getUrl=function(){
			return _url;
		};
		
		/**
		 * Méthode qui sera exécutée après invocation
		 * d'un script distant.
		 * Elle peut retourner un objet ou une valeur
		 * si besoin.
		 */
		this.execute=function(){
			// rien à faire ici (sera surchargée par script distant);
			return undefined;
		};
		/**
		 * Equality (used in cache) is defined by URL which correspond to a 
		 * particular instance.
		 */
		this.equals=function(other){
			return _url===other.getUrl();
		};
		this.toString=function(){
			return "ev.rjs.Resource{"+_url+"}";
		};
	};

	/**
	 * This class manages buffered invokations to RJS.<br>
	 * Moreover this class can instanciate a RJS class
	 * over a particular URL and invokes in this instance
	 * the method #execute() (that should be implemented,
	 * though no error will be thrown if it is not).<br>
	 * <br>
	 * Example:<br>
	 *	<code>
	 *	function MyRJSListener() {<br>
	 *		this.onRJSEvent=function(){<br>
	 *			// implement here<br>
	 *		};<br>
	 *	}<br>
	 *	var listener=new MyRJSListener();<br>
	 *	var rjsLocator=new ev.rjs.Locator(3000);// timeout de 3s<br>
	 *	rjsLocator.addListener(listener);<br>
	 *	var resource=new ev.rjs.Resource("http://www.xyz.nwh/aaa.rjs");<br>
	 *	rjsLocator.invoke(resource);// This will invoke #onRJSEvent on listener, when the resource's #execute() method is terminated<br>
	 *<br>
	 *	// the resource 'aaa.rjs' (located at 'http://www.xyz.nwh/')<br>
	 *	// should contain something like following, otherwise it will be<br>
	 *	// considered as missing (by reaching the chosen timeout delay) :<br>
	 *	ev.rjs.RJS=function(){<br>
	 *		this.execute=function(){<br>
	 *			...<br>
	 *		};<br>
	 *	};<br>
	 *	</code>
	 *	<br>
	 *	This means that the remote resource must define a resource
	 *	holder 'ev.rjs.RJS' as a function that creates an
	 *	#execute() method in the current context 'this'.<br>
	 *	The #invoke() function will use the ResourceHolder with
	 *	the given resource as current context.<br>
	 *	Then the resource will possess an #execute() method
	 *	(if correctly defined, like shown above) that will
	 *	called by the locator's timeline listener.<br>
	 *	<br>
	 *	This class uses a cache based on a hashtable to
	 *	prevent from re-loads of RJS instances. This cache
	 *	is held by the MAP hashtable.
	 *	@param {Integer} _timeout : The timeout in ms for a
	 *			resource to load. If timeout is reached we
	 *			consider the resource does not exist.
	 *	@param {String} _name (optional) : the name of the
	 *			locator, which allows to get more than one
	 *			resource at the same time (each resource
	 *			should be added in in respective
	 *			'ev.rjs.multiple[<_name>]' object)
	 *	TODO Create a process for no method invokation?
	 */
	ev.rjs.Locator=function(_timeout, _name){
		if(_timeout===undefined){throw new Error("timeout is undefined");}
		if(_timeout===null){throw new Error("timeout is null");}
		if(typeof(_timeout)!=="number"){throw new Error("timeout is not a number");}
		if(_timeout<=0){throw new Error("timeout is <=0");}

		/**
		 * This private property defines this instance reference
		 * that can be used in inner methods.
		 */
		var thisLocator=this,

		/**
		 * Private reference of node HEAD where 'script'
		 * nodes will be added.
		 */
		headNode=ev.dom.tags("HEAD")[0],
		
		/**
		 * Private reference of node 'script'.
		 * (house for RJS)
		 */
		scriptNode=(function (){
			var s=ev.dom.create('script');
			// Adding empty SCRIPT node to HEAD.
			s.type="text/javascript";
			s.charset="iso-8859-1";
			s.text="// Empty script";
			headNode.appendChild(s);
			return s;
		}()),
		
		/**
		 * Private reference of RJS invokations buffer. In this array are pushed all 
		 * resources, and then this class synchronized by its Timeline pops those 
		 * resources one by one, preventing rebounds.
		 */
		buffer=[],
	
		/**
		 * State property, synchronizes all process.
		 * Default is WAITING of course.
		 */
		state=LocatorState.WAITING,

		/**
		 * This property holds the timestamp of last RUNNING state, it is used 
		 * to compute eventual timeout.
		 */
		timestamp,

		/**
		 * This property holds the last resource asked to invoke.
		 * @see ev.rjs.Resource
		 */
		resource,

		/**
		 * Internal TIMELINE listener class managing buffered resources invokations.
		 */
		timelineListener=new ev.time.TimelineListener(),

		/**
		 * Tableau de listeners du client RJS actuel.
		 */
		listeners=[];

		/**
		 * @return the object that should contains the last resource called
		 * according to the name of the current Locator.
		 * If the Locator does not have any name, the resource holder will
		 * be 'ev.rjs.RJS' (In that case, there should be only one Locator
		 * in the current page).
		 */
		function getResourceHolder(){
			if(_name){
//				ev.log.debug("getResourceHolder(): ev.rjs.multiple."+_name+" ; "+ev.rjs.multiple[_name]+" ; RJS:"+ev.rjs.RJS);
				return ev.rjs.multiple[_name];
			}
//			ev.log.debug("getResourceHolder(): ev.rjs.RJS ; "+ev.rjs.RJS);
			return ev.rjs.RJS;
		}

		/**
		 * Reset the object that should contains the last resource called
		 * to the value 'undefined'.
		 */
		function resetResourceHolder(){
			if(_name){
//				ev.log.debug("resetResourceHolder(): ev.rjs.multiple."+_name+" ; "+ev.rjs.multiple[_name]+" ; RJS:"+ev.rjs.RJS);
				ev.rjs.multiple[_name]=undefined;
			}
			else{
//				ev.log.debug("resetResourceHolder(): ev.rjs.RJS ; "+ev.rjs.RJS);
				ev.rjs.RJS=undefined;
			}
		}

		/**
		 * Méthode d'activation du Locator.<br>
		 * Cette méthode s'occupe d'enregistrer le listener de
		 * l'objet Locator dans la TIMELINE et démarre cette
		 * TIMELINE si elle n'est pas encore démarrée.
		 */
		function resume(){
			// on lance l'écoute de la TIMELINE
			TIMELINE.addTimelineListener(timelineListener);
			// démarrage de la TIMELINE si ce n'est déjà fait
			if(!TIMELINE.isRunning()){
				TIMELINE.start();
			}
		}

		/**
		 * Méthode de désactivation du Locator.<br>
		 * Cette méthode s'occupe de supprimer le listener de
		 * l'objet Locator de la TIMELINE.
		 */
		function suspend(){
			// suppression du listener de l'objet de la TIMELINE
			TIMELINE.removeTimelineListener(timelineListener);
		}

		/**
		 * @return the locator's name (if defined).
		 */
		this.getName=function(){
			return _name;
		};

		/**
		 * Permet d'ajouter un listener à la fin du
		 * tableau de listeners.
		 * @param {Object} _listener : le listener à ajouter
		 */
		this.addListener=function(_listener){
			if(!_listener){throw new Error('Un listener ne doit pas être nul.');}
			if(typeof(_listener.onRJSEvent)!=='function'){throw new Error('Un listener doit posséder une méthode #onRJSEvent().');}
			listeners.push(_listener);
		};

		/**
		 * Permet de supprimer le listener donné, du
		 * tableau de listeners (s'il existe).
		 * @param {Object} _listener : le listener à supprimer
		 */
		this.removeListener=function(_listener){
			if(!_listener){return;}
			var cnt=listeners.length;
			for(var i=0; i<cnt; ++i){
				if(listeners[i]===_listener){
					// suppression du listener 'i' dans le tableau
					listeners.splice(i, 1);
					// et on sort
					return;
				}
			}
		};

		/**
		 * This method which overrides TimelineListener's manages 
		 * synchronization of invokations of resources stored in buffer. Method 
		 * {@link #invoke(ev.rjs.Resource)} has pushed resources in buffer, each 
		 * TIMELINE's step, for each of those resources the resource is invoked 
		 * (state RUNNING), then asked to unload (state UNLOADING), then really 
		 * unload (state UNRUNNING). If no more resource is present in the 
		 * buffer, the TIMELINE waits for a new resource to come (state 
		 * WAITING).
		 * @param {Object} e : timeline event
		 */
		timelineListener.throwTimelineEvent=function(e){
			switch(state){
				case LocatorState.WAITING:
					// In WAITING state, buffer was empty, and we test if a new resource 
					// was pushed in the buffer and is to be load and runned
					if(!buffer.length){
						// If buffers contains no data, there is nothing to load.
						suspend();
						break;
					}
					// The resource property is defined with the just popped 
					// resource from buffer
					resource=buffer[0];
					buffer.shift();
					// Removes node referenced by scriptNode from HEAD node. 
					// Replaces it by a new 'script' node with src matching to the 
					// resource's URL.
					headNode.removeChild(scriptNode);
					scriptNode=ev.dom.create("script");
					scriptNode.type="text/javascript";
					scriptNode.charset="iso-8859-1";
					scriptNode.src=resource.getUrl();
					// May cause a non blocking JS error in IE when src does not 
					// exist. this error cannot be catch.
					headNode.appendChild(scriptNode);
					// tiemstamp is renewed
					timestamp=new Date().getTime();
					// state is changed to RUNNING until the RJS is loaded
					state=LocatorState.RUNNING;
					break;
				case LocatorState.RUNNING:
					// A new 'script' node with its scr set to the resource URL has been 
					// added to HEAD node. Navigator will take some time to load the 
					// file and run it. Trying to instanciate RJS will throw an 
					// exception until the file is arrived. So when all loaded no 
					// exception is to be catched.
					
//ev.log.info('LocatorState: RUNNING ['+resource.getUrl()+']');
					// if timeout is reached we unload the 'script' node if not we try 
					// to instanciate a new RJS
					if(new Date().getTime()-timestamp>=_timeout){
						ev.log.error("RJSTimeoutException after "+(_timeout/1000)+" seconds on resource : "+resource);
						state=LocatorState.UNLOADING;
						break;
					}
					if(!getResourceHolder()){break;}
					// If the RJS is not ready that will throw an exception
					try{
//ev.log.debug('BEFORE ResourceHolder execution...');
						if(typeof(getResourceHolder())==='function'){
						        getResourceHolder().call(resource);
						}
						else if(getResourceHolder().execute&&typeof(getResourceHolder().execute)==='function'){
						        resource.execute=getResourceHolder().execute;
						}
//ev.log.debug('AFTER  ResourceHolder execution... ['+resource.getUrl()+']');
						// If the RJS has been correctly instanciated its method 
						// is invoked, and state is set to unload the 'script'
						// node
						MAP.put(resource, resource);
						state=LocatorState.UNLOADING;
						var event;
						if(typeof(resource.execute)==='function'){
							event=new ev.rjs.Event(resource, resource.execute(), thisLocator);
						}
						else{
							event=new ev.rjs.Event(resource, undefined, thisLocator);
						}
						for(var i=0;i<listeners.length;i++){
//ev.log.info('FIRE (post-invoke) ['+resource.getUrl()+'] : '+i);
							// We asure here that the listener doesn't make the event fail
							try {
								listeners[i].onRJSEvent(event);
							}
							catch(eOnEvent){
								ev.log.error("RJSListener Exception: "+eOnEvent);
							}
						}
					}
					catch(eRunning){
						ev.log.error(eRunning);
						resetResourceHolder();
						var errEvent=new ev.rjs.Event(resource, eRunning, thisLocator);
						for(var j=0;j<listeners.length;j++){
//ev.log.warn('FIRE After error (post-invoke) : '+j);
							// We asure here that the listener doesn't make the event fail
							try {
								listeners[j].onRJSEvent(errEvent);
							}
							catch(eOnErrorEvent){
								ev.log.error("RJSListener Exception (on error): "+eOnErrorEvent);
							}
						}
						state=LocatorState.UNLOADING;
					}
					break;
				case LocatorState.UNLOADING:
					// Unloading replaces the precedent 'script' node by a new one which 
					// throws an Error when completely loaded (this behaviour is opposed 
					// to the formal 'script' node that was NOT throwing an exception). 
					// When this node is changed the state is changed to UNRUNNING that 
					// will try to run the RJS class (the one that throws the error)
//ev.log.info('LocatorState: UNLOADING ['+resource.getUrl()+']');
					headNode.removeChild(scriptNode);
//					scriptNode=ev.dom.create("script");
//					scriptNode.type="text/javascript";
//					scriptNode.charset="iso-8859-1";
//					if(_name){
//						scriptNode.text=
//							"ev.rjs.multiple[\""+_name+"\"]=function(){" +
//							"	throw new Error(\"NULL\");" +
//							"};";
//					}
//					else{
//						scriptNode.text=
//							"ev.rjs.RJS=function(){" +
//							"	throw new Error(\"NULL\");" +
//							"};";
//					}
//					headNode.appendChild(scriptNode);
//					state=LocatorState.UNRUNNING;
					// Adding empty SCRIPT node to HEAD.
					scriptNode=ev.dom.create("script");
					scriptNode.type="text/javascript";
					scriptNode.charset="iso-8859-1";
					scriptNode.text="// Empty script";
					headNode.appendChild(scriptNode);
					resetResourceHolder();
					state=LocatorState.WAITING;
					break;
				case LocatorState.UNRUNNING:
//FIXME revoir commentaires si on supprime vraiment cela !!!
					ev.log.fatal("LocatorState.UNRUNNING : should NEVER be here!");
					// Tries to catch the error made by the RJS loaded in the new 
					// 'script' node. When this script is loaded the error is thrown 
					// catched and state is changed to WAITING
//ev.log.info('LocatorState: UNRUNNING ['+resource.getUrl()+']');
					try{
						if(getResourceHolder()){
//ev.log.debug('BEFORE unrunning ResourceHolder execution...');
							getResourceHolder()();
//ev.log.debug('AFTER  unrunning ResourceHolder execution...');
						}
					}
					catch(eUnload){
//ev.log.debug('Exception (normal) on unrunning ResourceHolder execution... ['+(resource? resource.getUrl():"null")+']');
						resetResourceHolder();
						state=LocatorState.WAITING;
					}
					break;
				default:
					break;
			}
		};

		/**
		 * This method invokes the given resource.
		 * @param resource An instance of RJSResource to invoke.
		 * @param force [optionel] parametre permettant de préciser
		 *   s'il on souhaite absoluement faire la requete (true) ou
		 *   s'il on souhaite utiliser le cache (false) lorsque la
		 *   requête a déjà été exécutée
		 */
		this.invoke=function(resource, force){
			var lastResource=MAP.get(resource);
			if(lastResource!==null&&!force){
				// If resource is cached we use it
//ev.log.info("pas d'invocation...");
				var event;
				if(typeof(lastResource.execute)==='function'){
					event=new ev.rjs.Event(resource, lastResource.execute(), thisLocator);
				}
				else{
					event=new ev.rjs.Event(resource, undefined, thisLocator);
				}
				for(var i=0;i<listeners.length;i++){
//ev.log.debug('FIRE (no invoke) ['+lastResource.getUrl()+'] : '+i);
					listeners[i].onRJSEvent(event);
				}
			}
			else{
				// else push the resource in buffer, TIMELINE will do the job next time
//ev.log.debug('1st time for \''+resource.toString()+'\' => dans buffer!');
				buffer.push(resource);
				// on lance l'écoute de la TIMELINE
				resume();
			}
		};

		/**
		 * Méthode donnant un aperçu bref du locator.
		 */
		this.toString=function(){
			return "ev.rjs.Locator{"+(_name? _name: "no name")+", timeout: "+_timeout+"}";
		};
	};
	
	ev.rjs.simpleInvoke=function(loc, url, after, force){
		if(!loc){throw new Error('argument 1 : a Locator must be specified');}
		if(!(loc.constructor&&loc.constructor===ev.rjs.Locator)){throw new Error('argument 1 : the Locator must be of type ev.rjs.Locator');}
		if(!url){throw new Error('argument 2 : an URL must be specified');}
		var res=new ev.rjs.Resource(url), r;
		var t0, t1;
		loc.addListener({
			onRJSEvent: function(e){
				if(!e){throw new Error('argument 1 : an Event must be specified');}
				if(!(e.constructor&&e.constructor===ev.rjs.Event)){throw new Error('argument 1 : the Event must be of type ev.rjs.Event');}
				// s'il ne s'agit pas de la ressource attendue, on sort
				if(e.getResource()!==res){return;}
				t1=new Date().getTime();
				// ici, on peut dire que la ressource attendue est arrivée, on n'écoute plus les événements du Locator
				loc.removeListener(this);
				r=e.getReturned();
				if(t1-t0>500){
					ev.log.debug('rjs.simpleInvoke()> Ressource \''+res+'\' reçue ['+(t1-t0)+' ms]');
				}
				// S'il y a un exception dans la ressource, on l'affiche
				if(r&&r.exception){
					ev.log.error('rjs.simpleInvoke()> Ressource \''+res+'\' - Exception : '+r.exception);
				}
//				else{
//					ev.log.info('rjs.simpleInvoke()> Ressource \''+res+'\' '+res+' récupérée sans erreur [after callback: '+!!after+']');
//				}
				// une fois l'événement contrôlé, on le transfère à la fonction de callback donnée, le cas échéant
				if(typeof(after)==='function'){
//					ev.log.info('rjs.simpleInvoke()> sending returned value \''+r+'\' to callback : '+after+" from ressource: "+e.getResource().getUrl());
					after(r);
				}
			}
		});
//		ev.log.debug('rjs#simpleInvoke()> invoking \''+url+'\'...');
		t0=new Date().getTime();
		loc.invoke(res, force);	
	};

	ev.log.debug('ev.rjs ok');
	ev.tools.onFileLoad('ev/rjs.js');
}()); // exécution de la fonction anonyme, ici