///<reference path="all.ts"/>
module jg {
	/**
	 * ブラウザ情報
	 */
	export interface BrowserInfo {
		/** Chromeであればtrue */
		chrome?: boolean;
		/** Webkit系であればtrue */
		webkit?: boolean;
		/** Safariであればtrue */
		safari?: boolean;
		/** Operaであればtrue */
		opera?: boolean;
		/** Internet Explorerであればtrue */
		msie?: boolean;
		/** FireFoxであればtrue */
		mozilla?: boolean;
		/** ブラウザのバージョン */
		version?: string;
	}

	/**
	 * jgame.jsのユーティリティ関数群。
	 * Note: このクラスに似たようなコードが多いのは関数コールのオーバーヘッドを避けるためで、意図的に最適化をしていない結果
	 */
	export class JGUtil {
		/** ブラウザ情報 */
		static browser: BrowserInfo;

		/**
		 * 座標の中心地を返す。
		 * 引数がCommonAreaである場合、サイズも計算して返し、CommonOffsetである場合は何も計算せずに返す
		 * @param p 判定する座標または領域
		 */
		static getCenterPoint(p:CommonOffset):CommonOffset {
			var a = <CommonArea>p;
			if (a.width && a.height) {
				return {
					x: p.x + a.width / 2,
					y: p.y + a.height / 2
				}
			}
			return p;
		}

		/**
		 * 座標の中止点から見ての余白を返す。
		 * 引数がCommonAreaである場合、サイズを計算して返し、CommonOffsetである場合はx:0, y:0を返す
		 * @param p 判定する座標または領域
		 */
		static getMargin(p:CommonOffset):CommonOffset {
			var a = <CommonArea>p;
			if (a.width && a.height) {
				return {
					x: a.width / 2,
					y: a.height / 2
				}
			}
			return {x:0, y:0};
		}

		/**
		 * 二つの座標または領域が衝突しているかを判定する
		 * @param p1 判定する座標または領域
		 * @param p2 判定する座標または領域
		 */
		static intersect(p1:CommonOffset, p2:CommonOffset):boolean {
			var a1 = <CommonArea>p1;
			var a2 = <CommonArea>p2;
			if (a1.width && a1.height) {
				if (a2.width && a2.height) {
					//area:area
					return a1.x < (a2.x+a2.width) &&
					       a2.x < (a1.x+a1.width) &&
					       a1.y < (a2.y+a2.height) &&
					       a2.y < (a1.y+a1.height);
				} else {
					//area:point
					return a2.x >= a1.x && a2.x < (a1.x + a1.width)
					    && a2.y >= a1.y && a2.y < (a1.y + a1.height);
				}
			} else if (a2.width && a2.height) {
				//point:area
				return a1.x >= a2.x && a1.x < (a2.x + a2.width)
				    && a1.y >= a2.y && a1.y < (a2.y + a2.height);
			}

			//point:point
			return p1.x == p2.x && p1.y == p2.y;
		}

		/**
		 * 二つの座標または領域の距離を返す。
		 * 領域である場合、中心点からの距離となる点に注意
		 * @param p1 判定する座標または領域
		 * @param p2 判定する座標または領域
		 */
		static getDistance(p1:CommonOffset, p2:CommonOffset):CommonOffset {
			var _p1 = JGUtil.getCenterPoint(p1);
			var _p2 = JGUtil.getCenterPoint(p2);
			return {
				x: Math.abs(_p1.x - _p2.x),
				y: Math.abs(_p1.y - _p2.y)
			}
		}

