module jg {
	// This classes is has modified based on the paintbrush.js.
	// 1. Filter
	// 2. BlurFilter
	// 3. EmbossFilter
	// 4. SharpenFilter
	// 5. MosaicFilter
	// 6. GreyscaleFilter
	// 7. SepiaFilter
	// 8. TintFilter
	// 9. EdgesFilter
	// 10. NoiseFilter
	// 11. MatrixFilter
	// 12. PosterizeFilter
	// --------------------------------------------------
	// paintbrush.js, v0.3
	// A browser-based image processing library for HTML5 canvas
	// Developed by Dave Shea, http://www.mezzoblue.com/
	//
	// This project lives on GitHub:
	//    http://github.com/mezzoblue/PaintbrushJS
	//
	// Except where otherwise noted, PaintbrushJS is licensed under the MIT License:
	//    http://www.opensource.org/licenses/mit-license.php
	// --------------------------------------------------
	export module ImageFilter {
		export interface IFilter {
			filter(pixels:ImageData);
		}

		export class FilterChain implements IFilter {
			filters:Filter[];
			constructor() {
				this.filters = new Filter[];
			}

			get(index:number):Filter {
				return this.filters[index];
			}

			add(filter:Filter):FilterChain {
				this.filters.push(filter);
				return this;
			}

			set(filter:Filter):FilterChain {
				this.filters = [filter];
				return this;
			}

			insert(index:number, filter:Filter):FilterChain {
				for (var i=1; i<arguments.length; i++)
					this.filters.splice(index, 0, <Filter>arguments[i]);
				return this;
			}

			remove(filter:Filter) {
				for (var i=0; i<this.filters.length; i++) {
					if (this.filters[i] == filter) {
						this.filters.splice(i, 1);
						return;
					}
				}
			}

			clear():FilterChain {
				this.filters = [];
				return this;
			}

			count():number {
				return this.filters.length;
			}

			has():bool {
				return this.filters.length > 0;
			}

			createSprite(entity:E):Sprite {
				var buffer = new BufferedRenderer({width:entity.width, height:entity.height});
				buffer.filter = this;
				var x = entity.x;
				var y = entity.y;
				entity.x = 0;
				entity.y = 0;
				buffer.renderUnit(entity);
				entity.x = x;
				entity.y = y;
				return buffer.createSprite();
			}

			createImage(entity:Sprite):HTMLCanvasElement {
				var buffer = new BufferedRenderer({width:entity.image.width, height:entity.image.height});
				var image = new Sprite(entity.image);
				image.x = 0;
				image.y = 0;
				buffer.filter = this;
				buffer.renderUnit(image);
				return buffer.createImage();
			}

			filter(pixels:ImageData) {
				var length = this.filters.length;
				for (var i=0; i<length; i++)
					this.filters[i].filter(pixels);
			}
		}

		export class Filter implements IFilter {
			opt:any;
			width:number;
			height:number;
			game:Game;

			constructor(game:Game) {
				this.opt = {}
				this.game = game;
			}

			filter(pixels:ImageData) {

			}

			getOption(name:string, defaultValue?:any):any {
				if (this.opt[name] === undefined)
					return defaultValue;
				return this.opt[name];
			}

			findColorDifference(dif:number, dest:number, src:number):number {
				return (dif * dest + (1 - dif) * src);
			}

