///<reference path="all.ts"/>
module jg {
	/**
	 * メインループなどを管理するゲームエンジン。本クラスを基点にjgame.jsのゲームは実行される。
	 * オーバーライドすることも想定しており、https://github.com/tsugehara/jgengine にいくつかサンプルがある。
	 */
	export class Game {
		/** 終了フラグ。_がついているが直接操作してもいい */
		_exit: boolean;
		/** 前回の実行時間 */
		tick: number;
		/** 前回の描画時間 */
		renderTick: number;
		/** KeytypeとkeyCodeの関連付けを行うマップ。JGUtilへの移行を検討中 */
		keymap: any;
		/** ドラッグ中情報。マルチタッチサポート後にインターフェース変更の可能性がある */
		dragParam: InputPointEvent;

		/** 描画オブジェクト */
		renderer: GameRenderer;
		/** 管理しているシーン */
		scenes: Scene[];
		/** 現在のシーン */
		scene: Scene;
		/** 管理しているリソース。リソースは単一インスタンスであるため、Resource.getInstanceと等価であることが保証されている */
		resource: Resource;
		/** ゲームの横幅 */
		width:number;
		/** ゲームの縦幅 */
		height:number;
		/** 表示上の拡大比率 */
		scale: number;
		/** 読み込み中シーンのコンストラクタ */
		loadingSceneClass: any;	//これの型指定方法わかんない
		/** 読み込み中シーン。読み込み中の場合のみ値が設定される */
		loadingScene: LoadingScene;
		/** このゲームの内部ID。単一ページに複数のゲームを表示するような場合以外、特に利用する機会は無い */
		id: number;

		/** 画面描画間隔。指定するとこのFPS以下に画面描画が抑制される */
		targetFps: number;

		/** FPS表示用のDOM要素。将来的に変更される可能性がある */
		fps: HTMLElement;

		/** preload処理の完了時に呼び出されるイベント */
		loaded: Trigger;
		/**
		 * ゲーム内時間の更新時に呼び出されるイベント。
		 * 基本的なゲーム内更新処理はすべてこのイベントのハンドラで実行するが、精度が不要でかつ定期的に実行するアニメーションのような処理は、addTimerメソッドでの実行でもよい。
		 * 引数として経過時間がミリ秒で渡されるため、その経過時間に則った処理を行う必要がある。
		 * また、解放漏れに注意。
		 */
		update: Trigger;
		/** 
		 * ゲーム内タイマー
		 */
		timers: GameTimer[];
		/**
		 * 描画時に呼び出されるイベント。利用は想定されていないので、通常時はundefined。利用する際は自前でnewする必要がある。
		 * updateとは異なり、経過時間は取得出来ない。
		 */
		render: Trigger;
		/** 
		 * キーが押された時に呼び出されるイベント。InputKeyboardEventをパラメータとして持つ。
		 * 将来的にインターフェース変更の可能性あり。
		 */
		keyDown: Trigger;
		/** 
		 * キーが離された時に呼び出されるイベント。InputKeyboardEventをパラメータとして持つ。
		 * 将来的にインターフェース変更の可能性あり。
		 */
		keyUp: Trigger;
		/** 
		 * ポインティングデバイスが押された時に呼び出されるイベント。InputPointEventをパラメータとして持つ。
		 * 将来的にインターフェース変更の可能性あり。
		 */
		pointDown: Trigger;
		/** 
		 * ポインティングデバイスが離された時に呼び出されるイベント。InputPointEventをパラメータとして持つ。
		 * 将来的にインターフェース変更の可能性あり。
		 */
		pointUp: Trigger;
		/** 
		 * ポインティングデバイスが移動された時に呼び出されるイベント。ただし移動だけでは発生せず、必ずpointDownが事前に発生している必要がある。InputPointEventをパラメータとして持つ。
		 * 将来的にインターフェース変更の可能性あり。
		 */
		pointMove: Trigger;

		/**
		 * 発生済みのユーザ入力イベント群。
		 * jgame.jsではメインループ内で入力処理を発火させるため、keydownなどのDOMイベントでいったんここにプールしてから、メインループでイベント発火という手順を踏む。
		 */
		eventQueue: InputEvent[];
		/** Enumと各種イベント名のマップ */
		inputEventMap: any;
		/** pointDown発生済みかどうかのフラグ */
		isPointDown: boolean;

