///<reference path="all.ts"/>
module jg {
	/**
	 * 描画オプションをあらわすインターフェース
	 */
	export interface IEntityOptions {
		/** 回転確度 */
		rotate: number;
		/** 位置 */
		translate: CommonOffset;
		/** 変形 */
		transform: {
			m11: number;
			m12: number;
			m21: number;
			m22: number;
			dx: number;
			dy: number;
		};
		/** 拡縮 */
		scale: CommonOffset;
		/** 透明度 */
		globalAlpha?: number;
		/** フォント */
		font?: string;
		/** 塗りつぶしスタイル */
		fillStyle?: string;
		/** 線スタイル */
		strokeStyle?: string;
		/** 線の末端形状 */
		lineCap?: string;
		/** 線の結合形状 */
		lineJoin?: string;
		/** 線の太さ */
		lineWidth?: number;
		/** マイター限界比率 */
		miterLimit?: string;
		/** 影の量 */
		shadowBlur?: number;
		/** 影の色 */
		shadowColor?: string;
		/** 影のオフセットX座標 */
		shadowOffsetX?: number;
		/** 影のオフセットY座標 */
		shadowOffsetY?: number;
		/** 文字の方向 */
		textAlign?: string;
		/** 文字基準線 */
		textBaseline?: string;
		/** 結合方式 */
		globalCompositeOperation?: string;
	}

	/**
	 * デフォルトの描画オプション
	 */
	export var ENTITY_OPTIONS_DEFAULT_VALUES:IEntityOptions = {
		/** 0 */
		rotate: 0,
		/** x:0, y:0 */
		translate: {x: 0, y: 0},
		/** m11: 1, m12: 0, m21: 0, m22: 1, dx: 0, dy: 0 */
		transform: {m11: 1, m12: 0, m21: 0, m22: 1, dx: 0, dy: 0},
		/** x:1, y:1 */
		scale: {x: 1, y: 1},
		/** 環境依存 */
		globalAlpha: undefined,
		/** 環境依存 */
		font: undefined,
		/** 環境依存 */
		fillStyle: undefined,
		/** 環境依存 */
		strokeStyle: undefined,
		/** 環境依存 */
		lineCap: undefined,
		/** 環境依存 */
		lineJoin: undefined,
		/** 環境依存 */
		lineWidth: undefined,
		/** 環境依存 */
		miterLimit: undefined,
		/** 環境依存 */
		shadowBlur: undefined,
		/** 環境依存 */
		shadowColor: undefined,
		/** 環境依存 */
		shadowOffsetX: undefined,
		/** 環境依存 */
		shadowOffsetY: undefined,
		/** 環境依存 */
		textAlign: undefined,
		/** 環境依存 */
		textBaseline: undefined,
		/** 環境依存 */
		globalCompositeOperation: undefined
	};

	/**
	 * jgame.jsにおいて、最も基本的な描画単位です。より正確な名称はエンティティ（Entity）です。
	 * Sprite、Shape、Label、Tile、さらにはLayerもこのクラスを継承して作られています。jgame.jsで描画される全てのオブジェクトはEntityであり、Entityでないもの（Sceneなど）は描画されません。
	 * 動作速度が重用視されるjgame.jsにおいて、Gameクラス、Rendererクラスと並ぶ最も重要なクラスですが、通常のユーザがこのクラスを利用することはありません。
	 * このクラスを利用するのは、このクラスを継承して新しい描画オブジェクトを作るなど、一歩進んだ使い方をする時でしょう。
	 */
	export class E {
		/** X座標 */
		x: number;
		/** Y座標 */
		y: number;
		/** 横幅 */
		width: number;
		/** 縦幅 */
		height: number;
		/** Timelineのインスタンス。通常このフィールドを直接参照せず、tl()メソッドを通して参照する */
		_tl: Timeline;
		/** 現在のシーン */
		scene:Scene;
		/** 親Entity */
		parent:E;
		/** 初期化が可能な状態になった場合に実行する処理群 */
		active_queue:Function[];
		/** startメソッドを実行済みかどうかのフラグ */
		started:boolean;
		/** 前回の描画から更新されているかを表すフラグ */
		isUpdated:boolean;
		/** 変形などを無効化するかのフラグ。これがセットされているものは、座標計算を含めて自前で描画処理を実装する必要があるが、その分高速 */
		disableTransform:boolean;
		/** 子Entity */
		entities:E[];
		/** ポイントイベントを受信するかどうか */
		pointCapture:boolean;
		/** ポインティングデバイスで押下処理が発生したことを示すイベント。InputPointEventをパラメータとして受け取る */
		pointDown: Trigger;
		/** ポインティングデバイスで押下処理が終了したことを示すイベント。InputPointEventをパラメータとして受け取る */
		pointUp: Trigger;
		/** ポインティングデバイスで押下処理中に移動が発生したことを示す。InputPointEventをパラメータとして受け取る */
		pointMove: Trigger;
		/** 描画オプション。詳細はIEntityOptionsなどを参照 */
		options:Object;
		/** このEntityの子Entityを描画する際に利用する描画順制御関数 */
		orderDraw:Function;
		/** フィルタ */
		filter:ImageFilter.FilterChain;
		/** スクロール座標 */
		scroll:CommonOffset;
		/** 透明度 */
		opacity:number;

