

/**
 * 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() {
	//'use strict';
	// raccourci vers l'objet window de la page
	var WIN = this,
			// raccourci vers la package principal d'easyvoyage (ev)
			EASY = WIN.ev,
			// raccourci vers le package ev.time (pakage principal du fichier courant)
			TIME,
			// raccourcis vers des méthodes utiles
			setTimeout = WIN.setTimeout;

	if (!EASY) {throw "time.Timeline#<clinit>: Le namespace 'ev' doit exister";}

	// On s'assure que le namespace ev.time existe
	TIME = EASY.time;
	if(!TIME) {
		EASY.time = {};
		TIME=EASY.time;
	}

	// Si la classe ev.time.Timeline est déjà déclarée, on sort
	if (TIME.Timeline) {return;}

	/**
	 * Singleton declared to hold timeline's events type enumeration, i.e.
	 * START, RUNNING, and STOP.
	 */
	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.
	 */
	TIME.TimelineListener = function() {
		/**
		 * The method periodically invoked by the controled Timeline, it defines the
		 * Timeline's behaviour.
		 * @param {!Object} timelineEvent 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 {!number} _execCnt Step number from Timeline's instance last #start() invokation.
	 * @param {!number} _type Instance of TimelineEventType holding this TimelineEvent's type.
	 * @param {!Object} _source The Timeline instance who produced this event.
	 */
	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('time.TimelineEvent#<init>: type is not a number ['+_type+']');}
		if (_type !== TIME.TimelineEventType.START && _type !== TIME.TimelineEventType.RUNNING && _type !== 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 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=' + 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 {!Object} l un tableau de TimelineListener à informer.
	 * @param {!Object} e l'événement à envoyer.
	 * @throws If an error occur while sending event
	 */
	function fireEvent(l, e) {
		var n = l.length;
		while (n) {
			--n;
			try {
				l[n].throwTimelineEvent(e);
			}
			catch (r) {
				if (WIN.console) {
					WIN.console.error(r);
				}
			}
		}
	}

	/**
	 * 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 {?string} _name name of the Timeline.
	 * @param {!number} _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.
	 */
	TIME.Timeline = function(_name, _delay) {
		if (!_delay) {
			_delay = _name;
			_name = 'default';
		}
		if (!WIN.Classe) {throw 'Namespace window.Classe is not defined';}
		if (typeof(_delay) !== 'number') {throw 'delay is not a number';}
		if (_delay <= 0) {throw 'delay is negative';}
		if (_delay < 20) {throw 'delay is too small (minimal value is 20)';}

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

		// This private property defines this instance reference that can be used in inner methods.
		var theTimeline = 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 {!Function} f fonction à lancer à chaque impulsion.
		 */
		function resume(f) {
			if (typeof(f) !== 'function') { throw '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;
				//        EASY.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!
				if (WIN) { 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 fixe la méthode vide sur 'execMethod', ce qui va suspendre le fonctionnement de la Timeline dés la prochaine impulsion
			execMethod = NF;
			//      EASY.log.debug('ev.time.timeline['+_name+'].__.suspend()> suspending...');
		}

		/**
		 * 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;}
			//      EASY.log.debug('ev.time.timeline['+_name+'].__.execute()> executing... ['+(running? 'on': 'off')+']');
			var t = TIME.TimelineEventType.RUNNING;
			if (!running) {
				// Si l'état n'est plus 'running', l'evenement est STOP
				t = TIME.TimelineEventType.STOP;
			}
			else if (!execCnt) {
				// Si le nombre d'execution est nul, l'evenement est START
				t = TIME.TimelineEventType.START;
			}
			//      EASY.log.debug('ev.time.timeline['+_name+'].__.execute()> sending event... ['+t+']');
			fireEvent(listeners, new TIME.TimelineEvent(execCnt, t, theTimeline));
			if (t !== TIME.TimelineEventType.STOP) {
				setTimeout(function() {execMethod();},_delay); // execMethod est wrappé dans une fonction annonyme, pour corriger un bug sur IE9..
				++execCnt;
			}
		}

		/**
		 * This method adds a TimelineListener instance as a listener of this
		 * Timeline.
		 *
		 * @param {!Object} _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.
		 */
		theTimeline.addTimelineListener = function(_tl) {
			if (!_tl) {throw 'the given Timeline listener is not valid';}
			if (!(WIN.Classe.isInstanceOf(_tl, TIME.TimelineListener))) {throw 'the given Timeline listener is not a ev.time.TimelineListener instance';}
			//      EASY.log.debug('ev.time.timeline['+_name+'].addTimelineListener()> new listener : '+_tl);
			var l;
			for (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 {!Object} _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.
		 */
		theTimeline.removeTimelineListener = function(_tl) {
			if (!_tl) {throw 'time.Timeline#removeTimelineListener(): timelineListener is not valid';}
			var l = listeners.length;
			while (l--) {
				if (listeners[l] === _tl) {
					listeners.splice(l, 1);
					//          EASY.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.
		 */
		theTimeline.isRunning = function() {
			return running;
		};

		/**
		 * Starts this Timeline, i.e. this Timeline becomes running.
		 * @throws If thi Timeline is allready running.
		 */
		theTimeline.start = function() {
			if (running) {throw 'time.Timeline#start(): timeline allready running';}
			//      EASY.log.info('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.
		 */
		theTimeline.stop = function() {
			// WARNING: This will stop this Timeline only during next delay step.
			running = !1;
			//      EASY.log.info('time.timeline['+_name+']#stop(): OFF');
		};

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

	EASY.log.debug('time.Timeline#<clinit>: OK');
}()); // exécution de la fonction anonyme, ici