		/** このゲームの乱数シード */
		seed: number;
		/** このゲームの乱数エンジン */
		mt: MT;

		/**
		 * 新しいゲームを生成する。
		 * 現在引数をwidth, heightを廃止しargsのみにする変更が検討されている。
		 * @param width ゲームの横幅
		 * @param height ゲームの縦幅
		 * @param args RenderTransferModeを指定すると、このゲームのRenderTransferModeの変更が可能。HTMLElementを指定すると、DOMコンテナを指定可能。文字列を指定すると、window[文字列]のコンストラクタをRendererに指定する
		 */
		constructor(width:number, height:number, ...args:any[]) {
			this._exit = false;
			this.id = JGUtil.generateId();
			this.width = width;
			this.height = height;
			this.targetFps = 0;
			this.loadingSceneClass = LoadingScene;
			this.keymap = {
				13: Keytype.Enter,
				27: Keytype.Esc,
				37: Keytype.Left,
				38: Keytype.Up,
				39: Keytype.Right,
				40: Keytype.Down
			}

			this.loaded = new Trigger();
			this.update = new Trigger();
			this.pointDown = new Trigger();
			this.pointUp = new Trigger();
			this.pointMove = new Trigger();
			this.keyDown = new Trigger();
			this.keyUp = new Trigger();
			this.timers = [];

			this.scene = new Scene(this);
			this.scenes = [];
			this.scenes.push(this.scene);

			this.resource = Resource.getInstance();

			var container:HTMLElement, transferMode:RenderTransferMode;
			for (var i=2; i<arguments.length; i++) {
				if (arguments[i] instanceof HTMLElement)
					container = <HTMLElement>arguments[i];
				else if (typeof arguments[i] == "string") {
					this.renderer = new window[arguments[i]](this, container, transferMode);
					this.renderer.changeScene(this.scene);
				} else
					transferMode = <RenderTransferMode>arguments[i]
			}
			if (! this.renderer) {
				this.renderer = new GameRenderer(this, container, transferMode);
				this.renderer.changeScene(this.scene);
			}

			this.eventQueue = [];
			this.inputEventMap = {}
			this.inputEventMap[InputEventType.Keyboard] = {};
			this.inputEventMap[InputEventType.Keyboard][InputEventAction.Down] = "keyDown";
			this.inputEventMap[InputEventType.Keyboard][InputEventAction.Up] = "keyUp";
			this.inputEventMap[InputEventType.Point] = {};
			this.inputEventMap[InputEventType.Point][InputEventAction.Down] = "pointDown";
			this.inputEventMap[InputEventType.Point][InputEventAction.Move] = "pointMove";
			this.inputEventMap[InputEventType.Point][InputEventAction.Up] = "pointUp";
			//this.enableKeyboardHandler();
			//this.enablePointHandler();

			if (document.getElementById("fps_show"))
				this.fps = document.getElementById("fps_show");

			this.setSeed();

			this.main()
		}

		/**
		 * 乱数シードを指定する。
		 * 再現性のあるゲーム以外で明示的に呼び出す必要はなく、通常は初期化時に自動的に設定される。
		 * @param seed 乱数シード。省略時は自動設定
		 */
		setSeed(seed?:number) {
			this.seed = seed === undefined ? new Date().getTime() : seed;
			this.mt = new MT(seed);
		}

		/**
		 * 乱数を取得する。再現性のあるゲームを作る場合、Math.randomではなくこちらのrandomを利用する必要がある
		 * @param min 最小値
		 * @param max 最大値
		 */
		random(min:number, max:number) {
			return this.mt.nextInt(min, max-min+1);
		}

		/**
		 * windowのサイズを取得する
		 */
		getWindowSize() {
			return {
				width: document.documentElement.clientWidth,
				height: document.documentElement.clientHeight
			};
		}