		/**
		 * 二つのオブジェクトが追尾しあう場合の速度に応じた移動量を返す
		 * @param p1 追尾する座標または領域
		 * @param p2 追尾する座標または領域
		 * @param power 移動力。省略時は1（瞬時に移動を完了するだけの量）
		 * @param maxMove 移動最大値。省略時は判定しない
		 */
		static getMovePoint(p1:CommonOffset, p2:CommonOffset, power?:number, maxMove?:number):CommonOffset {
			var _p1 = JGUtil.getCenterPoint(p1);
			var _p2 = JGUtil.getCenterPoint(p2);

			if (! power)
				power = 1;
			var ret = {
				x: (_p1.x - _p2.x) * power,
				y: (_p1.y - _p2.y) * power
			}
			var absx = Math.abs(ret.x);
			var absy = Math.abs(ret.y);
			var max = Math.max(absx, absy);
			var xper = absx / max;
			var yper = absy / max;
			if (maxMove) {
				if (absx > maxMove || absy > maxMove) {
					if (ret.x < 0)
						ret.x = -maxMove * xper;
					else
						ret.x = maxMove * xper;
					if (ret.y < 0)
						ret.y = -maxMove * yper;
					else
						ret.y = maxMove * yper;
				}
			}
			return ret;
		}

		/**
		 * 二つの座標または領域を比較し、p1から見てp2がどの方角にあるかをAngle型で返す
		 * @param p1 判定する座標または領域
		 * @param p2 判定する座標または領域
		 * @param minDistance 最小距離。指定するとこの距離以上に距離がある場合、nullを返す
		 */
		static getDirectionAngle(p1: CommonOffset, p2:CommonOffset, minDistance?:number):Angle {
			var _p1 = JGUtil.getCenterPoint(p1);
			var _p2 = JGUtil.getCenterPoint(p2);
			var xp = Math.abs(_p1.x - _p2.x);
			var yp = Math.abs(_p1.y - _p2.y);

			if (minDistance && Math.max(xp, yp) < minDistance)
				return null;

			if (xp > yp)
				return (_p1.x > _p2.x) ? Angle.Left : Angle.Right;
			else
				return (_p1.y > _p2.y) ? Angle.Up : Angle.Down;
		}

		/**
		 * 二つの座標または領域を比較し、p1から見てp2がどの方角にあるかをKeytype型で返す
		 * @param p1 判定する座標または領域
		 * @param p2 判定する座標または領域
		 * @param minDistance 最小距離。指定するとこの距離以上に距離がある場合、nullを返す
		 */
		static getDirectionKeytype(p1: CommonOffset, p2:CommonOffset, minDistance?:number):Keytype {
			var _p1 = JGUtil.getCenterPoint(p1);
			var _p2 = JGUtil.getCenterPoint(p2);
			var xp = Math.abs(_p1.x - _p2.x);
			var yp = Math.abs(_p1.y - _p2.y);

			if (minDistance && Math.max(xp, yp) < minDistance)
				return null;

			if (xp > yp)
				return (_p1.x > _p2.x) ? Keytype.Left : Keytype.Right;
			else
				return (_p1.y > _p2.y) ? Keytype.Up : Keytype.Down;
		}

		/**
		 * p1にp2を横方向で追尾させる
		 * @param p1 追尾する座標または領域
		 * @param p2 追尾される座標または領域
		 * @param speed 移動速度
		 * @param t 経過時間
		 */
		static homingX(p1:CommonOffset, p2:CommonOffset, speed:number, t:number):boolean {
			var m1 = JGUtil.getMargin(p1);
			var m2 = JGUtil.getMargin(p2);
			var x = p2.x + m2.x - m1.x;
			if (p1.x > x) {
				p1.x -= speed * t;
				if (p1.x < x) {
					p1.x = x;
					return true;
				}
				return false;
			} else if (p1.x < x) {
				p1.x += speed * t;
				if (p1.x > x) {
					p1.x = x;
					return true;
				}
				return false;
			}
			return true;
		}

		/**
		 * p1にp2を縦方向で追尾させる
		 * @param p1 追尾する座標または領域
		 * @param p2 追尾される座標または領域
		 * @param speed 移動速度
		 * @param t 経過時間
		 */
		static homingY(p1:CommonOffset, p2:CommonOffset, speed:number, t:number):boolean {
			var m1 = JGUtil.getMargin(p1);
			var m2 = JGUtil.getMargin(p2);
			var y = p2.y + m2.y - m1.y;
			if (p1.y > y) {
				p1.y -= speed * t;
				if (p1.y < y) {
					p1.y = y;
					return true;
				}
				return false;
			} else if (p1.y < y) {
				p1.y += speed * t;
				if (p1.y > y) {
					p1.y = y;
					return true;
				}
				return false;
			}
			return true;
		}

