/**
 * @preserve
 * Copyright (c) 2015, Igor Bezkrovny
 * All rights reserved. (MIT Licensed)
 *
 * wuQuant.ts - part of Image Quantization Library
 */
///<reference path="../../distance/common.ts"/>
module IQ.Palette {

	function createArray1D(dimension1) {
		var a = [];
		for (var k = 0; k < dimension1; k++) {
			a[k] = 0;
		}
		return a;
	}

	function createArray4D(dimension1, dimension2, dimension3, dimension4) : number[][][][] {
		var a = new Array(dimension1);
		for (var i = 0; i < dimension1; i++) {
			a[i] = new Array(dimension2);
			for (var j = 0; j < dimension2; j++) {
				a[i][j] = new Array(dimension3);
				for (var k = 0; k < dimension3; k++) {
					a[i][j][k] = new Array(dimension4);
					for (var l = 0; l < dimension4; l++) {
						a[i][j][k][l] = 0;
					}
				}
			}
		}
		return a;
	}

	function createArray3D(dimension1, dimension2, dimension3) : number[][][] {
		var a = new Array(dimension1);
		for (var i = 0; i < dimension1; i++) {
			a[i] = new Array(dimension2);
			for (var j = 0; j < dimension2; j++) {
				a[i][j] = new Array(dimension3);
				for (var k = 0; k < dimension3; k++) {
					a[i][j][k] = 0;
				}
			}
		}
		return a;
	}

	function fillArray3D<T>(a : T[][][], dimension1, dimension2, dimension3, value : T) : void {
		for (var i = 0; i < dimension1; i++) {
			a[i] = [];
			for (var j = 0; j < dimension2; j++) {
				a[i][j] = [];
				for (var k = 0; k < dimension3; k++) {
					a[i][j][k] = value;
				}
			}
		}
	}

	function fillArray1D<T>(a : T[], dimension1, value : T) : void {
		for (var i = 0; i < dimension1; i++) {
			a[i] = value;
		}
	}

	export class WuColorCube {
		public redMinimum;
		public redMaximum;
		public greenMinimum;
		public greenMaximum;
		public blueMinimum;
		public blueMaximum;
		public volume;
		public alphaMinimum;
		public alphaMaximum;
	}

	export class WuQuant {

		private static alpha = 3;
		private static red = 2;
		private static green = 1;
		private static blue = 0;

		private _reds : number[];
		private _greens : number[];
		private _blues : number[];
		private _alphas : number[];
		private _sums : number[];

		private _weights : number[][][][];
		private _momentsRed : number[][][][];
		private _momentsGreen : number[][][][];
		private _momentsBlue : number[][][][];
		private _momentsAlpha : number[][][][];
		private _moments : number[][][][];

		private _table : number[];
		private _pixels : Utils.Point[];

		private _cubes : WuColorCube[];
		private _colors : number;

		private _significantBitsPerChannel: number;
		private _maxSideIndex: number;
		private _alphaMaxSideIndex: number;
		private _sideSize: number;
		private _alphaSideSize: number;

		private _distance : Distance.IDistanceCalculator;

		constructor(colorDistanceCalculator : Distance.IDistanceCalculator, colors : number = 256, significantBitsPerChannel : number = 5) {
			this._distance = colorDistanceCalculator;
			this._setQuality(significantBitsPerChannel);
			this._initialize(colors);
		}

		public sample(image : Utils.PointContainer) : void {
			var pointArray = image.getPointArray();

			for (var i = 0, l = pointArray.length; i < l; i++) {
				this._addColor(pointArray[i]);
			}

			this._pixels = this._pixels.concat(pointArray);
		}