		/**
		 * コンストラクタ
		 */
		constructor() {
			this.opacity = 1;
			this.x = 0;
			this.y = 0;
		}

		/**
		 * ポインティングイベントを有効化する
		 */
		enablePointingEvent() {
			this.pointCapture = true;
			if (! this.pointDown)
				this.pointDown = new Trigger();
			if (! this.pointUp)
				this.pointUp = new Trigger();
			if (! this.pointMove)
				this.pointMove = new Trigger();
		}

		/**
		 * ポインティングイベントを無効化する。
		 * pointDown, pointMove, pointUpに設定したハンドラの解放処理は別途行う必要がある
		 */
		disablePointingEvent() {
			delete this.pointCapture;
		}

		/**
		 * 描画オプションを一つ削除する。詳細はIEntityOptionsなどを参照
		 *  一つも描画オプションを指定していない状態が最速であるため、利用が終わったオプションは迅速な削除が推奨される。
		 * @param name 削除するオプション
		 */
		removeDrawOption(name:string) {
			if (! this.options)
				return;

			if (this.options[name] !== undefined)
				delete this.options[name];
			var cnt = 0;
			for (var i in this.options) {
				cnt++;
				break;
			}
			if (! cnt)
				delete this.options;
			this.updated();
		}

		/**
		 * 描画オプションを指定する。詳細はIEntityOptionsなどを参照
		 * @param name 指定するオプション名
		 * @param value オプションに指定する値
		 */
		setDrawOption(name:string, value:any) {
			if (! this.options)
				this.options = {}
			this.options[name] = value;
			this.updated();
		}

		/**
		 * 描画オプションの現在値を取得する。
		 * なお、指定されていない場合はデフォルト値を返すため、指定されているかどうかの判定でこのメソッドを利用してはならない。
		 * 指定されているかどうかの判定は、optionsフィールドを直接見る必要がある。
		 * @param name 取得する描画オプション名
		 */
		getDrawOption(name:string):any {
			if (! this.options || this.options[name] == undefined)
				return ENTITY_OPTIONS_DEFAULT_VALUES[name];
			return this.options[name];
		}

		/**
		 * 指定座標に移動し、更新フラグを立てる。x, yフィールドを直接操作すると、更新フラグを立てずに移動させることも可能。
		 * @param x 移動先x座標
		 * @param y 移動先y座標
		 */
		moveTo(x:number, y: number) {
			this.x = x;
			this.y = y;
			this.updated();
		}

		/**
		 * 現在位置から相対的に移動させ、更新フラグを立てる。x, yフィールドを直接操作すると、更新フラグを立てずに移動させることも可能。
		 * @param x x座標の移動量
		 * @param y y座標の移動量
		 */
		moveBy(x:number, y:number) {
			this.x += x;
			this.y += y;
			this.updated();
		}

		/**
		 * 指定座標にスクロールさせる
		 * @param x スクロール先x座標
		 * @param y スクロール先y座標
		 */
		scrollTo(x:number, y: number) {
			this.scroll = {
				x: x,
				y: y
			}
			this.updated();
		}

		/**
		 * 相対的にスクロールさせる
		 * @param x x座標のスクロール量
		 * @param y y座標のスクロール量
		 */
		scrollBy(x:number, y:number) {
			if (! this.scroll)
				this.scroll = {x: 0, y: 0}
			this.scroll.x += x;
			this.scroll.y += y;
			this.updated();
		}

		/**
		 * このEntityの初期化処理を行う
		 */
		activate() {
			if (this.active_queue) {
				var f:Function;
				while (f = this.active_queue.shift())
					f.call(this);

				delete this.active_queue;
			}
			if (this.entities) {
				for (var i=0; i<this.entities.length; i++) {
					if (! this.entities[i].scene) {
						this.entities[i].scene = this.scene;
						this.entities[i].activate();
					}
				}
			}
		}