		/**
		 * 現在の画面の大きさに合わせて拡大する。
		 * @param no_center trueに設定すると中央寄せにしない
		 */
		fitToWindow(no_center?:boolean) {
			var elem = this.renderer.container.parentElement;
			elem.style.margin = "0";
			elem.style.padding = "0";
			elem.style.overflow = "hidden";
			this.renderer.container.style.margin = "0";
			this.renderer.container.style.padding = "0";

			var size = this.getWindowSize();
			this.renderer.container.style.width = size.width+"px";
			this.renderer.container.style.height = size.height+"px";

			this.scale = Math.min(
				size.width / this.width,
				size.height / this.height
			);
			var size2 = {
				width: Math.floor(this.width * this.scale),
				height: Math.floor(this.height * this.scale)
			}
			this.renderer.changeFrontCanvasSize(size2, no_center ? undefined : {
				x:Math.floor((size.width-size2.width) / 2),
				y:Math.floor((size.height-size2.height) / 2)
			});
		}

		/**
		 * 背景色を設定する。
		 * このメソッドは廃止が検討されている。
		 * @param r 0～255の範囲で赤色値を指定
		 * @param g 0～255の範囲で緑色値を指定
		 * @param b 0～255の範囲で青色値を指定
		 * @param a 0～255の範囲で透明度を指定
		 */
		setBgColor(r:number, g:number, b:number, a:number) {
			for (var i=0; i<this.renderer.bg.data.length; i+=4) {
				this.renderer.bg.data[i] = r;
				this.renderer.bg.data[i+1] = g;
				this.renderer.bg.data[i+2] = b;
				this.renderer.bg.data[i+3] = a;
			}
		}

		/**
		 * ゲーム内のすべてのオブジェクトをリフレッシュする。
		 * スタンバイからの復帰時などでcanvasが壊れていても本メソッドでの復旧が可能だが、BufferedRendererなど、破壊されるオブジェクトもある点に注意。
		 */
		refresh() {
			this.renderer.refresh();
			for (var i=0; i<this.scenes.length; i++)
				this.scenes[i].refresh();
		}

		/**
		 * 現在実行されている環境でタッチイベントが有効化を判定する。
		 * copied by enchant.js (enchant.ENV.TOUCH_ENABLED)
		 */
		isTouchEnable() {
			var div:any = document.createElement('div');
			div.setAttribute('ontouchstart', 'return');
			return typeof div.ontouchstart === 'function';
		}

		/**
		 * マウスなどのDOMイベントからjgame.jsが利用可能なoffset値を取得する
		 */
		getOffsetByEvent(e:any):CommonOffset {
			e.offset = {
				x: e.pageX - this.renderer._pageX,
				y: e.pageY - this.renderer._pageY
			}
			return {
				x: this.scale ? e.offset.x / this.scale : e.offset.x,
				y: this.scale ? e.offset.y / this.scale : e.offset.y
			}
		}

		/**
		 * DOMのmousedownに対するイベントハンドラ
		 * @param e DOMのMouseEvent
		 */
		onmousedown(e:MouseEvent) {
			this.isPointDown = true;
			this.eventQueue.push(new InputPointEvent(
				InputEventAction.Down, e, this.getOffsetByEvent(e)
			));
			e.preventDefault();
		}
		/**
		 * DOMのtouchstartに対するイベントハンドラ。
		 * 現状lib.d.tsに型情報が定義されていないようなので、anyになっている。
		 * @param e DOMのTouchEvent
		 */
		ontouchstart(e:any) {
			var touches = e.changedTouches;
			this.isPointDown = true;
			for (var i = 0, l = touches.length; i < l; i++)
				this.eventQueue.push(new InputPointEvent(
					InputEventAction.Down, touches[i], this.getOffsetByEvent(touches[i])
				));

			e.preventDefault();
		}

		/**
		 * DOMのmousemoveに対するイベントハンドラ
		 * @param e DOMのMouseEvent
		 */
		onmousemove(e:MouseEvent) {
			if (! this.isPointDown)
				return;

			this.eventQueue.push(new InputPointEvent(
				InputEventAction.Move, e, this.getOffsetByEvent(e)
			));

			e.preventDefault();
		}
		/**
		 * DOMのtouchmoveに対するイベントハンドラ。
		 * 現状lib.d.tsに型情報が定義されていないようなので、anyになっている。
		 * @param e DOMのTouchEvent
		 */
		ontouchmove(e:any) {
			if (! this.isPointDown)
				return;

			var touches = e.changedTouches;
			for (var i = 0, l = touches.length; i < l; i++)
				this.eventQueue.push(new InputPointEvent(
					InputEventAction.Move, touches[i], this.getOffsetByEvent(touches[i])
				));

			e.preventDefault();
		}