			createColor(src:string):string {
				src = src.replace(/^#/, '');
				if (src.length == 3)
					src = src.replace(/(.)/g, '$1$1');

				return src;
			}

			applyMatrix(pixels:ImageData, matrix:number[], amount:number) {
				var data = pixels.data, imgWidth = pixels.width, height = pixels.height;
				var datalen = data.length;
				var bufferedData = new Array(data.length);
				for (var i=0; i<datalen; i++)
					bufferedData[i] = data[i];

				// calculate the size of the matrix
				var matrixSize = Math.sqrt(matrix.length);
				// also store the size of the kernel radius (half the size of the matrix)
				var kernelRadius = Math.floor(matrixSize / 2);

				// loop through every pixel
				for (var i = 1; i < imgWidth - 1; i++) {
					for (var j = 1; j < height - 1; j++) {
						// temporary holders for matrix results
						var sumR=0, sumG = 0, sumB = 0;

						// loop through the matrix itself
						for (var h = 0; h < matrixSize; h++) {
							for (var w = 0; w < matrixSize; w++) {
								// get a refence to a pixel position in the matrix
								var i2 = ((i + w - kernelRadius) +  (j + h - kernelRadius) * imgWidth) << 2;

								// apply the value from the current matrix position
								sumR += bufferedData[i2   ] * matrix[w + h * matrixSize];
								sumG += bufferedData[i2+ 1] * matrix[w + h * matrixSize];
								sumB += bufferedData[i2+ 2] * matrix[w + h * matrixSize];
							}
						}

						// get a reference for the final pixel
						var ref = (i + j * imgWidth) << 2;
						var r = data[ref],
						    g = data[ref + 1],
						    b = data[ref + 2];

						// finally, apply the adjusted values
						data[ref]     = this.findColorDifference(amount, sumR, r);
						data[ref + 1] = this.findColorDifference(amount, sumG, g);
						data[ref + 2] = this.findColorDifference(amount, sumB, b);
					}
				}

				// code to clean the secondary buffer out of the DOM would be good here

				return(pixels);
			}

			checkRGBBoundary(val:number) {
				if (val < 0)
					return 0;
				else if (val > 255)
					return 255;

				return val;
			}
		}

		export class UniversalTransitionFilter extends Filter {
			ruleImage:ImageData;
			mask:any;

			constructor(game:Game, image?:any, amount?:number, repeat?:bool) {
				super(game);
				this.opt.amount = amount;
				this.opt.image = image;
				this.opt.repeat = repeat;
			}

			getImageData(image:any, canvas?:HTMLCanvasElement):ImageData {
				var context;
				if (! canvas) {
					canvas = window.createCanvas(image.width, image.height);
					context = canvas.getContext("2d");
					context.drawImage(image, 0, 0);
				} else {
					context = canvas.getContext("2d");
				}
				return context.getImageData(0, 0, image.width, image.height);
			}

			createRuleImage() {
				var image = this.opt.image;
				if (image instanceof ImageData) {
					this.ruleImage = image;
				} else if (image instanceof HTMLCanvasElement) {
					this.ruleImage = this.getImageData(image, image);
				} else if (image instanceof HTMLImageElement || image instanceof HTMLVideoElement) {
					this.ruleImage = this.getImageData(image);
				} else {
					//これは多分E系じゃないかな
					var sprite = image.createSprite();
					this.ruleImage = this.getImageData(sprite.image);
				}
			}

			createMask() {
				var mask;
				var a = (this.getImageData(this.opt.mask1)).data;
				var b = (this.getImageData(this.opt.mask2)).data;
				var d = this.ruleImage.data;
				mask = {};
				var enableCount = 0;
				for (var i=0, len=a.length; i<len; i+=4) {
					if (a[i] == b[i] && a[i+1] == b[i+1] && a[i+2] == b[i+2] && a[i+3] == b[i+3])
						mask[i] = 1;
					else 
						mask[i] = 0;
				}
				this.mask = mask;
				return mask;
			}