		public quantize() : Utils.Palette {
			this._preparePalette();

			var palette : Utils.Palette = new Utils.Palette();

			// generates palette
			for (var paletteIndex = 0; paletteIndex < this._colors; paletteIndex++) {
				if (this._sums[paletteIndex] > 0) {
					var sum = this._sums[paletteIndex],
						r = this._reds[paletteIndex] / sum,
						g = this._greens[paletteIndex] / sum,
						b = this._blues[paletteIndex] / sum,
						a = this._alphas[paletteIndex] / sum;

					var color = Utils.Point.createByRGBA(r | 0, g | 0, b | 0, a | 0);
					palette.add(color);
				}
			}

			palette.sort();
			return palette;
		}

		private _preparePalette() : void {
			var l : number;
			// preprocess the colors
			this._calculateMoments();

			var next           = 0,
				volumeVariance = createArray1D(this._colors);

			// processes the cubes
			for (var cubeIndex = 1; cubeIndex < this._colors; ++cubeIndex) {
				// if cut is possible; make it
				if (this._cut(this._cubes[next], this._cubes[cubeIndex])) {
					volumeVariance[next] = this._cubes[next].volume > 1 ? this._calculateVariance(this._cubes[next]) : 0.0;
					volumeVariance[cubeIndex] = this._cubes[cubeIndex].volume > 1 ? this._calculateVariance(this._cubes[cubeIndex]) : 0.0;
				} else {
					// the cut was not possible, revert the index
					volumeVariance[next] = 0.0;
					cubeIndex--;
				}

				next = 0;
				var temp = volumeVariance[0];

				for (var index = 1; index <= cubeIndex; ++index) {
					if (volumeVariance[index] > temp) {
						temp = volumeVariance[index];
						next = index;
					}
				}

				if (temp <= 0.0) {
					this._colors = cubeIndex + 1;
					break;
				}
			}

			var lookupRed   = [],
				lookupGreen = [],
				lookupBlue  = [],
				lookupAlpha = [];

			// precalculates lookup tables
			for (var k = 0; k < this._colors; ++k) {

				var weight = WuQuant._volume(this._cubes[k], this._weights);

				if (weight > 0) {
					lookupRed[k] = (WuQuant._volume(this._cubes[k], this._momentsRed) / weight) | 0;
					lookupGreen[k] = (WuQuant._volume(this._cubes[k], this._momentsGreen) / weight) | 0;
					lookupBlue[k] = (WuQuant._volume(this._cubes[k], this._momentsBlue) / weight) | 0;
					lookupAlpha[k] = (WuQuant._volume(this._cubes[k], this._momentsAlpha) / weight) | 0;
				} else {
					lookupRed[k] = 0;
					lookupGreen[k] = 0;
					lookupBlue[k] = 0;
					lookupAlpha[k] = 0;
				}
			}

			this._reds = createArray1D(this._colors + 1);
			this._greens = createArray1D(this._colors + 1);
			this._blues = createArray1D(this._colors + 1);
			this._alphas = createArray1D(this._colors + 1);
			this._sums = createArray1D(this._colors + 1);

			// scans and adds colors
			for (var index = 0, l = this._pixels.length; index < l; index++) {
				var color : Utils.Point = this._pixels[index];

				var match        = -1,
					bestMatch    = match,
					bestDistance = Number.MAX_VALUE;

				for (var lookup = 0; lookup < this._colors; lookup++) {
					var foundRed   = lookupRed[lookup],
						foundGreen = lookupGreen[lookup],
						foundBlue  = lookupBlue[lookup],
						foundAlpha = lookupAlpha[lookup];

					var distance = this._distance.calculateRaw(foundRed, foundGreen, foundBlue, foundAlpha, color.r, color.g, color.b, color.a);
					//var distance = this._distance.calculateRaw(Utils.Point.createByRGBA(foundRed, foundGreen, foundBlue, foundAlpha), color);
						//deltaRed   = color.r - foundRed,
						//deltaGreen = color.g - foundGreen,
						//deltaBlue  = color.b - foundBlue,
						//deltaAlpha = color.a - foundAlpha,

						//distance   = deltaRed * deltaRed + deltaGreen * deltaGreen + deltaBlue * deltaBlue + deltaAlpha * deltaAlpha;

					if (distance < bestDistance) {
						bestDistance = distance;
						bestMatch = lookup;
					}
				}

				this._reds[bestMatch] += color.r;
				this._greens[bestMatch] += color.g;
				this._blues[bestMatch] += color.b;
				this._alphas[bestMatch] += color.a;
				this._sums[bestMatch]++;
			}
		}