		/**
		 * DOMのmouseupに対するイベントハンドラ
		 * @param e DOMのMouseEvent
		 */
		onmouseup(e:MouseEvent) {
			if (! this.isPointDown)
				return;

			this.eventQueue.push(new InputPointEvent(
				InputEventAction.Up, e, this.getOffsetByEvent(e)
			));

			this.isPointDown = false;

			e.preventDefault();
		}
		/**
		 * DOMのtouchendに対するイベントハンドラ。
		 * 現状lib.d.tsに型情報が定義されていないようなので、anyになっている。
		 * @param e DOMのTouchEvent
		 */
		ontouchend(e:any) {
			if (! this.isPointDown)
				return;

			var touches = e.changedTouches;
			for (var i = 0, l = touches.length; i < l; i++)
				this.eventQueue.push(new InputPointEvent(
					InputEventAction.Up, touches[i], this.getOffsetByEvent(touches[i])
				));

			this.isPointDown = false;

			e.preventDefault();
		}


		/**
		 * ポインティングイベントを有効にする。
		 * 無効化処理も実行するため、何度も切り替えるアプリケーションの場合pointDown, pointMove, pointUpのイベントハンドラを独自に復旧する必要がある点に注意。
		 */
		enablePointHandler() {
			this.disablePointHandler();

			try {
				if (this.isTouchEnable()) {
					this.renderer.handler.addEventListener("touchstart", JGUtil.createIdProxy(this.id, this.ontouchstart, this), false);
					this.renderer.handler.addEventListener("touchmove" , JGUtil.createIdProxy(this.id, this.ontouchmove, this) , false);
					this.renderer.handler.addEventListener("touchend"  , JGUtil.createIdProxy(this.id, this.ontouchend, this)  , false);
				} else {
					this.renderer.handler.addEventListener("mousedown" , JGUtil.createIdProxy(this.id, this.onmousedown, this) , false);
					this.renderer.handler.addEventListener("mousemove" , JGUtil.createIdProxy(this.id, this.onmousemove, this) , false);
					this.renderer.handler.addEventListener("mouseup"   , JGUtil.createIdProxy(this.id, this.onmouseup, this)   , false);
				}
			} catch (ex) {
				// ignore error of addEventListener
			}
		}
		/**
		 * ポインティングイベントを無効化する。
		 */
		disablePointHandler() {
			this.dragParam = null;
			try {
				if (this.isTouchEnable()) {
					if (JGUtil.getIdProxy(this.id, this.ontouchstart, this)) {
						this.renderer.handler.removeEventListener("touchstart", JGUtil.getIdProxy(this.id, this.ontouchstart, this), false);
						this.renderer.handler.removeEventListener("touchmove" , JGUtil.getIdProxy(this.id, this.ontouchmove, this) , false);
						this.renderer.handler.removeEventListener("touchend"  , JGUtil.getIdProxy(this.id, this.ontouchend, this)  , false);
						JGUtil.deleteIdProxy(this.id, this.ontouchstart, this);
						JGUtil.deleteIdProxy(this.id, this.ontouchmove, this);
						JGUtil.deleteIdProxy(this.id, this.ontouchend, this);
					}
				} else {
					if (JGUtil.getIdProxy(this.id, this.onmousedown, this)) {
						this.renderer.handler.removeEventListener("mousedown" , JGUtil.getIdProxy(this.id, this.onmousedown, this) , false);
						this.renderer.handler.removeEventListener("mousemove" , JGUtil.getIdProxy(this.id, this.onmousemove, this) , false);
						this.renderer.handler.removeEventListener("mouseup"   , JGUtil.getIdProxy(this.id, this.onmouseup, this)   , false);
						JGUtil.deleteIdProxy(this.id, this.onmousedown, this);
						JGUtil.deleteIdProxy(this.id, this.onmousemove, this);
						JGUtil.deleteIdProxy(this.id, this.onmouseup, this);
					}
				}
			} catch (ex) {
				// ignore error of removeEventListener
			}
		}

