///<reference path="all.ts"/>
module jg {
	/*
	Copyright (c) Ubiquitous Entertainment Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
	*/
	/**
	 * アニメーションを管理するためのクラス.
	 *
	 * 操作するエンティティひとつに対して、必ずひとつのタイムラインが対応する。
	 * Timelineクラス を読み込むと、Entity クラスを継承したすべてのクラスの
	 * tl プロパティに、タイムラインクラスのインスタンスが生成される。
	 * 
	 * タイムラインクラスは、自身に様々なアクションを追加するメソッドを持っており、
	 * これらを使うことで簡潔にアニメーションや様々な操作をすることができる。
	 * タイムラインクラスはフレームとタイムのアニメーションができる。
	 * 
	 * 元ソースはenchant.jsに提供されていたtl.enchant.jsです。
	 * http://enchantjs.com/ja/
	 *
	 * @param entity 操作の対象となるEntity
	*
	 */
	export class Timeline {
		/** 操作対象のEntity */
		entity: E;
		/** 対象の全アクション */
		queue: Action[];
		/** 停止中かどうか */
		paused: boolean;
		/** ループするかどうか */
		looped: boolean;
		/** */
		_activated: boolean;
		/** */
		_parallel: ParallelAction;
		/** フレームベースかどうか。jgame.jsではデフォルトfalseで、変更は推奨されない */
		isFrameBased: boolean;

		/**
		 *
		 * @param entity
		 */
		constructor(entity:E) {
			this.entity = entity;
			this.queue = [];
			this.paused = false;
			this.looped = false;
			this._parallel = null;
			this._activated = false;
		}

		/**
		 *
		 * @param force
		 */
		_deactivateTimeline(force?:boolean) {
			if (force || this._activated) {
				this._activated = false;
				if (this.entity.scene)
					this.entity.scene.game.update.remove(this, this.tick);
				else
					this.entity.addActiveQueue(() => {
						this.entity.scene.game.update.remove(this, this.tick);
					});
			}
		}

		/**
		 *
		 * @param force
		 */
		_activateTimeline(force?:boolean) {
			if (force || (!this._activated && !this.paused)) {
				if (this.entity.scene)
					this.entity.scene.game.update.handle(this, this.tick);
				else
					this.entity.addActiveQueue(() => {
						this.entity.scene.game.update.handle(this, this.tick);
					});
				this._activated = true;
			}
		}

		/**
		 * フレームベースのアニメーションにする
		 */
		setFrameBased() {
			this.isFrameBased = true;
		}

		/**
		 * 時間ベースのアニメーションにする
		 */
		setTimeBased() {
			this.isFrameBased = false;
		}

		/**
		 * キューの先頭にあるアクションを終了し、次のアクションへ移行する。
		 * @param remainingTime
		 */
		next(remainingTime?:number) {
			var action = this.queue.shift();
			if (! action)	//おそらくdestoryされてしまっている
				return;
			if (action.action_end) {
				action.action_end.fire({timeline: this});
			}

			if (this.queue.length === 0 && !this.looped) {
				action.removed_from_timeline.fire({timeline: this});
				this._deactivateTimeline(true);
				return;
			}

			if (this.looped) {
				action.removed_from_timeline.fire({timeline: this});
				action.frame = 0;

				this.add(action);
			} else {
				// remove after dispatching removedfromtimeline event
				action.removed_from_timeline.fire({timeline: this});
			}
			if (remainingTime > 0 || (this.queue[0] && this.queue[0].time === 0)) {
				this.tick(remainingTime);
			}
		}

		/**
		 * 時間経過処理
		 * @param t 経過時間
		 */
		tick(t:number) {
			if (this.paused) {
				return;
			}
			if (this.queue.length > 0) {
				var action = this.queue[0];
				if (action.frame === 0) {
					if (action.action_start) {
						action.action_start.fire({timeline: this});
					}
				}

				action.action_tick.fire({
					timeline: this,
					elapsed: (this.isFrameBased) ? 1 : t
				})
			}
		}

		/**
		 * 新しいアクションを追加する
		 * @param action 追加するアクション
		 */
		add(action:Action) {
			if (!this._activated) {
				this._activateTimeline(true);
			}
			if (this._parallel) {
				this._parallel.actions.push(action);
				this._parallel = null;
			} else {
				this.queue.push(action);
			}
			action.frame = 0;

			action.added_to_timeline.fire({timeline: this});

			return this;
		}

		/**
		 * アクションを簡単に追加するためのメソッド。実体は add メソッドのラッパ。
		 * @param params アクションの設定オブジェクト
		 */
		action(params:any) {
			return this.add(new Action(params));
		}