		/**
		 * p1にp2を横方向で追尾させる
		 * @param p1 追尾する座標または領域
		 * @param p2 追尾される座標または領域
		 * @param speed 移動速度
		 * @param t 経過時間
		 */
		static homing(p1:CommonOffset, p2:CommonOffset, speed:number, t:number):boolean {
			var m1 = JGUtil.getMargin(p1);
			var m2 = JGUtil.getMargin(p2);
			var p = {
				x: p2.x + m2.x - m1.x,
				y: p2.y + m2.y - m1.y
			}
			var xng, yng;
			if (p1.x > p.x) {
				p1.x -= speed * t;
				if (p1.x < p.x)
					p1.x = p.x;
				else
					xng = true;
			} else if (p1.x < p.x) {
				p1.x += speed * t;
				if (p1.x > p.x)
					p1.x = p.x;
				else
					xng = true;
			}

			if (p1.y > p.y) {
				p1.y -= speed * t;
				if (p1.y < p.y)
					p1.y = p.y;
				else
					yng = true;
			} else if (p1.y < p.y) {
				p1.y += speed * t;
				if (p1.y > p.y)
					p1.y = p.y;
				else
					yng = true;
			}

			return xng || yng ? false : true;
		}

		//bubble sort
		/**
		 * 表示順序をY座標順にするためのメソッドサンプル	。
		 * 下記のようにLayerなどに指定すると、描画順を変更できる。
		 * game.scene.root.orderDraw = JGUtil.orderDrawY;
		 */
		static orderDrawY() {
			var e = this.entities, len = e.length;
			for (var i=1; i<len; i++) {
				for (var j=i; j>0; j--) {
					if (e[j].y >= e[j-1].y)
						break;
					//slow e.splice(j-1, 2, e[j],e[j-1]);
					var tmp = e[j];
					e[j] = e[j-1];
					e[j-1] = tmp;
				}
			}
		}

		/**
		 * 線形グラデーションを作成する。ここで作成したグラデーションは、Shapeの色などに用いることが出来る。
		 * @param rect グラデーションの範囲を指定。Rectangleクラス、またはjsの場合は4つの引数に分けてもよい。引数を4つにけると、colorsが第五引数、offsetsが第六引数となる
		 * @param colors グラデーション色。CSSカラーで指定する
		 * @param offsets グラデーションのオフセット値を0～1の範囲で指定。colorsと同じ個数指定する。省略した場合、等分に割られた値が自動的に割り当てられる
		 */
		static createLinearGradient(rect:any, colors:string[], offsets?:number[]):CanvasGradient {
			var canvas = window.createCanvas(1, 1);
			var context = canvas.getContext("2d");

			if (typeof rect == "number") {
				rect = new Rectangle(arguments[0], arguments[1], arguments[2], arguments[3]);
				colors = arguments[4];
				offsets = arguments[5];
			}

			if (offsets == undefined) {
				offsets = [];
				var p = 1 / (colors.length-1);
				for (var i=0; i<colors.length; i++)
					offsets.push(i*p);
			}

			var gradient = context.createLinearGradient(rect.left, rect.top, rect.right, rect.bottom);
			for (var i=0; i<colors.length; i++)
				gradient.addColorStop(offsets[i], colors[i]);

			return gradient;
		}