		/**
		 * DOMのkeydownイベントに対するイベントハンドラ。
		 * lib.d.tsにおいて型情報が不明なためanyになっている。KeyboardEventExtensionsでいいのだろうか。
		 * @param e DOMのキーボードイベントパラメータ。内部的にはkeyCodeしか利用していない
		 */
		onkeydown(e:any) {
			var keyParam = new InputKeyboardEvent(InputEventAction.Down,this.keymap[e.keyCode], e);
			this.eventQueue.push(keyParam);
			if (this.keymap[e.keyCode] != undefined)
				e.preventDefault();
		}

		/**
		 * DOMのkeyupイベントに対するイベントハンドラ。
		 * lib.d.tsにおいて型情報が不明なためanyになっている。KeyboardEventExtensionsでいいのだろうか。
		 * @param e DOMのキーボードイベントパラメータ。内部的にはkeyCodeしか利用していない
		 */
		onkeyup(e:any) {
			var keyParam = new InputKeyboardEvent(InputEventAction.Up,this.keymap[e.keyCode], e);
			this.eventQueue.push(keyParam);
			if (this.keymap[e.keyCode] != undefined)
				e.preventDefault();
		}

		/**
		 * キーボードイベントを有効化する。
		 * このイベントを有効にしてしまうと、keymapに登録されているキーコードにpreventDefaultが動くため、textareaなどが存在するページで実行する場合は注意が必要。
		 * また無効化処理も実行するため、何度も切り替えるアプリケーションの場合keyDown, keyUpのイベントハンドラを独自に復旧する必要がある点に注意。
		 */
		enableKeyboardHandler() {
			this.disableKeyboardHandler();
			try {
				document.addEventListener("keydown", JGUtil.createIdProxy(this.id, this.onkeydown, this), false);
				document.addEventListener("keyup"  , JGUtil.createIdProxy(this.id, this.onkeyup, this)  , false);
			} catch(ex) {
				//ignore error of addEventListener
			}
		}
		/**
		 * キーボードイベントを無効化する。
		 */
		disableKeyboardHandler() {
			if (JGUtil.getIdProxy(this.id, this.onkeydown, this)) {
				document.removeEventListener("keydown", JGUtil.getIdProxy(this.id, this.onkeydown, this), false);
				document.removeEventListener("keyup"  , JGUtil.getIdProxy(this.id, this.onkeyup, this)  , false);
				JGUtil.deleteIdProxy(this.id, this.onkeydown, this);
				JGUtil.deleteIdProxy(this.id, this.onkeyup, this);
			}
		}

		/**
		 * 指定した時間間隔で実行するタイマーを追加する。
		 * このタイマーは大体のタイマーであるため、アニメーションなど正確性の不要な作業のみの利用に限定するべきである。
		 * @param wait 実行時間間隔をミリ秒で指定する
		 * @param owner タイマーのコールバックに対するthis
		 * @param handler コールバック
		 */
		addTimer(wait:number, owner:any, handler:Function) {
			var timer:GameTimer = null;
			for (var i=0; i<this.timers.length; i++) {
				if (this.timers[i].wait == wait) {
					timer = this.timers[i];
					break;
				}
			}
			if (timer == null) {
				timer = new GameTimer(wait);
				this.timers.push(timer);
			}
			timer.trigger.handle(owner, handler);
		}

		/**
		 * 指定した時間間隔で実行するタイマーのコールバックを削除する。
		 * 一つもコールバックが存在しないタイマー自体の削除処理も行っている。
		 * @param wait 実行時間間隔をミリ秒で指定する
		 * @param owner タイマーのコールバックに対するthis
		 * @param handler コールバック
		 */
		removeTimer(wait:number, owner:any, handler:Function) {
			var timer:GameTimer = null;
			for (var i=0; i<this.timers.length; i++) {
				if (this.timers[i].wait == wait) {
					timer = this.timers[i];
					break;
				}
			}
			if (timer == null)
				throw "error removeTimer: dont have "+wait+" timer";

			timer.trigger.remove(owner, handler);
		}

		/**
		 * 指定したオーナーのタイマーに対するコールバックをすべて削除する
		 * @param owner タイマーのコールバックに対するthis
		 */
		removeTimerAll(owner:any) {
			for (var i=0; i<this.timers.length; i++) {
				this.timers[i].trigger.removeAll(owner);
			}
		}