		/**
		 * トゥイーンを簡単に追加するためのメソッド。実体は add メソッドのラッパ。
		 * @param  params トゥイーンの設定オブジェクト。
		 */
		tween(params:any) {
			return this.add(new Tween(params));
		}

		/**
		 * タイムラインのキューをすべて破棄する。終了イベントは発行されない。
		 */
		clear() {
			for (var i = 0, len = this.queue.length; i < len; i++) {
				this.queue[i].removed_from_timeline.fire({timeline:this});
			}
			this.queue = [];
			this._deactivateTimeline();
			return this;
		}

		/**
		 * タイムラインを早送りする。巻き戻しは出来ない
		 * @param frames 早送りするフレーム数（jgame.jsの場合は時間）
		 */
		skip(frames:number) {
			var e:any = {}
			if (this.isFrameBased) {
				e.elapsed = 1;
			} else {
				e.elapsed = frames;
				frames = 1;
			}
			while (frames--) {
				this.tick(e);
			}
			return this;
		}

		/**
		 * タイムラインの実行を一時停止する
		 */
		pause() {
			if (!this.paused) {
				this.paused = true;
				this._deactivateTimeline();
			}
			return this;
		}

		/**
		 * タイムラインの実行を再開する
		 */
		resume() {
			if (this.paused) {
				this.paused = false;
				this._activateTimeline();
			}
			return this;
		}

		/**
		 * タイムラインをループさせる。
		 */
		loop() {
			this.looped = true;
			return this;
		}

		/**
		 * タイムラインのループを解除する。
		 */
		unloop() {
			this.looped = false;
			return this;
		}

		/**
		 * 指定した時間を待ち、何もしないアクションを追加する。
		 * @param time 待ち時間
		 */
		delay(time:number) {
			this.add(new Action({
				time: time
			}));
			return this;
		}

		/**
		 * 関数を実行し、即時に次のアクションに移るアクションを追加する。
		 * @param func 実行する関数
		 */
		then(func:Function) {
			var action = new Action({time:0});
			action.action_tick.handleInsert(0, action, (e:ActionTickEventArgs) => {
				func.call(this.entity, e);
			});
			this.add(action);
			return this;
		}

		/**
		 * フレームを変更する。このメソッドを実行する場合、EntityがFrameSpriteである必要がある
		 * @param wait この時間待ってから操作を実行
		 * @param frame 切り替え後のフレーム。詳細はFrameSprite.frameを参照。省略すると待ち時間無しで第一引数に指定したフレームに変更する
		 */
		frame(wait:any, frame?:number[]) {
			var s = <FrameSprite>this.entity;
			if (frame == undefined) {
				this.then(() => {s.frame = wait; s.changeFrame(); })
			} else {
				this.delay(wait).then(() => { s.frame = frame; s.changeFrame(); });
			}
			return this;
		}

		/**
		 * フレーム番号を変更する
		 * @param wait この時間待ってから操作を実行
		 * @param fno 切り替え後のフレーム番号。詳細はFrameSprite.fnoを参照。省略すると待ち時間無しで第一引数に指定したフレーム番号に変更する
		 */
		fno(wait:number, fno?:number) {
			var s = <FrameSprite>this.entity;
			if (fno == undefined) {
				this.then(() => {s.fno = wait; s.changeFrame(); })
			} else {
				this.delay(wait).then(() => { s.fno = fno; s.changeFrame(); });
			}
			return this;
		}

		/**
		 * 実行したい関数を、経過時間をキーとした連想配列(オブジェクト)で複数指定し追加する。
		 * sprite.tl().cue({
		 *    10: function(){ 10msec後に実行される関数 },
		 *    20: function(){ 20msec後に実行される関数 },
		 *    30: function(){ 30msec後に実行される関数 }
		 * });
		 * @param cue 経過時間をキーとした連想配列
		 */
		cue(cue:any) {
			var ptr = 0;
			for (var frame in cue) {
				var f = parseInt(frame);
				if (cue.hasOwnProperty(frame)) {
					this.delay(f - ptr);
					this.then(cue[frame]);
					ptr = f;
				}
			}
		}

		/**
		 * 指定した関数を指定した時間繰り返し実行するアクションを追加する。
		 * @param func 実行する関数
		 * @param time 時間
		 */
		repeat(func:Function, time:number) {
			var action = new Action({time:time});
			action.action_tick.handle(action, (e:ActionTickEventArgs) => {
				func.call(action, e);
			});
			return this;
		}