		/**
		 * 初期化処理を追加する
		 */
		addActiveQueue(f:Function) {
			if (this.scene) {
				f.call(this);
				return;
			}
			if (! this.active_queue)
				this.active_queue = [];
			this.active_queue.push(f);
		}

		/**
		 * このEntityをシーンに追加する
		 * @param scene 追加対象scene
		 * @param layerName 追加対象レイヤー名。省略時はrootになる
		 */
		appendTo(scene:Scene, layerName?:string) {
			scene.append(this, layerName);
		}

		/**
		 * このEntityを削除する。
		 * Layerはこのメソッドでは削除出来ないため、Scene.deleteLayerメソッドを利用する必要がある
		 */
		remove() {
			if (this.parent)
				this.parent.removeChild(this);
			else
				throw "Can not remove layer. (use scene.deleteLayer)";
		}

		/**
		 * このEntityの指定位置に子供を挿入する。
		 * @param entity 挿入対象の子Entity
		 * @param index 挿入位置
		 */
		insert(entity:E, index: any) {
			if (! this.entities)
				throw "Can not call append of non-container entity";
			entity.scene = this.scene;
			entity.parent = this;
			if (typeof index != "number") {
				for (var i=0; i<this.entities.length; i++) {
					if (this.entities[i] == index) {
						index = i;
						break;
					}
				}

			}
			this.entities.splice(index, 0, entity);
			entity.activate();
			this.updated();
		}

		/**
		 * このEntityに子供を追加する
		 * @param entity 追加対象の子Entity
		 */
		append(entity:E) {
			if (! this.entities)
				throw "Can not call append of non-container entity";
			entity.scene = this.scene;
			entity.parent = this;
			this.entities.push(entity);
			if (this.scene)
				entity.activate();
			this.updated();
		}

		/**
		 * このEntityから子供を削除する
		 * @param entity 削除対象の子Entity
		 */
		removeChild(entity:E) {
			if (! this.entities)
				throw "Can not call removeChild of non-container entity";

			for (var i=0; i<this.entities.length; i++) {
				if (this.entities[i] == entity) {
					if (entity.entities) {
						var childEntity;
						while (childEntity = entity.entities.pop())
							entity.removeChild(childEntity);
					}
					this.entities.splice(i, 1);
					entity.destroy();
					this.updated();
					return true;
				}
			}
			return false;
		}

		/**
		 * このEntityを活性化させる。このメソッドを呼び出した後は、game.updateイベントのハンドラとしてE.updateメソッドが呼び出されるようになる
		 */
		start() {
			if (this.started)
				return;
			this.started = true;
			if (this.scene)
				this.scene.game.update.handle(this, this.update);
			else
				this.addActiveQueue(function() {this.scene.game.update.handle(this, this.update);} );
		}

		/**
		 * このEntityを非活性化させる。
		 */
		stop() {
			if (! this.started)
				return;
			this.started = false;
			if (this.scene)
				this.scene.game.update.remove(this, this.update);
			else
				this.addActiveQueue(function() {this.scene.game.update.remove(this, this.update);});
		}

		/**
		 * 指定のタイマーを稼動させる。
		 * @param wait タイマー起動間隔をミリ秒で指定
		 * @param method タイマーのコールバック。省略時はintervalメソッドが指定される
		 */
		startTimer(wait:number, method?:Function) {
			if (this.scene)
				this.scene.game.addTimer(wait, this, method ? method : this.interval);
			else
				this.addActiveQueue(function() {this.scene.game.addTimer(wait, this, method ? method : this.interval);});
		}

		/**
		 * 指定のタイマーを停止させる
		 * @param wait 停止対象タイマーの起動間隔
		 * @param method 停止対象タイマーのコールバック。省略時はintervalメソッド
		 */
		stopTimer(wait:number, method?:Function) {
			if (this.scene)
				this.scene.game.removeTimer(wait, this, method ? method : this.interval)
			else
				this.addActiveQueue(function() {this.scene.game.removeTimer(wait, this, method ? method : this.interval);});
		}

		/**
		 * 更新フラグを立てる。これが立っていないと再描画されない。
		 * 高速描画を目指すjgame.jsで一番多いトラブルが更新フラグ立て忘れなので、怪しい場合は呼び出してみるといいかもしれない。
		 * なお、親要素がある場合は親要素の更新フラグ更新を優先させるため、本メソッドを呼び出した後に必ずthis.isUpdate()の戻り値がtrueになるわけではない
		 */
		updated() {
			var p = this;
			while (p.parent)
				p = p.parent;
			p.isUpdated = true;
		}