		/**
		 * シーンを変更する
		 * @param scene 変更後のシーン
		 * @param effect 変更時にかけるエフェクト。省略時はエフェクト無しになる。通常、EffectTypeの値を指定する
		 * @param endOldScene trueを指定すると、切り替え前に前のシーンを削除する。
		 */
		changeScene(scene:Scene, effect?:any, endOldScene?:boolean) {
			if (effect) {
				var currentScene = this.scene;
				Effect.sceneEffect(this, currentScene, scene, effect, () => {
					this.endScene();
					this.changeScene(scene);
				}, endOldScene);
				return;
			}
			this.scenes.push(scene);
			scene.game = this;
			this.scene.hid.fire();
			this.scene = scene;
			this.renderer.changeScene(this.scene);
			this.scene.started.fire();
		}

		/**
		 * シーンを終了する
		 * @param effect 変更時にかけるエフェクト。省略時はエフェクト無しになる。通常、EffectTypeの値を指定する
		 */
		endScene(effect?:any) {
			if (this.scenes.length == 1) {
				this.end();
				return;
			}
			if (effect) {
				Effect.sceneEffect(this, this.scene, this.scenes[this.scenes.length-2], effect, () => {
					this.endScene();
				}, true);
				return;
			}
			this.scene.destroy();
			this.scenes.pop();
			this.scene.ended.fire();
			this.scene = this.scenes[this.scenes.length-1];
			this.renderer.changeScene(this.scene);
			this.scene.showed.fire();
		}

		/**
		 * 指定した名前のリソースを取得する。サウンドはsメソッドでの取得である点に注意
		 * @param name リソース名
		 */
		r(name:string) {
			return this.resource.get(name);
		}

		/**
		 * 指定した名前のサウンドリソースを取得する。画像などはrメソッドの取得である点に注意
		 * @param name サウンドリソース名
		 */
		s(name:string) {
			return this.resource.sound(name);
		}

		/**
		 * 事前の読み込み処理を行う。
		 * 配列、オブジェクト、文字列のいずれかが指定可能で、複数回の呼び出しも可能。
		 * 配列の場合、リソース名はURLと同じ扱いになる。
		 * game.preload(["a.png", "b.png", "c.png"])
		 * オブジェクトの場合、リソース名はキーで値がURLとなる。
		 * game.preload({a: "a.png", b: "b.png", c: "c.png"})
		 * 文字列の場合、第二、第三引数などを配列と同じように処理する。
		 * game.preload("a.png", "b.png", "c.png")
		 * @param ary 配列、オブジェクト、文字列のいずれかが指定可能。
		 */
		preload(ary: any) {
			if (ary instanceof Array) {
				for (var i=0; i<ary.length; i++)
					this.resource.load(ary[i], ary[i]);
			} else if (typeof ary == "string") {
				for (var i=0; i<arguments.length; i++)
					this.resource.load(arguments[i], arguments[i]);
			} else {
				for (var j in ary)
					this.resource.load(j, ary[j]);
			}

			if (this.loadingSceneClass && !this.loadingScene)
				this.setLoadingScene(this.loadingSceneClass);
		}

		/**
		 * 他のライブラリで読み込み中のリソースをjgame.jsに登録する。
		 * @param identity リソースの識別名
		 */
		preloadOther(identity:string) {
			this.resource.loadManual(identity);
		}

		/**
		 * 他のライブラリで読み込み中のリソースが読み込み完了となった事をjgame.jsに通知する
		 * @param identity リソースの識別名
		 */
		preloadCompleteOther(identity:string) {
			this.resource.completeManual(identity);
		}

		/**
		 * 読み込み中シーンを設定し、現在のシーンを切り替える。
		 * 現在既に読み込み中である場合、本メソッドは処理を行わない。
		 */
		setLoadingScene(scene:any) {
			if (! this.loadingScene) {
				if (scene instanceof LoadingScene)
					this.loadingScene = scene;
				else
					this.loadingScene = new scene(this, this.resource);

				this.loadingScene.finished.handle(this, this.preloadComplete);
				this.changeScene(this.loadingScene);
			}
		}