			filter(pixels:ImageData) {
				if (! this.ruleImage)
					this.createRuleImage();

				var amount = this.getOption("amount", 255);
				var repeat = this.getOption("repeat", false);

				var w = pixels.width;
				var h = pixels.height;
				var w2 = this.ruleImage.width;
				var h2 = this.ruleImage.height;
				var data = pixels.data;
				var data2 = this.ruleImage.data;
				var mask;

				if (this.opt.mask1 && this.opt.mask2)
					mask = (this.mask) ? this.mask : this.createMask();

				if (w == w2 && h == h2) {
					for (var i = 0, length = data.length; i < length >> 2; i++) {
						var index = i << 2;
						if (mask && mask[index]) {
							if (this.opt.maskDraw) {
								continue;
							} else {
								data[index + 3] = 0;
								continue;
							}
						}
						data[index + 3] = Math.round(data[index + 3] / 255 * Math.max(0, data2[index] - amount));
					}
				} else if (repeat) {
					for (var x=0; x<w; x++) {
						for (var y=0; y<h; y++) {
							var index = (x + y * w) * 4;
							if (mask && mask[index]) {
								if (this.opt.maskDraw) {
									continue;
								} else {
									data[index + 3] = 0;
									continue;
								}
							}
							var x2 = x % w2;
							var y2 = y % h2;
							var index2 = (x2 + y2 * w2) * 4;
							data[index + 3] = Math.round(data[index + 3] / 255 * Math.max(0, data2[index2] - amount));
						}
					}
				} else {
					//Note: 計算簡略化。0.133333...とかだと環境によっては異常に遅くなるので、大体で。
					var xPer = Math.round(w2 / w * 100) / 100;
					var yPer = Math.round(h2 / h * 100) / 100;
					for (var x=0; x<w; x++) {
						for (var y=0; y<h; y++) {
							var index = (x + y * w) * 4;
							if (mask && mask[index]) {
								if (this.opt.maskDraw) {
									continue;
								} else {
									data[index + 3] = 0;
									continue;
								}
							}
							var x2 = Math.round(xPer * x);
							var y2 = Math.round(yPer * y);
							var index2 = (x2 + y2 * w2) * 4;
							data[index + 3] = Math.round(data[index + 3] / 255 * Math.max(0, data2[index2] - amount));
						}
					}
				}
			}
		}

		export class ReverseUniversalTransitionFilter extends UniversalTransitionFilter {
			createImageData(width:number, height:number) {
				var canvas = window.createCanvas(width, height);
				var context = canvas.getContext("2d");
				return context.createImageData(width, height);
			}

			createRuleImage() {
				super.createRuleImage();
				var d = this.ruleImage.data;
				for (var i = 0, length = d.length; i < length >> 2; i++) {
					var index = i << 2;
					d[index]   = 255-d[index];
					d[index+1] = 255-d[index+1];
					d[index+2] = 255-d[index+2];
				}
			}
		}

		export class GreyscaleFilter extends Filter {
			constructor(game:Game, opacity?:number) {
				super(game);
				this.opt.opacity = opacity;
			}

			filter(pixels:ImageData) {
				var opacity = this.getOption("opacity", 1);
				var data = pixels.data;

				for (var i = 0, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];

					var val = r * 0.21 + g * 0.71 + b * 0.07;
					data[index]     = this.findColorDifference(opacity, val, r);
					data[index + 1] = this.findColorDifference(opacity, val, g);
					data[index + 2] = this.findColorDifference(opacity, val, b);
				}
			}
		}

		export class SepiaFilter extends Filter {
			constructor(game:Game, opacity?:number) {
				super(game);
				this.opt.opacity = opacity;
			}

			filter(pixels:ImageData) {
				var opacity = this.getOption("opacity", 1);
				var data = pixels.data;

				for (var i = 0, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];
					data[index]     = this.findColorDifference(
						opacity,
						r * 0.393 + g * 0.769 + b * 0.189,
						r
					);
					data[index + 1] = this.findColorDifference(
						opacity,
						r * 0.349 + g * 0.686 + b * 0.168,
						g
					);
					data[index + 2] = this.findColorDifference(
						opacity,
						r * 0.272 + g * 0.534 + b * 0.131,
						b
					);
				}
			}
		}

		export class TintFilter extends Filter {
			constructor(game:Game, color?:string, opacity?:number) {
				super(game);
				this.opt.color = color;
				this.opt.opacity = opacity;
			}