		private _addColor(color : Utils.Point) : void {
			var bitsToRemove = 8 - this._significantBitsPerChannel,
				indexRed     = (color.r >> bitsToRemove) + 1,
				indexGreen   = (color.g >> bitsToRemove) + 1,
				indexBlue    = (color.b >> bitsToRemove) + 1,
				indexAlpha   = (color.a >> bitsToRemove) + 1;

			//if(color.a > 10) {
			this._weights[indexAlpha][indexRed][indexGreen][indexBlue]++;
			this._momentsRed[indexAlpha][indexRed][indexGreen][indexBlue] += color.r;
			this._momentsGreen[indexAlpha][indexRed][indexGreen][indexBlue] += color.g;
			this._momentsBlue[indexAlpha][indexRed][indexGreen][indexBlue] += color.b;
			this._momentsAlpha[indexAlpha][indexRed][indexGreen][indexBlue] += color.a;
			this._moments[indexAlpha][indexRed][indexGreen][indexBlue] += this._table[color.r] + this._table[color.g] + this._table[color.b] + this._table[color.a];
//			}
		}

		/**
		 * Converts the histogram to a series of _moments.
		 */
		private _calculateMoments() : void {
			var area : number[]      = [],
				areaRed : number[]   = [],
				areaGreen : number[] = [],
				areaBlue : number[]  = [],
				areaAlpha : number[] = [],
				area2 : number[]     = [];

			var xarea : number[][][]      = createArray3D(this._sideSize, this._sideSize, this._sideSize),
				xareaRed : number[][][]   = createArray3D(this._sideSize, this._sideSize, this._sideSize),
				xareaGreen : number[][][] = createArray3D(this._sideSize, this._sideSize, this._sideSize),
				xareaBlue : number[][][]  = createArray3D(this._sideSize, this._sideSize, this._sideSize),
				xareaAlpha : number[][][] = createArray3D(this._sideSize, this._sideSize, this._sideSize),
				xarea2 : number[][][]     = createArray3D(this._sideSize, this._sideSize, this._sideSize);

			for (var alphaIndex = 1; alphaIndex <= this._alphaMaxSideIndex; ++alphaIndex) {
				fillArray3D<number>(xarea, this._sideSize, this._sideSize, this._sideSize, 0);
				fillArray3D<number>(xareaRed, this._sideSize, this._sideSize, this._sideSize, 0);
				fillArray3D<number>(xareaGreen, this._sideSize, this._sideSize, this._sideSize, 0);
				fillArray3D<number>(xareaBlue, this._sideSize, this._sideSize, this._sideSize, 0);
				fillArray3D<number>(xareaAlpha, this._sideSize, this._sideSize, this._sideSize, 0);
				fillArray3D<number>(xarea2, this._sideSize, this._sideSize, this._sideSize, 0);

				for (var redIndex = 1; redIndex <= this._maxSideIndex; ++redIndex) {
					fillArray1D<number>(area, this._sideSize, 0);
					fillArray1D<number>(areaRed, this._sideSize, 0);
					fillArray1D<number>(areaGreen, this._sideSize, 0);
					fillArray1D<number>(areaBlue, this._sideSize, 0);
					fillArray1D<number>(areaAlpha, this._sideSize, 0);
					fillArray1D<number>(area2, this._sideSize, 0);

					for (var greenIndex = 1; greenIndex <= this._maxSideIndex; ++greenIndex) {
						var line      = 0,
							lineRed   = 0,
							lineGreen = 0,
							lineBlue  = 0,
							lineAlpha = 0,
							line2     = 0.0;

						for (var blueIndex = 1; blueIndex <= this._maxSideIndex; ++blueIndex) {
							line += this._weights[alphaIndex][redIndex][greenIndex][blueIndex];
							lineRed += this._momentsRed[alphaIndex][redIndex][greenIndex][blueIndex];
							lineGreen += this._momentsGreen[alphaIndex][redIndex][greenIndex][blueIndex];
							lineBlue += this._momentsBlue[alphaIndex][redIndex][greenIndex][blueIndex];
							lineAlpha += this._momentsAlpha[alphaIndex][redIndex][greenIndex][blueIndex];
							line2 += this._moments[alphaIndex][redIndex][greenIndex][blueIndex];

							area[blueIndex] += line;
							areaRed[blueIndex] += lineRed;
							areaGreen[blueIndex] += lineGreen;
							areaBlue[blueIndex] += lineBlue;
							areaAlpha[blueIndex] += lineAlpha;
							area2[blueIndex] += line2;

							xarea[redIndex][greenIndex][blueIndex] = xarea[redIndex - 1][greenIndex][blueIndex] + area[blueIndex];
							xareaRed[redIndex][greenIndex][blueIndex] = xareaRed[redIndex - 1][greenIndex][blueIndex] + areaRed[blueIndex];
							xareaGreen[redIndex][greenIndex][blueIndex] = xareaGreen[redIndex - 1][greenIndex][blueIndex] + areaGreen[blueIndex];
							xareaBlue[redIndex][greenIndex][blueIndex] = xareaBlue[redIndex - 1][greenIndex][blueIndex] + areaBlue[blueIndex];
							xareaAlpha[redIndex][greenIndex][blueIndex] = xareaAlpha[redIndex - 1][greenIndex][blueIndex] + areaAlpha[blueIndex];
							xarea2[redIndex][greenIndex][blueIndex] = xarea2[redIndex - 1][greenIndex][blueIndex] + area2[blueIndex];

							this._weights[alphaIndex][redIndex][greenIndex][blueIndex] = this._weights[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xarea[redIndex][greenIndex][blueIndex];
							this._momentsRed[alphaIndex][redIndex][greenIndex][blueIndex] = this._momentsRed[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xareaRed[redIndex][greenIndex][blueIndex];
							this._momentsGreen[alphaIndex][redIndex][greenIndex][blueIndex] = this._momentsGreen[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xareaGreen[redIndex][greenIndex][blueIndex];
							this._momentsBlue[alphaIndex][redIndex][greenIndex][blueIndex] = this._momentsBlue[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xareaBlue[redIndex][greenIndex][blueIndex];
							this._momentsAlpha[alphaIndex][redIndex][greenIndex][blueIndex] = this._momentsAlpha[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xareaAlpha[redIndex][greenIndex][blueIndex];
							this._moments[alphaIndex][redIndex][greenIndex][blueIndex] = this._moments[alphaIndex - 1][redIndex][greenIndex][blueIndex] + xarea2[redIndex][greenIndex][blueIndex];
						}
					}
				}
			}
		}

		/**
		 * Computes the volume of the cube in a specific moment.
		 */
		private static _volumeFloat(cube : WuColorCube, moment : number[][][][]) : number {
			return (moment[cube.alphaMaximum][cube.redMaximum][cube.greenMaximum][cube.blueMaximum] -
				moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] -
				moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] +
				moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum] -
				moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][cube.blueMaximum] +
				moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] +
				moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] -
				moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum]) -