		/**
		 * preloadの完了処理として、loadingSceneフィールドの削除、loadedイベントの発火を行う
		 */
		preloadComplete() {
			if (this.loadingScene)
				delete this.loadingScene;
			this.loaded.fire();
		}

		/**
		 * ゲームを終了する。
		 * 実態はメインループの終了のみであり、本処理実行後でも_exitフラグの削除とmainメソッドの再実行によりゲームは再開可能
		 */
		end() {
			this.renderer.render();
			this._exit = true;
		}

		/**
		 * ポインティングされたEntityを設定する
		 * @param param 対象のポインティングイベント
		 */
		setPointingEntity(param:InputPointEvent) {
			var layers = this.scene.getLayerArray();
			var layer;
			var offset = param.point;
			while (layer = layers.pop()) {	//上のレイヤーから先に処理
				if (! layer.pointCapture)
					continue;

				var dragObj = layer.getEntityByPoint(offset);
				if (! dragObj)
					dragObj = layer;

				param.set(dragObj);
				this.dragParam = param;

				break;
			}
		}

		/**
		 * 入力イベントを実行する
		 */
		raiseInputEvent() {
			var e:InputEvent;
			while (e = this.eventQueue.shift()) {
				var n = this.inputEventMap[e.type][e.action];
				if (e.type == InputEventType.Keyboard) {
					if (this.scene[n])
						this.scene[n].fire(e);
					this[n].fire(e);
				} else {
					if (e.action == InputEventAction.Down)
						this.setPointingEntity(<InputPointEvent>e);
					else if (!this.dragParam)
						continue;
					else
						(<InputPointEvent>e).set(this.dragParam.entity);

					if ((<InputPointEvent>e).entity && (<InputPointEvent>e).entity[n])
						(<InputPointEvent>e).entity[n].fire(e);
					if (this.scene[n])
						this.scene[n].fire(e);

					this[n].fire(e);
				}
			}
		}

		/**
		 * メインループ
		 */
		main() {
			var fps_stack:number[] = [];
			var _main = (t:number) => {
				if (this._exit)
					return;

				if (t === undefined)
					t = Date.now ? Date.now() : new Date().getTime();
				if (this.tick > (t+10000) || (this.tick+10000) < t) {
					//this.tick > (t+10000): 前回更新分が10秒以上未来の時間の場合。多分タイマーバグっとるのでリセット
					//(this.tick+10000) < t: 10秒以上更新されてない。多分タイマーバグっとる。バグっとるよね？
					this.tick = t - 1;
					this.renderTick = t - this.targetFps;
					this.refresh();
				}

				var time = t - this.tick;
				if (this.tick < t) {
					this.raiseInputEvent();
					this.update.fire(time);
					this.tick = t;
				}

				for (var i=0; i<this.timers.length; i++)
					this.timers[i].tryFire(time);

				if (this.targetFps == 0 || this.renderTick <= t) {
					if (this.render)
						this.render.fire();

					this.renderer.render();
					if (this.targetFps)
						this.renderTick = t+this.targetFps;
					if (this.fps) {
						if (fps_stack.length == 19) {
							this.fps.innerHTML = Math.round(20000 / (t-fps_stack[0])).toString();
							fps_stack = [];
						} else {
							fps_stack.push(t);
						}
					}
				}

				window.requestAnimationFrame(_main);
			}

			this.tick = 0;
			this.renderTick = 0;
			window.requestAnimationFrame(_main);
		}

		/**
		 * フルスクリーン化を行う
		 */
		fullscreen() {
			var t = this.renderer.container;
			if (t["requestFullScreen"])
				t["requestFullScreen"]();
			else if (t["webkitRequestFullScreen"])
				t["webkitRequestFullScreen"]();
			else if (t["mozRequestFullScreen"])
				t["mozRequestFullScreen"]();
			else
				return false;
			return true;
		}

		/**
		 * フルスクリーンを終了する
		 */
		exitFullscreen() {
			var t = this.renderer.container;
			if (t["exitFullscreen"])
				t["exitFullscreen"]();
			else if (t["webkitCancelFullScreen"])
				t["webkitCancelFullScreen"]();
			else if (t["mozCancelFullScreen"])
				t["mozCancelFullScreen"]();
			else
				return false;
			return true;
		}
	}
}