/*global window, ev */

/**
 * Fonction anonyme de déclaration des classes de gestion
 * des timelines.
 * Certaines classes/fonctions ne sont visible que par
 * la(es) classe(s) concernée(s) (déclarée(s) 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).
 */
(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");}
	// On s'assure que le namespace ev.time existe
	if(!ev.time){ ev.time={}; }
	// Si la classe ev.time.Timeline est déjà déclarée, on sort
	if(ev.time.Timeline){return;}
		
	/**
	 * Singleton declared to hold timeline's events type enumeration, i.e.
	 * START, RUNNING, and STOP.
	 */
	ev.time.TimelineEventType={
		START: 0,
		RUNNING: 1,
		STOP: 2,
		toString: function(value){
			switch(value){
				case this.START: return "START";
				case this.RUNNING: return "RUNNING";
				case this.STOP: return "STOP";
				default: return "ev.time.TimelineEventType enumeration";
			}
		}
	};

	/**
	 * Abstract class designed to control a Timeline, this controls is a listener 
	 * whom #throwTimelineEvent(TimelineEvent) abstract method is invoked periodically by 
	 * the Timeline instance it controls. All TimelineListener subclass must 
	 * implement this method.
	 */
	ev.time.TimelineListener=function(){
		/**
		 * The method periodically invoked by the controled Timeline, it defines the 
		 * Timeline's behaviour.
		 * @param timlineEvent Contains a TimelineEvent instance produced by the controled Timeline.
		 * @throws If the method has not been implemented.
		 */
		this.throwTimelineEvent=function(timelineEvent){
			throw new Error("#throwTimelineEvent() must be overridden");
		};
	};
	
	/**
	 * Defines the event produced by a Timeline instance when it invokes 
	 * TimelineListener#throwTimelineEvent(TimelineEvent).
	 * @param _execCnt Step number from Timeline's instance last #start() invokation.
	 * @param _type Instance of TimelineEventType holding this TimelineEvent's type.
	 * @param _source The Timeline instance who produced this event.
	 */
	ev.time.TimelineEvent=function(_execCnt,_type,_source){
		if(_execCnt===undefined||_execCnt===null){throw new Error("execCnt is not valid");}
		if(typeof(_type)!=='number'){throw new Error("type is not a number");}
		if(_type!==ev.time.TimelineEventType.START&&_type!==ev.time.TimelineEventType.RUNNING&&_type!==ev.time.TimelineEventType.STOP){throw new Error("type is not START, RUNNING, or STOP");}
		if(!_source){throw new Error("source is not valid");}
		if(!(_source instanceof ev.time.Timeline||_source instanceof ev.time.Timeline)){throw new Error("source is not instance of ev.time.Timeline");}
		var date=new Date();
		this.getType=function() {
			return _type;
		};
		this.toString=function() {
			return "ev.time.TimelineEvent{executionCount="+_execCnt+", type="+ev.time.TimelineEventType.toString(_type)+", date="+date+", source="+_source+"}";
		};
		this.getDate=function() {
			return date;
		};
		this.getCount=function() {
			return _execCnt;
		};
		this.getSource=function() {
			return _source;
		};
	};

	/**
	 * Null Function.<br>
	 * Fonction qui ne fait rien.
	 */
	function NF(){}

	/**
	 * Fonction qui envoie un événement donné au listener donné.
	 * @param {ev.time.TimelineListener} l un tableau de TimelineListener à informer
	 * @param {ev.time.TimelineEvent} e l'événement à envoyer
	 * @throws If an error occur while sending event
	 */
	function fireEvent(l, e){
		var n=l.length;
		while(n--){
			try{
				l[n].throwTimelineEvent(e);
			}
			catch(r){
				window.setTimeout(function(){ throw r; }, 0);
			}
		}
	}

	/**
	 * This is the ev.time.Timeline constructor.
	 * 
	 * Timeline works as a Java thread, and manages to periodicaly invoke a 
	 * TimelineListener method. The timeline instance offers the methods to start and 
	 * stop the process. Implementation of behaviour is given to the implementation 
	 * of TimelineLister.
	 *
	 * Usage:
	 *	var timeline=new ev.time.Timeline(100);
	 *	var myTimelineListener=new ev.time.TimelineListener();
	 *	myTimelineListener.throwTimelineEvent=function(timelineEvent){
	 *		// Implement here behaviour of listener
	 *	};
	 *	timeline.addTimelineListener(myTimelineListener);
	 *		...
	 *	// starts timeline
	 *	timeline.start();
	 *		...
	 *	// stops timeline
	 *	timeline.stop();
	 *
	 * @param _delay Delay in ms between each invokation of 
	 * TimelineListener#throwTimelineEvent(TimelineEvent).
	 * @throws If namespace window.Classe is not defined
	 * @throws If _delay is null or undefined.
	 * @throws If _delay is not a number.
	 * @throws If _delay is negative or null.
	 * @throws If _delay is < 20.
	 */
	ev.time.Timeline=function(_name, _delay){
		if(!_delay){
			_delay=_name;
			_name='default';
		}
		if(!window.Classe){throw new Error("Namespace 'window.Classe' is not defined");}
		if(typeof(_delay)!=='number'){throw new Error("delay is not a number");}
		if(_delay<=0){throw new Error("delay is negative");}
		if(_delay<20){throw new Error("delay is too small (minimal value is 20)");}

		/************************************
		* Déclaration de variables privées. *
		************************************/
		var

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

		/**
		 * This private flag defines if this Timeline is running (i.e. started).
		 * [default = false]
		 */
		running=!1,

		/**
		 * This private field refers to the TimelineListener Array used by this instance 
		 * to manage their behaviour. Each delay (ms), #throwTimelineEvent(TimelineEvent) 
		 * method of each instance of TimelineListener is invoked, providing this timeline
		 * is running.
		 */
		listeners=[],

		/**
		 * Méthode à exécuter à chaque impulsion de la Timeline.<br>
		 * Cette est amenée à changer dés que l'on veut réellement
		 * qu'elle fonctionne. Au départ elle ne fait rien (c'est la
		 * fonction vide), donc si on l'éxécute, la Timeline ne fait
		 * rien. Dés qu'un listener sera ajouté la méthode sera
		 * modifiée et lancée à nouveau avec des actions à l'intérieur.
		 */
		execMethod=NF,

		/**
		 * this private field holds the count of 
		 * timelineListener#throwTimelineEvent(TimelineEvent) invokations since last 
		 * #start(), this count is given in TimelineEvent instances.
		 */
		execCnt=0;

		/**
		 * Méthode d'activation de la Timeline.<br>
		 * Cette méthode s'occupe de relancer les impulsions de la
		 * Timeline s'il y a au moins un listener.
		 * @param f fonction à lancer à chaque impulsion
		 */
		function resume(f){
			if(!f){throw new Error('argument 1: a function must be specified.');}
			if(typeof(f)!=='function'){throw new Error('argument 1: must be of type Function.');}
			// on utilise un while pour être sûr qu'aucune autre méthode js n'est appelée au même instant
			// cela simule une synchro entre les méthode 'resume()' et 'suspend()'
			while(listeners.length){
				// si la méthode est déjà la bonne, on sort (la Timeline est déjà fonctionnelle)
				if(execMethod===f){return;}
				// on fixe la méthode à lancer à chaque impulsion de la Timeline
				execMethod=f;
//				ev.log.debug('ev.time.timeline['+_name+'].__.resume()> resuming...');
				// on lance la méthode, pour relancer le mécanisme
				f();
				// on sort de la boucle virtuelle, sinon tout est mort ^_^x!
				return;
			}
		}

		/**
		 * Méthode de désactivation de la Timeline.<br>
		 * Cette méthode permet de stopper les impulsions
		 * envoyées aux listeners de la Timeline.<br>
		 * A utiliser par exemple, lorsqu'il n'y a plus de
		 * listener.
		 */
		function suspend(){
			// on utilise un while pour être sûr qu'aucune autre méthode js n'est appelée au même instant
			// cela simule une synchro entre les méthode 'resume()' et 'suspend()'
			while(!0){
				// on fixe la méthode vide sur 'execMethod', ce qui va suspendre le fonctionnement de la Timeline dés la prochaine impulsion
				execMethod=NF;
//				ev.log.debug('ev.time.timeline['+_name+'].__.suspend()> suspending...');
				// on sort de la boucle virtuelle, sinon tout est mort ^_^x!
				break;
			}
		}
	
		/**
		 * This private method executes itself recursively while this instance is running. 
		 * Mainly it invokes the #throwTimelineEvent(TimelineEvent) method of each
		 * TimelineListener of this instance.
		 * 
		 * @throws If an error occured while invoking #throwTimelineEvent(TimelineEvent)
		 * method on one of the TimelineListener Array
		 */
		function execute(){
			// si aucun listener on sort directement, c'est une impulsion en trop
			if(!listeners.length){return;}
//			ev.log.debug('ev.time.timeline['+_name+'].__.execute()> executing... ['+(running? 'on': 'off')+']');
			var t=ev.time.TimelineEventType.RUNNING;
			if(!running){
				// Si l'état n'est plus 'running', l'evenement est STOP
				t=ev.time.TimelineEventType.STOP;
			}
			else if(!execCnt){
				// Si le nombre d'execution est nul, l'evenement est START
				t=ev.time.TimelineEventType.START;
			}
//			ev.log.debug('ev.time.timeline['+_name+'].__.execute()> sending event... ['+t+']');
			fireEvent(listeners, new ev.time.TimelineEvent(execCnt, t, thisTimeline));
			if(t!==ev.time.TimelineEventType.STOP){
				window.setTimeout(execMethod, _delay);
				++execCnt;
			}
		}

		/**
		 * This method adds a TimelineListener instance as a listener of this 
		 * Timeline.
		 *
		 * @param _tl a TimelineListener that will listen to this 
		 * Timeline.
		 * @throws If _tl is null or undefined.
		 * @throws If _tl is not an instance of TimelineListener.
		 */
		this.addTimelineListener=function(_tl) {
			if(!_tl){throw new Error("the given Timeline listener is not valid");}
			if(!(window.Classe.isInstanceOf(_tl, ev.time.TimelineListener))){throw new Error("the given Timeline listener is not a 'ev.time.TimelineListener' instance");}
//			ev.log.debug('ev.time.timeline['+_name+'].addTimelineListener()> new listener : '+_tl);
			for(var l=listeners.length; l--;){
				if(listeners[l]===_tl){
					return;
				}
			}
			listeners.push(_tl);
			// si la Timeline est démarrée, on s'assure que le mécanisme soit en route
			if(running){
				resume(execute);
			}
		};

		/**
		 * This method removes a TimelineListener instance from this 
		 * Timeline if it exists. If it doesn't the method does nothing.
		 *
		 * @param _tl a TimelineListener that should be
		 *				listening to this Timeline.
		 * @throws If _tl is null or undefined.
		 * @throws If _tl is not an instance of TimelineListener.
		 */
		this.removeTimelineListener=function(_tl) {
			if(!_tl){throw new Error("timelineListener is not valid");}
			var l=listeners.length;
			while(l--){
				if(listeners[l]===_tl){
					listeners.splice(l, 1);
//					ev.log.debug('ev.time.timeline['+_name+'].removeTimelineListener()> listener deleted : '+_tl);
					break;
				}
			}
			if(!listeners.length){
				// s'il n'y a plus de listener, on suspend le mécanisme
				suspend();
			}
		};

		/**
		 * Indicates if the timeline is allready running.
		 */
		this.isRunning=function(){
			return running;
		};

		/**
		 * Starts this Timeline, i.e. this Timeline becomes running.
		 * @throws If thi Timeline is allready running.
		 */
		this.start=function() {
			if(running){throw new Error("timeline allready running");}
//			ev.log.info('ev.time.timeline['+_name+'].start()> ON');
			running=!0;
			execCnt=0;
			// on lance le mécanisme (la Timeline sera toutefois fonctionnelle seulement si au moins 1 listener existe)
			resume(execute);
		};
		/**
		 * Stops this Timeline. It becomes not running. If this Timeline is allready 
		 * not running, the method does nothing.
		 */
		this.stop=function() {
			// WARNING: This will stop this Timeline only during next delay step.
			running=!1;
//			ev.log.info('ev.time.timeline['+_name+'].stop()> OFF');
		};

		/**
		 * overrides #toString()
		 */
		this.toString=function(){
			return "ev.time.Timeline{delay="+_delay+", "+(running? "running": "off")+"}";
		};
	};

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