		/**
		 * 円形グラデーションを作成する。ここで作成したグラデーションは、Shapeの色などに用いることが出来る。
		 * @param rect グラデーションの範囲を指定。Rectangleクラス、またはjsの場合は4つの引数に分けてもよい。
		 * @param radius1 開始円の半径
		 * @param radius2 終了円の半径
		 * @param colors グラデーション色。CSSカラーで指定する
		 * @param offsets グラデーションのオフセット値を0～1の範囲で指定。colorsと同じ個数指定する。省略した場合、等分に割られた値が自動的に割り当てられる
		 */
		static createRadialGradient(rect:any, radius1:number, radius2:number, colors:string[], offsets?:number[]):CanvasGradient {
			var canvas = window.createCanvas(1, 1);
			var context = canvas.getContext("2d");

			if (typeof rect == "number") {
				rect = new Rectangle(arguments[0], arguments[1], arguments[2], arguments[3]);
				radius1 = arguments[4];
				radius2 = arguments[5];
				colors = arguments[6];
				offsets = arguments[7];
			}

			if (offsets == undefined) {
				offsets = [];
				var p = 1 / (colors.length-1);
				for (var i=0; i<colors.length; i++)
					offsets.push(i*p);
			}

			var gradient = context.createRadialGradient(rect.left, rect.top, radius1, rect.right, rect.bottom, radius2);
			for (var i=0; i<colors.length; i++)
				gradient.addColorStop(offsets[i], colors[i]);

			return gradient;
		}

		//repeat: repeat, repeat-x, repeat-y, no-repeat
		/**
		 * パターン画像を作成する。ここで作成したパターン画像は、Shapeの色などに用いることが出来る
		 * @param image パターン画像のソースイメージ
		 * @param repeat 繰り返し方法を、repeat, repeat-x, repeat-y, no-repeatいずれかの文字列で指定。省略時はrepeatになる
		 */
		static createPattern(image:any, repeat?:string):CanvasPattern {
			var canvas = window.createCanvas(1, 1);
			var context = canvas.getContext("2d");
			return context.createPattern(image, repeat == undefined ? "repeat" : repeat);
		}

		/**
		 * style属性を使って拡縮しているかどうか
		 */
		static isStyleScale:boolean;
		/**
		 * 変形方法を返し、JGUtil.isStyleScaleに値が無い場合は値をセットする
		 */
		static isTransformMode() {
			if (JGUtil.isStyleScale === undefined) {
				var test = document.createElement("canvas");
				JGUtil.isStyleScale = test.style['webkitTransform'] === undefined;
			}
			return !JGUtil.isStyleScale;
		}

		/**
		 * キャンバスを引き伸ばす。通常、ゲームのサイズ変更で利用される
		 * @param canvas 対象のキャンバス
		 * @param size 引き伸ばした後のサイズ
		 */
		static scaleCanvas(canvas:HTMLCanvasElement, size:CommonSize) {
			if (JGUtil.isTransformMode()) {
				canvas.style['webkitTransformOrigin'] = '0 0';
				canvas.style['webkitTransform'] = 'scale(' + Math.max(
					size.width / canvas.width,
					size.height / canvas.height
				) + ')';
			} else {
				canvas.style.width = size.width+"px";
				canvas.style.height = size.height+"px";
			}
		}

		/**
		 * ブラウザ情報を取得する。
		 * このメソッドは一度取得した後はJGUtil.browserに情報を格納し、以後はJGUtil.browserを返す。
		 */
		static getBrowser() {
			if (JGUtil.browser)
				return JGUtil.browser;
			//this function arranged by jquery 1.8.3. http://jquery.com/ (jquery 1.9 deleted this source)
			//Note: jQuery 1.9のsupportは煩雑なので、uaを偽装されたらその偽装されたua通りに処理すればいいという方針
			var ua = navigator.userAgent.toLowerCase();

			var match = /(chrome)[ \/]([\w.]+)/.exec( ua ) ||
				/(webkit)[ \/]([\w.]+)/.exec( ua ) ||
				/(opera)(?:.*version|)[ \/]([\w.]+)/.exec( ua ) ||
				/(msie) ([\w.]+)/.exec( ua ) ||
				ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec( ua ) ||
				[];

			var ret:BrowserInfo = {}
			if ( match[1] ) {
				ret[match[1]] = true;
				ret["version"] = match[2];
			}

			if (ret.chrome)
				ret.webkit = true;
			else if (ret.webkit)
				ret.safari = true;

			JGUtil.browser = ret;

			return ret;
		}

