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

	//parameters for public
	renderer: GameRenderer;
	scenes: Scene[];
	scene: 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;
	keyDown: Trigger;
	keyUp: Trigger;
	pointDown: Trigger;
	pointUp: Trigger;
	pointMove: Trigger;
	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.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.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() {
		try {
			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);
				this.renderer.handler.removeEventListener("touchstart" , this.onmousedown, false);
				this.renderer.handler.removeEventListener("touchmove" , this.onmousemove, false);
				this.renderer.handler.removeEventListener("touchend"   , 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);
			}
		}catch(ex) {
			//ignore error of removeEventListener
		}

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

	onmousedown(e:MouseEvent) {
		var layers = this.scene.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;
			this.dragParam = new InputPointEvent(
				e,
				dragObj,
				this.scale
			);
			this.pointDown.fire(this.dragParam);
			if (this.scene.pointDown)
				this.scene.pointDown.fire(this.dragParam);
			if (dragObj.pointDown)
				dragObj.pointDown.fire(this.dragParam);

			break;
		}

		e.preventDefault();
	}
	ontouchstart(e:any) {
		var layers = this.scene.getLayerArray();
		var layer;

		var touches = e.changedTouches;
		for (var i = 0, l = touches.length; i < l; i++) {
			var touch = touches[i];
			touch.offsetX = touch.pageX - this.renderer._pageX;
			touch.offsetY = touch.pageY - this.renderer._pageY;

			var offset:CommonOffset = {
				x: this.scale ? touch.offsetX / this.scale : touch.offsetX,
				y: this.scale ? touch.offsetY / this.scale : touch.offsetY
			}
			while (layer = layers.pop()) {	//上のレイヤーから先に処理
				if (! layer.pointCapture)
					continue;

				var dragObj = layer.getEntityByPoint(offset);
				if (! dragObj)
					dragObj = layer;
				this.dragParam = new InputPointEvent(
					touch,
					dragObj,
					this.scale
				);
				this.pointDown.fire(this.dragParam);
				if (this.scene.pointDown)
					this.scene.pointDown.fire(this.dragParam);
				if (dragObj.pointDown)
					dragObj.pointDown.fire(this.dragParam);

				break;
			}
		}

		e.preventDefault();
	}

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

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

		if (this.scene.pointMove)
			this.scene.pointMove.fire(param);
		this.pointMove.fire(param);

		e.preventDefault();
	}
	ontouchmove(e:any) {
		if (! this.dragParam)
			return;

		var touches = e.changedTouches;
		for (var i = 0, l = touches.length; i < l; i++) {
			var touch = touches[i];
			touch.offsetX = touch.pageX - this.renderer._pageX;
			touch.offsetY = touch.pageY - this.renderer._pageY;
			var param = new InputPointEvent(
				touch,
				this.dragParam.entity,
				this.scale
			);
			if (this.dragParam.entity.pointMove)
				this.dragParam.entity.pointMove.fire(param);

			if (this.scene.pointMove)
				this.scene.pointMove.fire(param);
			this.pointMove.fire(param);
		}

		e.preventDefault();
	}

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

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

		if (this.scene.pointUp)
			this.scene.pointUp.fire(param);
		this.pointUp.fire(param);

		this.dragParam = null;

		e.preventDefault();
	}
	ontouchend(e:any) {
		if (! this.dragParam)
			return;

		var touches = e.changedTouches;
		for (var i = 0, l = touches.length; i < l; i++) {
			var touch = touches[i];
			touch.offsetX = touch.pageX - this.renderer._pageX;
			touch.offsetY = touch.pageY - this.renderer._pageY;
			var param = new InputPointEvent(
				touch,
				this.dragParam.entity,
				this.scale
			);
			if (this.dragParam.entity.pointUp)
				this.dragParam.entity.pointUp.fire(param);

			if (this.scene.pointUp)
				this.scene.pointUp.fire(param);
			this.pointUp.fire(param);

			this.dragParam = null;
		}

		e.preventDefault();
	}


	pointHandler() {
		this.dragParam = null;

		//TODO: Can not implementation because i dont have a test devices.
		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(this.keymap[e.keyCode], e);
		this.keyDown.fire(keyParam);
		if (this.scene.keyDown)
			this.scene.keyDown.fire(keyParam);
		if (this.keymap[e.keyCode] != undefined)
			e.preventDefault();
	}

	onkeyup(e:any) {
		var keyParam = new InputKeyboardEvent(this.keymap[e.keyCode], e);
		this.keyUp.fire(keyParam);
		if (this.scene.keyUp)
			this.scene.keyUp.fire(keyParam);
		if (this.keymap[e.keyCode] != undefined)
			e.preventDefault();
	}

	keyboardHandler() {
		this.keymap = {
			13: Keytype.enter,
			27: Keytype.esc,
			37: Keytype.left,
			38: Keytype.up,
			39: Keytype.right,
			40: Keytype.down
		}
		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.tick === undefined ? 0 : this.tick);
			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/*{[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.scene;
		loadingScene.addOtherResource(identity);
	}

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

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

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

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