// 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
// --------------------------------------------------
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.width, entity.image.height, 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;

		constructor() {
			this.opt = {}
		}

		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 GreyscaleFilter extends Filter {
		constructor(opacity?:number) {
			super();
			this.opt.opacity = opacity;
		}

		filter(pixels:ImageData) {
			var opacity = this.getOption("opacity", 1);
			var data = 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(opacity?:number) {
			super();
			this.opt.opacity = opacity;
		}

		filter(pixels:ImageData) {
			var opacity = this.getOption("opacity", 1);
			var data = 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(color?:string, opacity?:number) {
			super();
			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 = 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(amount?:number) {
			super();
			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(amount?:number) {
			super();
			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(amount?:number) {
			super();
			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(amount?:number, matrix?:number[]) {
			super();
			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(amount?:number) {
			super();
			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(size?:number, opacity?:number) {
			super();
			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(amount?:number, type?:NoiseType) {
			super();
			this.opt.amount = amount;
			this.opt.type = type;
		}

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

			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 = Math.floor((amount >> 1) - (Math.random() * amount));
					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 + Math.floor((amount >> 1) - (Math.random() * amount)));
					data[index+1] = this.checkRGBBoundary(g + Math.floor((amount >> 1) - (Math.random() * amount)));
					data[index+2] = this.checkRGBBoundary(b + Math.floor((amount >> 1) - (Math.random() * amount)));
				}
			}
		}
	}

	export class PosterizeFilter extends Filter {
		constructor(amount?:number, opacity?:number) {
			super();
			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
				);
			}			
		}
	}
}