		/**
		 * 画像描画時のアンチエイリアス効果を変更する。
		 * 通常ドット絵を拡大するとアンチエイリアスによって滲むが、滲ませずにドットを強調して拡大する場合などに利用する。
		 * 変更後、ゲームはrefreshによって再構築される。
		 * このメソッドはページに存在するすべてのゲームに影響を及ぼす。将来的には指定されたゲームのみに影響が及ぶよう改修予定
		 * @param game 対象のゲーム
		 * @param crispEdges trueにすると転送時にアンチエイリアスをオフにする
		 */
		static setCrispEdges(game:Game, crispEdges:boolean) {
			if (crispEdges)
				window["imageSmoothingEnabled"] = crispEdges;
			else
				delete window["imageSmoothingEnabled"];
			game.refresh();
		}

		/**
		 * 必ず引数selfをthisとして受け取るコールバック関数を生成する。
		 * このメソッドで生成したコールバックでDOMのaddEventListenerを実行すると、removeEventListenerがしにくくなる点に注意。
		 * removeEventListenerをする可能性がある場合、createIdProxyの利用を検討した方がよい。
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 */
		static proxy(func:Function, self:any):()=>void {
			return () => {
				func.apply(self, arguments);
			}
		}

		/**
		 * createIdProxyで生成されたID付きproxyデータ
		 */
		private static idData:IdData[];

		/**
		 * ID付きのプロキシデータを生成する。
		 * @param id 一意のID。通常、game.idを指定する
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 */
		static createIdProxy(id:number, func:Function, self:any):()=>void {
			var proxy = JGUtil.proxy(func, self);
			JGUtil.idData[id].proxies.push(new ProxyData(func, self, proxy));
			return proxy;
		}

		/**
		 * 指定のID付きプロキシデータを取得する
		 * @param id 一意のID。通常、game.idを指定する
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 */
		static getIdProxy(id:number, func:Function, self:any):()=>void {
			var proxies = JGUtil.idData[id].proxies;
			for (var i=0; i<proxies.length; i++)
				if (proxies[i].is(func, self))
					return proxies[i].proxy;

			return null;
		}

		/**
		 * ID付きプロキシデータを削除する
		 * @param id 一意のID。通常、game.idを指定する
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 */
		static deleteIdProxy(id:number, func:Function, self:any) {
			var proxies = JGUtil.idData[id].proxies;
			for (var i=0; i<proxies.length; i++) {
				if (proxies[i].is(func, self)) {
					proxies.splice(i, 1);
					i--;
				}
			}
		}

		/**
		 * 一意のIDを生成する
		 */
		static generateId():number {
			if (! JGUtil.idData)
				JGUtil.idData = [];
			JGUtil.idData.push(new IdData());
			return JGUtil.idData.length - 1;
		}

		/**
		 * 二次元配列のX軸とY軸を入れ替えた結果を返す。
		 * マトリックスのみのサポートであり、src[0].lengthとsrc[1].lengthが異なる配列はサポートされない。
		 * src[x][y] == ret[y][x]
		 * @param src 入力元配列
		 */
		static transpose(src:any[][]):any[][] {
			var ret = new Array(src[0].length);
			for (var y=0, ylen=ret.length; y<ylen; y++)
				ret[y] = new Array(src.length);

			for (var x=0, xlen=src.length; x<xlen; x++)
				for (var y=0, ylen=src[x].length; y<ylen; y++)
					ret[y][x] = src[x][y];

			return ret;
		}
	}

	/**
	 * ID付きプロキシデータを管理する内部クラス
	 */
	class IdData {
		/** プロキシの実データ */
		proxies: ProxyData[];

		/**
		 * コンストラクタ
		 */
		constructor() {
			this.proxies = [];
		}
	}

	/**
	 * プロキシデータ
	 */
	class ProxyData {
		/**
		 * コンストラクタ
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 * @param proxy プロキシ関数本体
		 */
		constructor(private func:Function, private self:any, public proxy:()=>void) {
		}

		/**
		 * 引数で指定された値とこのプロキシが同じものかを判定する
		 * @param func コールバック関数
		 * @param self thisとなるオブジェクト
		 */
		is(func:Function, self:any) {
			return this.func == func && this.self == self;
		}
	}
}
