module jg {
	export class Game {
		//inner parameters
		_exit: bool;
		tick: number;
		renderTick: number;
		keymap: any;
		dragParam:InputPointEvent;

		//parameters for public
		renderer: GameRenderer;
		scenes: Scene[];
		scene: Scene;
		resource: Resource;
		width:number;
		height:number;
		scale: number;
		loadingSceneClass: any;	//これの型指定方法わかんない
		loadingScene:LoadingScene;

		//for heavy games
		targetFps:number;

		//debug
		fps: HTMLElement;

		//events
		loaded: Trigger;
		update: Trigger;
		timers: GameTimer[];
		render: Trigger;
		keyDown: Trigger;
		keyUp: Trigger;
		pointDown: Trigger;
		pointUp: Trigger;
		pointMove: Trigger;

		//input events stack
		eventQueue: InputEvent[];
		inputEventMap: any;
		isPointDown:bool;

		//random
		seed:number;
		mt:MT;

		//未定義パラメータとして、第三引数以降にRenderTransferModeと、HTMLElementを指定可能（第三匹数以降であればどこでよい）
		//それぞれ、GameRendererの第二、第三引数として利用される
		constructor(width:number, height:number, ...args:any[]) {
			this._exit = false;
			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 = new GameTimer[];

			this.scene = new Scene(this);
			this.scenes = new Scene[];
			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()
		}

		setSeed(seed?:number) {
			this.seed = seed === undefined ? new Date().getTime() : seed;
			this.mt = new MT(seed);
		}

		random(min:number, max:number) {
			return this.mt.nextInt(min, max-min+1);
		}

		getWindowSize() {
			return {
				width: document.documentElement.clientWidth,
				height: document.documentElement.clientHeight
			};
		}

		fitToWindow(no_center?:bool) {
			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)
			});
		}

		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;
			}
		}

		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';
		}

		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
			}
		}

		onmousedown(e:MouseEvent) {
			this.isPointDown = true;
			this.eventQueue.push(new InputPointEvent(
				InputEventAction.Down, e, this.getOffsetByEvent(e)
			));
			e.preventDefault();
		}
		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();
		}

		onmousemove(e:MouseEvent) {
			if (! this.isPointDown)
				return;

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

			e.preventDefault();
		}
		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();
		}

		onmouseup(e:MouseEvent) {
			if (! this.isPointDown)
				return;

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

			this.isPointDown = false;

			e.preventDefault();
		}
		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();
		}


		enablePointHandler() {
			this.dragParam = null;

			try {
				if (this.isTouchEnable()) {
					this.renderer.handler.addEventListener("touchstart", JGUtil.proxy(this.ontouchstart, this), false);
					this.renderer.handler.addEventListener("touchmove" , JGUtil.proxy(this.ontouchmove, this) , false);
					this.renderer.handler.addEventListener("touchend"  , JGUtil.proxy(this.ontouchend, this)  , false);
				} else {
					this.renderer.handler.addEventListener("mousedown" , JGUtil.proxy(this.onmousedown, this) , false);
					this.renderer.handler.addEventListener("mousemove" , JGUtil.proxy(this.onmousemove, this) , false);
					this.renderer.handler.addEventListener("mouseup"   , JGUtil.proxy(this.onmouseup, this)   , false);
				}
			} catch (ex) {
				// ignore error of addEventListener
			}
		}

		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();
		}

		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();
		}

		enableKeyboardHandler() {
			try {
				document.addEventListener("keydown", JGUtil.proxy(this.onkeydown, this), false);
				document.addEventListener("keyup"  , JGUtil.proxy(this.onkeyup, this)  , false);
			} catch(ex) {
				//ignore error of addEventListener
			}
		}

		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);
		}

		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);
		}

		removeTimerAll(owner:any) {
			for (var i=0; i<this.timers.length; i++) {
				this.timers[i].trigger.removeAll(owner);
			}
		}

		exit() {
			this._exit = true;
		}

		changeScene(scene:Scene, effect?:any, endOldScene?:bool) {
			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();
		}

		endScene(effect?:any) {
			if (this.scenes.length == 1) {
				this.exit();
				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();
		}

		r(name:string) {
			return this.resource.get(name);
		}

		s(name:string) {
			return this.resource.sound(name);
		}

		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 i in ary)
					this.resource.load(i, ary[i]);
			}

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

		preloadOther(identity:string) {
			this.resource.loadManual(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);
			}
		}

		preloadComplete() {
			if (this.loadingScene)
				delete this.loadingScene;
			this.loaded.fire();
		}

		end() {
			this.renderer.render();
			this._exit = true;
		}

		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)
						return;
					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 = new number[];
			var _main = (t:number) => {
				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);
						}
					}
				}

				if (! this._exit)
					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;
		}
	}
}