		/**
		 * 複数のアクションを並列で実行したいときに指定する。
		 * and で結ばれたすべてのアクションが終了するまで次のアクションには移行しない
		 * 300msecでフェードインしながら360度回転する例
		 * sprite.tl().fadeIn(300).and.rotateBy(360, 300);
		 */
		and() {
			var last = this.queue.pop();
			if (last instanceof ParallelAction) {
				this._parallel = <ParallelAction>last;
				this.queue.push(last);
			} else {
				var parallel = new ParallelAction();
				parallel.actions.push(last);
				this.queue.push(parallel);
				this._parallel = parallel;
			}
			return this;
		}

		/**
		 * true値 が返るまで、関数を毎フレーム実行するアクションを追加する。
		 * @param func 実行する関数
		 */
		waitUntil(func:Function) {
			var action = new Action();
			action.action_start = new Trigger();
			//action.action_start.handle(action, func);
			action.action_tick.handle(action, (e:ActionTickEventArgs) => {
				if (func.call(action, e))
					this.next(0);
			});
			this.add(action);
			return this;
		}

		/**
		 * 指定座標に移動する
		 * @param x X座標
		 * @param y Y座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		moveTo(x:number, y:number, time:number, easing?:Function) {
			return this.tween({
				x: x,
				y: y,
				time: time,
				easing: easing
			});
		}

		/**
		 * 指定したX座標に移動する
		 * @param x X座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		moveX(x:number, time:number, easing?:Function) {
			return this.tween({
				x: x,
				time: time,
				easing: easing
			});
		}

		/**
		 * 指定したY座標に移動する
		 * @param y Y座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		moveY(y:number, time:number, easing?:Function) {
			return this.tween({
				y: y,
				time: time,
				easing: easing
			});
		}

		/**
		 * 現在位置から指定した座標分移動する
		 * @param x X座標の変化量
		 * @param y Y座標の変化量
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		moveBy(x:number, y:number, time:number, easing?:Function) {
			return this.tween({
				x: function() {
					return this.x + x;
				},
				y: function() {
					return this.y + y;
				},
				time: time,
				easing: easing
			});
		}

		/**
		 * 指定座標にスクロールする
		 * @param x X座標
		 * @param y Y座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		scrollTo(x:number, y:number, time:number, easing?:Function) {
			if (! this.entity.scroll)
				this.entity.scrollTo(0, 0);
			return this.tween({
				scroll: {x: x, y: y},
				time: time,
				easing: easing
			});
		}

		/**
		 * 指定したX座標にスクロールする
		 * @param x X座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		scrollX(x:number, time:number, easing?:Function) {
			if (! this.entity.scroll)
				this.entity.scrollTo(0, 0);
			return this.tween({
				scroll: {x: x},
				time: time,
				easing: easing
			});
		}

		/**
		 * 指定したY座標にスクロールする
		 * @param y Y座標
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		scrollY(y:number, time:number, easing?:Function) {
			if (! this.entity.scroll)
				this.entity.scrollTo(0, 0);
			return this.tween({
				scroll: {y: y},
				time: time,
				easing: easing
			});
		}

		/**
		 * 現在位置から指定した座標分スクロールする
		 * @param x X座標の変化量
		 * @param y Y座標の変化量
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		scrollBy(x:number, y:number, time:number, easing?:Function) {
			if (! this.entity.scroll)
				this.entity.scrollTo(0, 0);
			return this.tween({
				scroll: {
					x: function() {
						return this.scroll ? this.scroll.x + x : x;
					},
					y: function() {
						return this.scroll ? this.scroll.y + y : y;
					}
				},
				time: time,
				easing: easing
			});
		}

		/**
		 * Entityの不透明度をなめらかに変えるアクションを追加する。
		 * @param opacity 目標の不透明度
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		fadeTo(opacity:number, time:number, easing?:Function) {
			this.tween({
				opacity: opacity,
				time: time,
				easing: easing
			});
			return this;
		}

		/**
		 * Entityをなめらかに表示するアクションを追加する。
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		fadeIn(time:number, easing?:Function) {
			return this.fadeTo(1, time, easing);
		}

		/**
		 * Entityをなめらかに非表示にするアクションを追加する。
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		fadeOut(time:number, easing?:Function) {
			return this.fadeTo(0, time, easing);
		}

		/**
		 * Entityを即座に非表示にする
		 */
		hide() {
			return this.then(function() {
				this.hide();
			});
		}
		/**
		 * Entityを即座に表示する
		 */
		show() {
			return this.then(function() {
				this.show();
			});
		}