				(moment[cube.alphaMaximum][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] -
				moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] -
				moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] +
				moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] -
				moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] +
				moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] +
				moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum] -
				moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);
		}

		/**
		 * Computes the volume of the cube in a specific moment.
		 */
		private static _volume(cube : WuColorCube, moment : number[][][][]) : number {
			return WuQuant._volumeFloat(cube, moment) | 0;
		}

		/**
		 * Splits the cube in given position][and color direction.
		 */
		private static _top(cube : WuColorCube, direction : number, position : number, moment : number[][][][]) : number {
			var result;
			switch (direction) {
				case WuQuant.alpha:
					result = (moment[position][cube.redMaximum][cube.greenMaximum][cube.blueMaximum] -
						moment[position][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] -
						moment[position][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] +
						moment[position][cube.redMinimum][cube.greenMinimum][cube.blueMaximum]) -
						(moment[position][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] -
						moment[position][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] -
						moment[position][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] +
						moment[position][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);
					break;

				case WuQuant.red:
					result = (moment[cube.alphaMaximum][position][cube.greenMaximum][cube.blueMaximum] -
						moment[cube.alphaMaximum][position][cube.greenMinimum][cube.blueMaximum] -
						moment[cube.alphaMinimum][position][cube.greenMaximum][cube.blueMaximum] +
						moment[cube.alphaMinimum][position][cube.greenMinimum][cube.blueMaximum]) -
						(moment[cube.alphaMaximum][position][cube.greenMaximum][cube.blueMinimum] -
						moment[cube.alphaMaximum][position][cube.greenMinimum][cube.blueMinimum] -
						moment[cube.alphaMinimum][position][cube.greenMaximum][cube.blueMinimum] +
						moment[cube.alphaMinimum][position][cube.greenMinimum][cube.blueMinimum]);
					break;

				case WuQuant.green:
					result = (moment[cube.alphaMaximum][cube.redMaximum][position][cube.blueMaximum] -
						moment[cube.alphaMaximum][cube.redMinimum][position][cube.blueMaximum] -
						moment[cube.alphaMinimum][cube.redMaximum][position][cube.blueMaximum] +
						moment[cube.alphaMinimum][cube.redMinimum][position][cube.blueMaximum]) -
						(moment[cube.alphaMaximum][cube.redMaximum][position][cube.blueMinimum] -
						moment[cube.alphaMaximum][cube.redMinimum][position][cube.blueMinimum] -
						moment[cube.alphaMinimum][cube.redMaximum][position][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMinimum][position][cube.blueMinimum]);
					break;

				case WuQuant.blue:
					result =  (moment[cube.alphaMaximum][cube.redMaximum][cube.greenMaximum][position] -
						moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][position] -
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][position] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][position]) -
						(moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][position] -
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][position] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][position] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][position]);
					break;
			}

			return result | 0;
		}

		/**
		 * Splits the cube in a given color direction at its minimum.
		 */
		private static _bottom(cube : WuColorCube, direction : number, moment : number[][][][]) : number {
			switch (direction) {
				case WuQuant.alpha:
					return (-moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][cube.blueMaximum] +
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum]) -
						(-moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);

				case WuQuant.red:
					return (-moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMaximum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum]) -
						(-moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);

				case WuQuant.green:
					return (-moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum] +
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMaximum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMaximum]) -
						(-moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);

				case WuQuant.blue:
					return (-moment[cube.alphaMaximum][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] +
						moment[cube.alphaMaximum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] -
						moment[cube.alphaMaximum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]) -
						(-moment[cube.alphaMinimum][cube.redMaximum][cube.greenMaximum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMaximum][cube.greenMinimum][cube.blueMinimum] +
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMaximum][cube.blueMinimum] -
						moment[cube.alphaMinimum][cube.redMinimum][cube.greenMinimum][cube.blueMinimum]);

				default:
					return 0;
			}
		}

		/**
		 * Calculates statistical variance for a given cube.
		 */
		private  _calculateVariance(cube : WuColorCube) : number {
			var volumeRed    = WuQuant._volume(cube, this._momentsRed),
				volumeGreen  = WuQuant._volume(cube, this._momentsGreen),
				volumeBlue   = WuQuant._volume(cube, this._momentsBlue),
				volumeAlpha  = WuQuant._volume(cube, this._momentsAlpha),
				volumeMoment = WuQuant._volumeFloat(cube, this._moments),
				volumeWeight = WuQuant._volume(cube, this._weights),

				distance     = volumeRed * volumeRed + volumeGreen * volumeGreen + volumeBlue * volumeBlue + volumeAlpha * volumeAlpha;

			return volumeMoment - (distance / volumeWeight);
		}

		/**
		 * Finds the optimal (maximal) position for the cut.
		 */
		private _maximize(cube : WuColorCube, direction : number, first : number, last : number, wholeRed : number, wholeGreen : number, wholeBlue : number, wholeAlpha : number, wholeWeight : number) : {max : number; position : number} {
			var bottomRed    = WuQuant._bottom(cube, direction, this._momentsRed) | 0,
				bottomGreen  = WuQuant._bottom(cube, direction, this._momentsGreen) | 0,
				bottomBlue   = WuQuant._bottom(cube, direction, this._momentsBlue) | 0,
				bottomAlpha  = WuQuant._bottom(cube, direction, this._momentsAlpha) | 0,
				bottomWeight = WuQuant._bottom(cube, direction, this._weights) | 0,

				result       = 0.0,
				cutPosition  = -1;

			for (var position = first; position < last; ++position) {
				// determines the cube cut at a certain position
				var halfRed    = bottomRed + WuQuant._top(cube, direction, position, this._momentsRed),
					halfGreen  = bottomGreen + WuQuant._top(cube, direction, position, this._momentsGreen),
					halfBlue   = bottomBlue + WuQuant._top(cube, direction, position, this._momentsBlue),
					halfAlpha  = bottomAlpha + WuQuant._top(cube, direction, position, this._momentsAlpha),
					halfWeight = bottomWeight + WuQuant._top(cube, direction, position, this._weights);

				// the cube cannot be cut at bottom (this would lead to empty cube)
				if (halfWeight != 0) {
					var halfDistance = halfRed * halfRed + halfGreen * halfGreen + halfBlue * halfBlue + halfAlpha * halfAlpha,
						temp         = halfDistance / halfWeight;

					halfRed = wholeRed - halfRed;
					halfGreen = wholeGreen - halfGreen;
					halfBlue = wholeBlue - halfBlue;
					halfAlpha = wholeAlpha - halfAlpha;
					halfWeight = wholeWeight - halfWeight;

					if (halfWeight != 0) {
						halfDistance = halfRed * halfRed + halfGreen * halfGreen + halfBlue * halfBlue + halfAlpha * halfAlpha;
						temp += halfDistance / halfWeight;

						if (temp > result) {
							result = temp;
							cutPosition = position;
						}
					}
				}
			}

			return {max : result, position : cutPosition};
		}

		// Cuts a cube with another one.
		private _cut(first : WuColorCube, second : WuColorCube) : boolean {
			var direction,

				wholeRed    = WuQuant._volume(first, this._momentsRed),
				wholeGreen  = WuQuant._volume(first, this._momentsGreen),
				wholeBlue   = WuQuant._volume(first, this._momentsBlue),
				wholeAlpha  = WuQuant._volume(first, this._momentsAlpha),
				wholeWeight = WuQuant._volume(first, this._weights),

				red         = this._maximize(first, WuQuant.red, first.redMinimum + 1, first.redMaximum, wholeRed, wholeGreen, wholeBlue, wholeAlpha, wholeWeight),
				green       = this._maximize(first, WuQuant.green, first.greenMinimum + 1, first.greenMaximum, wholeRed, wholeGreen, wholeBlue, wholeAlpha, wholeWeight),
				blue        = this._maximize(first, WuQuant.blue, first.blueMinimum + 1, first.blueMaximum, wholeRed, wholeGreen, wholeBlue, wholeAlpha, wholeWeight),
				alpha       = this._maximize(first, WuQuant.alpha, first.alphaMinimum + 1, first.alphaMaximum, wholeRed, wholeGreen, wholeBlue, wholeAlpha, wholeWeight);

			if (alpha.max >= red.max && alpha.max >= green.max && alpha.max >= blue.max) {
				direction = WuQuant.alpha;

				// cannot split empty cube
				if (alpha.position < 0) return false;
			} else {
				if (red.max >= alpha.max && red.max >= green.max && red.max >= blue.max) {
					direction = WuQuant.red;
				} else if (green.max >= alpha.max && green.max >= red.max && green.max >= blue.max) {
					direction = WuQuant.green;
				} else {
					direction = WuQuant.blue;
				}
			}

			second.redMaximum = first.redMaximum;
			second.greenMaximum = first.greenMaximum;
			second.blueMaximum = first.blueMaximum;
			second.alphaMaximum = first.alphaMaximum;

			// cuts in a certain direction
			switch (direction) {
				case WuQuant.red:
					second.redMinimum = first.redMaximum = red.position;
					second.greenMinimum = first.greenMinimum;
					second.blueMinimum = first.blueMinimum;
					second.alphaMinimum = first.alphaMinimum;
					break;

				case WuQuant.green:
					second.greenMinimum = first.greenMaximum = green.position;
					second.redMinimum = first.redMinimum;
					second.blueMinimum = first.blueMinimum;
					second.alphaMinimum = first.alphaMinimum;
					break;

				case WuQuant.blue:
					second.blueMinimum = first.blueMaximum = blue.position;
					second.redMinimum = first.redMinimum;
					second.greenMinimum = first.greenMinimum;
					second.alphaMinimum = first.alphaMinimum;
					break;

				case WuQuant.alpha:
					second.alphaMinimum = first.alphaMaximum = alpha.position;
					second.blueMinimum = first.blueMinimum;
					second.redMinimum = first.redMinimum;
					second.greenMinimum = first.greenMinimum;
					break;
			}

			// determines the volumes after cut
			first.volume = (first.redMaximum - first.redMinimum) * (first.greenMaximum - first.greenMinimum) * (first.blueMaximum - first.blueMinimum) * (first.alphaMaximum - first.alphaMinimum);
			second.volume = (second.redMaximum - second.redMinimum) * (second.greenMaximum - second.greenMinimum) * (second.blueMaximum - second.blueMinimum) * (second.alphaMaximum - second.alphaMinimum);

			// the cut was successful
			return true;
		}

		private _initialize(colors : number) : void {
			this._colors = colors;

			// creates all the _cubes
			this._cubes = [];

			// initializes all the _cubes
			for (var cubeIndex = 0; cubeIndex < colors; cubeIndex++) {
				this._cubes[cubeIndex] = new WuColorCube();
			}

			// resets the reference minimums
			this._cubes[0].redMinimum = 0;
			this._cubes[0].greenMinimum = 0;
			this._cubes[0].blueMinimum = 0;
			this._cubes[0].alphaMinimum = 0;

			// resets the reference maximums
			this._cubes[0].redMaximum = this._maxSideIndex;
			this._cubes[0].greenMaximum = this._maxSideIndex;
			this._cubes[0].blueMaximum = this._maxSideIndex;
			this._cubes[0].alphaMaximum = this._alphaMaxSideIndex;

			this._weights = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);
			this._momentsRed = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);
			this._momentsGreen = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);
			this._momentsBlue = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);
			this._momentsAlpha = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);
			this._moments = createArray4D(this._alphaSideSize, this._sideSize, this._sideSize, this._sideSize);

			this._table = [];
			for (var tableIndex = 0; tableIndex < 256; ++tableIndex) {
				this._table[tableIndex] = tableIndex * tableIndex;
			}

			this._pixels = [];
		}

		private _setQuality(significantBitsPerChannel : number = 5) : void {
			this._significantBitsPerChannel = significantBitsPerChannel;
			this._maxSideIndex = 1 << this._significantBitsPerChannel;
			this._alphaMaxSideIndex = this._maxSideIndex;

			this._sideSize = this._maxSideIndex + 1;
			this._alphaSideSize = this._alphaMaxSideIndex + 1;
		}

	}
}
