class Game {
	//inner parameters
	_exit: bool;
	tick: number;
	renderTick: number;
	keymap: Object;

	//parameters for public
	renderer: GameRenderer;
	scenes: Scene[];
	currentScene: Scene;
	resource: Resource;
	width:number;
	height:number;
	enterFrameTick: number;
	scale: number;

	//for heavy games
	targetFps:number;

	//debug
	fps: HTMLElement;

	//events
	loaded: Trigger;
	update: Trigger;
	timers: GameTimer[];
	render: Trigger;
	inputDown: Trigger;
	inputUp: Trigger;
	inputMove: Trigger;
	onmousedown: any;
	onmousemove: any;
	onmouseup: any;
	enterFrame: Trigger;

	//未定義パラメータとして、第三匹数以降にRenderTransferModeと、HTMLElementを指定可能（第三匹数以降であればどこでよい）
	//それぞれ、GameRendererの第二、第三匹数として利用される
	constructor(width:number, height:number) {
		this._exit = false;
		this.width = width;
		this.height = height;
		this.targetFps = 0;

		this.loaded = new Trigger();
		this.update = new Trigger();
		this.inputDown = new Trigger();
		this.inputUp = new Trigger();
		this.inputMove = new Trigger();
		this.timers = new GameTimer[];

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

		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.currentScene);
			} else
				transferMode = <RenderTransferMode>arguments[i]
		}
		if (! this.renderer) {
			this.renderer = new GameRenderer(this, container, transferMode);
			this.renderer.changeScene(this.currentScene);
		}

		this.keyboardHandler();
		this.pointHandler();

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

		this.main()
	}

	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() {
		if (document.addEventListener) {
			if (this.isTouchEnable()) {
				this.renderer.handler.removeEventListener("mousedown" , this.onmousedown, false);
				this.renderer.handler.removeEventListener("mousemove" , this.onmousemove, false);
				this.renderer.handler.removeEventListener("mouseup"   , this.onmouseup  , false);
			} else {
				this.renderer.handler.removeEventListener("mousedown" , this.onmousedown, false);
				this.renderer.handler.removeEventListener("mousemove" , this.onmousemove, false);
				this.renderer.handler.removeEventListener("mouseup"   , this.onmouseup  , false);
			}
		} else {
			if (this.isTouchEnable()) {
				this.renderer.handler.detachEvent("onmousedown", this.onmousedown);
				this.renderer.handler.detachEvent("onmousemove", this.onmousemove);
				this.renderer.handler.detachEvent("onmouseup",   this.onmouseup  );
			} else {
				this.renderer.handler.detachEvent("onmousedown", this.onmousedown);
				this.renderer.handler.detachEvent("onmousemove", this.onmousemove);
				this.renderer.handler.detachEvent("onmouseup",   this.onmouseup  );
			}
		}

		this.renderer.refresh();
		for (var i=0; i<this.scenes.length; i++) {
			this.scenes[i].refresh();
		}
		this.pointHandler();
	}

	//copied by enchant.js (enchant.ENV.TOUCH_ENABLED)
	isTouchEnable() {
		var div:any = document.createElement('div');
		div.setAttribute('ontouchstart', 'return');
		return typeof div.ontouchstart === 'function';
	}

	pointHandler() {
		var dragParam:InputPointEvent = null;
		/*
		var ontouchstart = (e) => {
		}
		var ontouchend = (e) => {
		}
		var ontouchmove = (e) => {
		}
		*/
		this.onmousedown = (e) => {
			var layers = this.currentScene.getLayerArray();
			var layer;
			var offset:CommonOffset = {
				x: this.scale ? e.offsetX / this.scale : e.offsetX,
				y: this.scale ? e.offsetY / this.scale : e.offsetY
			}
			while (layer = layers.pop()) {	//上のレイヤーから先に処理
				if (! layer.pointCapture)
					continue;

				var dragObj = layer.getEntityByPoint(offset);
				if (! dragObj)
					dragObj = layer;
				dragParam = new InputPointEvent(
					e,
					dragObj,
					this.scale
				);
				this.inputDown.fire(dragParam);
				if (dragObj.inputDown) {
					dragObj.inputDown.fire(dragParam);
				}
				break;
			}

			e.preventDefault();
		}
		this.onmousemove = (e) => {
			if (! dragParam)
				return;

			var param = new InputPointEvent(
				e,
				dragParam.entity,
				this.scale
			);
			if (dragParam.entity.inputMove) {
				dragParam.entity.inputMove.fire(param);
			}
			this.inputMove.fire(param);

			e.preventDefault();
		}
		this.onmouseup = (e) => {
			if (! dragParam)
				return;

			var param = new InputPointEvent(
				e,
				dragParam.entity,
				this.scale
			);
			if (dragParam.entity.inputUp) {
				dragParam.entity.inputUp.fire(param);
			}
			this.inputUp.fire(param);

			dragParam = null;

			e.preventDefault();
		}

		//TODO: Can not implementation because i dont have a test devices.
		if (document.addEventListener) {
			if (this.isTouchEnable()) {
				this.renderer.handler.addEventListener("mousedown" , this.onmousedown , false);
				this.renderer.handler.addEventListener("mousemove" , this.onmousemove , false);
				this.renderer.handler.addEventListener("mouseup"   , this.onmouseup   , false);
			} else {
				this.renderer.handler.addEventListener("mousedown" , this.onmousedown , false);
				this.renderer.handler.addEventListener("mousemove" , this.onmousemove , false);
				this.renderer.handler.addEventListener("mouseup"   , this.onmouseup   , false);
			}
		} else {
			if (this.isTouchEnable()) {
				this.renderer.handler.attachEvent("onmousedown" , onmousedown);
				this.renderer.handler.attachEvent("onmousemove" , onmousemove);
				this.renderer.handler.attachEvent("onmouseup"   , onmouseup);
			} else {
				this.renderer.handler.attachEvent("onmousedown" , onmousedown);
				this.renderer.handler.attachEvent("onmousemove" , onmousemove);
				this.renderer.handler.attachEvent("onmouseup"   , onmouseup);
			}
		}
	}

	keyboardHandler() {
		this.keymap = {
			13: Keytype.enter,
			27: Keytype.esc,
			37: Keytype.left,
			38: Keytype.up,
			39: Keytype.right,
			40: Keytype.down
		}
		var onkeydown = (e) => {
			this.inputDown.fire(new InputKeyboardEvent(this.keymap[e.keyCode], e));
			if (this.keymap[e.keyCode] != undefined)
				e.preventDefault();
		}
		var onkeyup = (e) => {
			this.inputUp.fire(new InputKeyboardEvent(this.keymap[e.keyCode], e));
			if (this.keymap[e.keyCode] != undefined)
				e.preventDefault();
		}
		if (document.addEventListener) {
			document.addEventListener("keydown", onkeydown, false);
			document.addEventListener("keyup"  , onkeyup  , false);
		} else {
			document.attachEvent("onkeydown", onkeydown);
			document.attachEvent("onkeyup"  , onkeyup);
		}
	}

	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.currentScene;
			Effect.sceneEffect(this, currentScene, scene, effect, () => {
				this.endScene();
				this.changeScene(scene);
			}, endOldScene);
			return;
		}
		this.scenes.push(scene);
		scene.game = this;
		this.currentScene.hid.fire();
		this.currentScene = scene;
		this.renderer.changeScene(this.currentScene);
		this.currentScene.started.fire();
	}

	endScene(effect?:any) {
		if (this.scenes.length == 1) {
			this.exit();
			return;
		}
		if (effect) {
			Effect.sceneEffect(this, this.currentScene, this.scenes[this.scenes.length-2], effect, () => {
				this.endScene();
			}, true);
			return;
		}
		this.currentScene.destroy();
		this.scenes.pop();
		this.currentScene.ended.fire();
		this.currentScene = this.scenes[this.scenes.length-1];
		this.renderer.changeScene(this.currentScene);
		this.currentScene.showed.fire();
	}

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

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

	preload(ary: any/*{[key:string]: string; }*/, loadingScene?:LoadingScene) {
		if (ary instanceof Array) {
			for (var i=0; i<ary.length; i++)
				this.resource.load(ary[i], ary[i]);
		} else if (typeof ary == "string") {
			var hasLoadingScene = false;
			for (var i=0; i<arguments.length; i++) {
				if (typeof arguments[i] != "string") {
					loadingScene = arguments[i];
					hasLoadingScene = true;
				} else {
					this.resource.load(arguments[i], arguments[i]);
				}
			}
			if (! hasLoadingScene)
				loadingScene = new LoadingScene(this, this.resource);

		} else {
			for (var i in ary)
				this.resource.load(i, ary[i]);
		}
		if (! loadingScene)
			loadingScene = new LoadingScene(this, this.resource);
		loadingScene.finished.handle(this, this.preloadComplete);
		this.changeScene(loadingScene);
	}

	preloadOther(identity:string) {
		var loadingScene = <LoadingScene>this.currentScene;
		loadingScene.addOtherResource(identity);
	}

	preloadCompleteOther(identity:string) {
		var loadingScene = <LoadingScene>this.currentScene;
		loadingScene.otherResourceComplete(identity);
	}

	preloadComplete() {
		this.loaded.fire();
	}

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

	main() {
		var fps_stack = new number[];
		var _main = () => {
			var t:number = window.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;
				if (this.enterFrame)
					this.enterFrameTick = t - 1;
				this.refresh();
			}

			if (this.tick < t) {
				this.update.fire(t - this.tick);
				this.tick = t;
			}
			if (this.enterFrame) {
				if (! this.enterFrameTick)
					this.enterFrameTick = t -1;

				while ((this.enterFrameTick+16) < t) {
					this.enterFrameTick += 16;
					this.enterFrame.fire();
				}
			}

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

			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 = window.getTime();
		this.renderTick = this.tick - this.targetFps;
		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;
	}
}