		/**
		 * 指定した大きさにサイズを変更する。scaleToが中心から拡大されるのに対し、resizeToは右方向へ拡大される
		 * @param size widthまたはwidthとheight。widthのみを指定する場合、第二引数にheightを指定する
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 * @param easing2 heightを指定した場合ここがeasing関数になる
		 */
		resizeTo(size:number, time:number, easing?:any, easing2?:any) {
			if (typeof easing === "number") {
				return this.tween({
					width: size,
					height: time,
					time: easing,
					easing: easing2
				});
			}
			return this.tween({
				width: size,
				height: size,
				time: time,
				easing: easing
			});
		}

		/**
		 * 現在の大きさから指定した量サイズを変化させる。scaleByが中心から拡大されるのに対し、resizeByは右方向へ拡大される
		 * @param size widthまたはwidthとheightの変化量。widthのみを指定する場合、第二引数にheightを指定する
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 * @param easing2 heightを指定した場合ここがeasing関数になる
		 */
		resizeBy(size:number, time:number, easing?:any, easing2?:any) {
			if (typeof easing === "number") {
				return this.tween({
					width: function() {
						return this.width + size;
					},
					height: function() {
						return this.height + time;
					},
					time: easing,
					easing: easing2
				});
			}
			return this.tween({
				width: function() {
					return this.width + size;
				},
				height: function() {
					return this.height + size;
				},
				time: time,
				easing: easing
			});
		}

		/**
		 * Entity をなめらかに拡大・縮小するアクションを追加する。
		 * @param scale
		 * @param time
		 * @param easing Easing関数。省略した場合はLINEAR
		 * @param easing2
		 */
		scaleTo(scale:number, time:number, easing?:any, easing2?:any) {
			if (typeof easing === "number") {
				return this.tween({
					scale: {x: scale, y:time},
					time: easing,
					easing: easing2
				});
			}
			return this.tween({
				scale: {x: scale, y: scale},
				time: time,
				easing: easing
			});
		}

		/**
		 * Entity をなめらかに拡大・縮小するアクションを追加する。
		 * @param scale
		 * @param time
		 * @param easing Easing関数。省略した場合はLINEAR
		 * @param easing2
		 */
		scaleBy(scale:number, time:number, easing?:any, easing2?:any) {
			if (typeof easing === "number") {
				return this.tween({
					scale: function() {
						var _scale = this.getDrawOption("scale");
						return {
							x: _scale.x * scale,
							y: _scale.y * time
						}
					},
					time: easing,
					easing: easing2
				});
			}
			return this.tween({
				scale: function() {
					var _scale = this.getDrawOption("scale");
					return {
						x: _scale.x * scale,
						y: _scale.y * scale
					}
				},
				time: time,
				easing: easing
			});
		}

		/**
		 * Entity をなめらかに回転させるアクションを追加する。
		 * @param deg 目標の回転角度 (弧度法: 1回転を 360 とする)
		 * @param time フレーム数
		 * @param easing Easing関数。省略した場合はLINEAR
		*/
		rotateTo(deg:number, time:number, easing?:Function) {
			return this.tween({
				rotate: deg,
				time: time,
				easing: easing
			});
		}

		/**
		 * Entity をなめらかに回転させるアクションを追加する。
		 * @param deg 目標の回転角度 (弧度法: 1回転を 360 とする)
		 * @param time フレーム数
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		rotateBy(deg:number, time:number, easing?:Function) {
			return this.tween({
				rotate: function() {
					return this.getDrawOption("rotate") + deg;
				},
				time: time,
				easing: easing
			});
		}

		/**
		 * フィルタをかける
		 * @param targetClass ImageFilter.TintFilterなど、対象のフィルタクラスを指定する
		 * @param props フィルタクラスに指定するプロパティ値。{propName: {start: val, end: val}}の形式で指定
		 * @param time 必要時間
		 * @param easing Easing関数。省略した場合はLINEAR
		 */
		filter(targetClass:Function, props:any, time:number, easing?:Function) {
			var filterVal = {targetClass: targetClass, autoDelete:true}
			for (var i in props)
				filterVal[i] = props[i];
			return this.tween({
				filter: filterVal,
				time: time,
				easing: easing
			});
		}

		/**
		 * Entity をシーンから削除する。
		 */
		removeFromScene() {
			return this.then(function() {
				this.remove();
			});
		}
	}

	//jgame.jsの場合は時間ベースなのでその設定
	Timeline.prototype.isFrameBased = false;
}