			filter(pixels:ImageData) {
				var opacity = this.getOption("opacity", 1);
				var color = this.getOption("color", "#f00");
				var data = pixels.data;

				var src  = parseInt(this.createColor(color), 16);
				var r2 = (src & 0xFF0000) >> 16,
				    g2 = (src & 0x00FF00) >> 8,
				    b2 = (src & 0x0000FF);

				for (var i = 0, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];
					data[index]     = this.findColorDifference(opacity, r2, r);
					data[index + 1] = this.findColorDifference(opacity, g2, g);
					data[index + 2] = this.findColorDifference(opacity, b2, b);
				}
			}
		}

		export class EdgesFilter extends Filter {
			constructor(game:Game, amount?:number) {
				super(game);
				this.opt.amount = amount;
			}

			filter(pixels:ImageData) {
				var matrix = [
					0,  1,  0,
					1, -4,  1,
					0,  1,  0
				];
				this.applyMatrix(
					pixels,
					matrix,
					this.getOption("amount", 1)
				);
			}
		}

		export class EmbossFilter extends Filter {
			constructor(game:Game, amount?:number) {
				super(game);
				this.opt.amount = amount;
			}

			filter(pixels:ImageData) {
				var matrix = [
					-2,-1,  0,
					-1, 1,  1,
					0,  1,  2
				];
				this.applyMatrix(
					pixels,
					matrix,
					this.getOption("amount", 0.5)
				);
			}
		}

		export class SharpenFilter extends Filter {
			constructor(game:Game, amount?:number) {
				super(game);
				this.opt.amount = amount;
			}

			filter(pixels:ImageData) {
				var matrix = [
					-1,-1, -1,
					-1, 9, -1,
					-1,-1, -1
				];
				this.applyMatrix(
					pixels,
					matrix,
					this.getOption("amount", 0.5)
				);
			}
		}

		export class MatrixFilter extends Filter {
			constructor(game:Game, amount?:number, matrix?:number[]) {
				super(game);
				this.opt.amount = amount;
				this.opt.matrix = matrix;
			}

			filter(pixels:ImageData) {
				var matrix = this.getOption("matrix", [
					0.111, 0.111, 0.111,
					0.111, 0.111, 0.111,
					0.111, 0.111, 0.111
				]);
				this.applyMatrix(
					pixels,
					matrix,
					this.getOption("amount", 0.5)
				);
			}
		}

		export class BlurFilter extends Filter {
			constructor(game:Game, amount?:number) {
				super(game);
				this.opt.amount = amount;
			}

			// calculate gaussian blur
			// adapted from http://pvnick.blogspot.com/2010/01/im-currently-porting-image-segmentation.html
			filter(pixels:ImageData) {
				var width = pixels.width;
				var width4 = width << 2;
				var height = pixels.height;
				var amount = this.getOption("amount", 2);
				
				if (pixels) {
					var data = pixels.data;
					
					// compute coefficients as a function of amount
					var q;
					if (amount < 0.0)
						amount = 0.0;

					if (amount >= 2.5) {
						q = 0.98711 * amount - 0.96330; 
					} else if (amount >= 0.5) {
						q = 3.97156 - 4.14554 * Math.sqrt(1.0 - 0.26891 * amount);
					} else {
						q = 2 * amount * (3.97156 - 4.14554 * Math.sqrt(1.0 - 0.26891 * 0.5));
					}
					
					//compute b0, b1, b2, and b3
					var qq = q * q;
					var qqq = qq * q;
					var b0 = 1.57825 + (2.44413 * q) + (1.4281 * qq ) + (0.422205 * qqq);
					var b1 = ((2.44413 * q) + (2.85619 * qq) + (1.26661 * qqq)) / b0;
					var b2 = (-((1.4281 * qq) + (1.26661 * qqq))) / b0;
					var b3 = (0.422205 * qqq) / b0; 
					var bigB = 1.0 - (b1 + b2 + b3); 
					
					// horizontal
					for (var c = 0; c < 3; c++) {
						for (var y = 0; y < height; y++) {
							// forward 
							var index = y * width4 + c;
							var indexLast = y * width4 + ((width - 1) << 2) + c;
							var pixel = data[index];
							var ppixel = pixel;
							var pppixel = ppixel;
							var ppppixel = pppixel;
							for (; index <= indexLast; index += 4) {
								pixel = bigB * data[index] + b1 * ppixel + b2 * pppixel + b3 * ppppixel;
								data[index] = pixel; 
								ppppixel = pppixel;
								pppixel = ppixel;
								ppixel = pixel;
							}
							// backward
							index = y * width4 + ((width - 1) << 2) + c;
							indexLast = y * width4 + c;
							pixel = data[index];
							ppixel = pixel;
							pppixel = ppixel;
							ppppixel = pppixel;
							for (; index >= indexLast; index -= 4) {
								pixel = bigB * data[index] + b1 * ppixel + b2 * pppixel + b3 * ppppixel;
								data[index] = pixel;
								ppppixel = pppixel;
								pppixel = ppixel;
								ppixel = pixel;
							}
						}
					}
					
					// vertical
					for (var c = 0; c < 3; c++) {
						for (var x = 0; x < width; x++) {
							// forward 
							var index = (x << 2) + c;
							var indexLast = (height - 1) * width4 + (x << 2) + c;
							var pixel = data[index];
							var ppixel = pixel;
							var pppixel = ppixel;
							var ppppixel = pppixel;
							for (; index <= indexLast; index += width4) {
								pixel = bigB * data[index] + b1 * ppixel + b2 * pppixel + b3 * ppppixel;
								data[index] = pixel;
								ppppixel = pppixel;
								pppixel = ppixel;
								ppixel = pixel;
							} 
							// backward
							index = (height - 1) * width4 + (x << 2) + c;
							indexLast = (x << 2) + c;
							pixel = data[index];
							ppixel = pixel;
							pppixel = ppixel;
							ppppixel = pppixel;
							for (; index >= indexLast; index -= width4) {
								pixel = bigB * data[index] + b1 * ppixel + b2 * pppixel + b3 * ppppixel;
								data[index] = pixel;
								ppppixel = pppixel;
								pppixel = ppixel;
								ppixel = pixel;
							}
						}
					} 
				}
			}
		}

		export class MosaicFilter extends Filter {
			constructor(game:Game, size?:number, opacity?:number) {
				super(game);
				this.opt.size = size;
				this.opt.opacity = opacity;
			}

			filter(pixels:ImageData) {
				var opacity = this.getOption("opacity", 1);
				var size = Math.round(this.getOption("size", 5));
				var width = pixels.width;

				for (var i = 0, data = pixels.data, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];

					var pos = index >> 2;
					var stepY = Math.floor(pos / width);
					var stepY1 = stepY % size;
					var stepX = pos - (stepY * width);
					var stepX1 = stepX % size;

					if (stepY1) pos -= stepY1 * width;
					if (stepX1) pos -= stepX1;
					pos = pos << 2;

					data[index  ] = this.findColorDifference(opacity, data[pos]    , r);
					data[index+1] = this.findColorDifference(opacity, data[pos + 1], g);
					data[index+2] = this.findColorDifference(opacity, data[pos + 2], b);
				}
			}
		}

		export enum NoiseType {
			Mono,	//monochrome
			Color
		}

		export class NoiseFilter extends Filter {
			constructor(game:Game, amount?:number, type?:NoiseType) {
				super(game);
				this.opt.amount = amount;
				this.opt.type = type;
			}

			filter(pixels:ImageData) {
				var amount = this.getOption("amount", 30);
				var type = this.getOption("type", NoiseType.Mono);
				var max = (amount >> 1) - 1;
				var min = max - amount;

				for (var i = 0, data = pixels.data, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];

					if (type == NoiseType.Mono) {
						var val = this.game.random(min, max);
						data[index  ] = this.checkRGBBoundary(r + val);
						data[index+1] = this.checkRGBBoundary(g + val);
						data[index+2] = this.checkRGBBoundary(b + val);
					} else {
						data[index  ] = this.checkRGBBoundary(r + this.game.random(min, max));
						data[index+1] = this.checkRGBBoundary(g + this.game.random(min, max));
						data[index+2] = this.checkRGBBoundary(b + this.game.random(min, max));
					}
				}
			}
		}

		export class PosterizeFilter extends Filter {
			constructor(game:Game, amount?:number, opacity?:number) {
				super(game);
				this.opt.opacity = opacity;
				this.opt.amount = amount;
			}

			filter(pixels:ImageData) {
				var opacity:number = this.getOption("opacity", 1);
				var amount:number = this.getOption("amount", 2);
				var areas:number = 256 / amount;
				var values:number = 255 / (amount - 1);

				for (var i = 0, data = pixels.data, length = data.length; i < length >> 2; i++) {
					var index = i << 2;
					var r = data[index],
					    g = data[index + 1],
					    b = data[index + 2];

					data[index  ] = this.findColorDifference(
						opacity,
						Math.round(values * Math.round(r / areas)),
						r
					);
					data[index+1] = this.findColorDifference(
						opacity,
						Math.round(values * Math.round(g / areas)),
						g
					);
					data[index+2] = this.findColorDifference(
						opacity,
						Math.round(values * Math.round(b / areas)),
						b
					);
				}			
			}
		}
	}
}