		/**
		 * このEntityのTimelineを取得する
		 */
		tl():Timeline {
			if (! this._tl)
				this._tl = new Timeline(this);
			return this._tl;
		}

		/**
		 * このEntityを破棄する
		 */
		destroy() {
			if  (this._tl) {
				this._tl.clear();
				delete this._tl;
			}
			this.stop();
			if (this.scene) {
				this.scene.game.removeTimerAll(this);
				this.scene = null;
			}
			delete this.parent;
			if (this.entities) {
				var childEntity;
				while (childEntity = this.entities.pop())
					childEntity.destroy();
			}
			if (this.pointDown) {
				this.pointDown.destroy();
				delete this.pointDown;
			}
			if (this.pointUp) {
				this.pointUp.destroy();
				delete this.pointUp;
			}
			if (this.pointMove) {
				this.pointMove.destroy();
				delete this.pointMove;
			}
		}

		/**
		 * このEntityのゲームから見たオフセット位置を取得する。
		 * x, y単体とは異なり、親要素の座標も考慮に入れた座標を返す。
		 */
		offset():CommonOffset {
			var parent_offset = this.parent ? this.parent.offset() : {x:this.scroll ? this.scroll.x : 0, y: this.scroll ? this.scroll.y : 0};
			return {
				x: this.x + parent_offset.x,
				y: this.y + parent_offset.y
			}
		}

		/**
		 * この要素のゲーム中での領域を取得する。
		 * offsetと同じく、親要素の座標も考慮に入れた領域を返す。
		 */
		rect():Rectangle {
			var offset = this.offset();
			return new Rectangle(
				offset.x,
				offset.y,
				offset.x + this.width,
				offset.y + this.height
			);
		}

		/**
		 * 指定の座標がこの要素と接触しているかを判定する。
		 * @param point 判定する座標。
		 */
		hitTest(point: CommonOffset) {
			return this.rect().hitTest(point);
		}

		/**
		 * 指定の座標との距離を取得する。中心点からの距離である点に注意
		 * @param point 距離を取得する座標。CommonAreaの場合領域の中心からの距離を取得する
		 */
		getDistance(point:CommonOffset):CommonOffset {
			var area:CommonArea = <CommonArea>point;
			if (area.width && area.height) {
				return {
					x: Math.abs((area.x+area.width/2) - (this.x+this.width/2)),
					y: Math.abs((area.y+area.height/2) - (this.y+this.height/2))
				};
			} else {
				return {
					x: Math.abs(point.x - (this.x+this.width/2)),
					y: Math.abs(point.y - (this.y+this.height/2))
				};
			}
		}

		/**
		 * 指定座標に合致する要素を返す。子要素や自分自身も含めて判定する
		 * @param point 判定する座標
		 * @param force trueの場合、各EntityのenablePointingEvent設定値を無視
		 */
		getEntityByPoint(point: CommonOffset, force?:boolean):E {
			if (this.entities) {
				for (var i=this.entities.length-1; i>=0; i--) {
					if (force || this.entities[i].pointCapture) {
						var p = this.entities[i].getEntityByPoint(point);
						if (p)
							return p;
					}
				}
			}
			if ((force || this.pointCapture) && this.hitTest(point))
				return this;

			return null;
		}

		/**
		 * このEntityをSpriteに変換する
		 */
		createSprite() {
			var buffer = new BufferedRenderer({width:this.width, height:this.height});
			var x = this.x;
			var y = this.y;
			this.x = 0;
			this.y = 0;
			buffer.renderUnit(this);
			this.x = x;
			this.y = y;
			return buffer.createSprite();
		}

		/** startによる活性化で呼び出されるGame.updateイベントのイベントハンドラ */
		update(t:number) {}

		/** startTimerによって呼び出されるタイマーのコールバック */
		interval() {}

		/**
		 * 描画
		 * @param context 描画対象context
		 */
		draw(context:CanvasRenderingContext2D) {}

		/**
		 * このEntityを表示し、更新フラグを立てる
		 */
		show() {
			this.opacity = 1;
			this.updated();
		}

		/**
		 * このEntityを非表示にし、更新フラグを立てる
		 */
		hide() {
			this.opacity = 0;
			this.updated();
		}
	}
}

(function() {
	var canvas:HTMLCanvasElement = <HTMLCanvasElement>document.createElement("canvas");
	var context = canvas.getContext("2d");
	for (var p in jg.ENTITY_OPTIONS_DEFAULT_VALUES)
		if (jg.ENTITY_OPTIONS_DEFAULT_VALUES[p] === undefined)
			jg.ENTITY_OPTIONS_DEFAULT_VALUES[p] = context[p];
})();
