package extractors.hog;

import Jama.Matrix;

import java.util.Arrays;

/**
 * This class represents a cell of gradients.
 * 
 */
public class Cell {

	// the histogram of the cell
	private double[][] histogram;

	// parameters concerning the histogram computation
	private static int phiRange, phiBins, thetaRange, thetaBins, phiBinSize, thetaBinSize;

	/**
	 * Constructs a new empty cell
	 */
	public Cell() {
		histogram = new double[phiBins][thetaBins];
	}

	/**
	 * Constructs a cell with the specified histogram
	 * 
	 * @param histogram
	 */
	public Cell(double[][] histogram) {
		this.histogram = histogram;
	}

	/**
	 * Inserts a gradient with the specified weight into the histogram of this cell without interpolation.
	 * This is mainly for test purposes.
	 * 
	 * @param gradient
	 * @param weight
	 */
	public void insert(Gradient gradient, double weight) {
		if (gradient != null) {
			double phi = gradient.getPhi();
			double theta = gradient.getTheta();

			// make sure orientations are positiv
			if (phi < 0) {
				phi += 180;
				theta = 180 - theta;
			}

			// select bin
			int phiBin = (int) (phi / phiBinSize);
			int thetaBin = (int) (theta / thetaBinSize);

			histogram[phiBin][thetaBin] += gradient.getMagnitude() * weight;
		} // else: ignore border
	}

	/**
	 * Inserts a gradient with the specified weight into the histogram of this cell with interpolation.
	 * 
	 * @param gradient
	 * @param weight
	 */
	public void insertInterpolated(Gradient gradient, double weight) {
		if (gradient != null) {
			// careful: if phirange==360 theta should not be interpolated between smallest and biggest bin

			double phi = gradient.getPhi();
			double theta = gradient.getTheta();

			// shift by half bin size to operate between centers and make sure orientations are positive
			if (phi < 0) {
				phi += phiRange;
				theta = thetaRange - theta;
			}
			phi = (phi - phiBinSize / 2.0 + phiRange) % phiRange;
			theta = (theta - thetaBinSize / 2.0 + thetaRange) % thetaRange;

			// select left bins
			int phiLeftBin = (int) (phi / phiBinSize);
			int thetaLeftBin = (int) (theta / thetaBinSize);

			// compute weights
			double phiRightWeight = phi % phiBinSize / phiBinSize;
			double thetaRightWeight = theta % thetaBinSize / thetaBinSize;
			double phiLeftWeight = 1 - phiRightWeight;
			double thetaLeftWeight = 1 - thetaRightWeight;

			double magnitude = gradient.getMagnitude() * weight;

			// interpolate bilinear
			histogram[phiLeftBin][thetaLeftBin] += magnitude * phiLeftWeight * thetaLeftWeight;
			histogram[phiLeftBin][(thetaLeftBin + 1) % thetaBins] += magnitude * phiLeftWeight * thetaRightWeight;
			histogram[(phiLeftBin + 1) % phiBins][thetaLeftBin] += magnitude * phiRightWeight * thetaLeftWeight;
			histogram[(phiLeftBin + 1) % phiBins][(thetaLeftBin + 1) % thetaBins] += magnitude * phiRightWeight
					* thetaRightWeight;
		} // else: ignore border
	}

	/**
	 * Sets the necessary parameters.
	 * 
	 * @param phiRange
	 * @param phiBins
	 * @param thetaRange
	 * @param thetaBins
	 */
	public static void setParameters(int phiRange, int phiBins, int thetaRange, int thetaBins) {
		Cell.phiRange = phiRange;
		Cell.phiBins = phiBins;
		Cell.thetaRange = thetaRange;
		Cell.thetaBins = thetaBins;

		phiBinSize = phiRange / phiBins;
		thetaBinSize = thetaRange / thetaBins;
	}

	/**
	 * Computes a cell grid as 3D {@link Cell} array from a given gradient field.
	 * 
	 * @param gradientField
	 * @param cellSizeX
	 * @param cellSizeY
	 * @param cellSizeZ
	 * @return
	 */
	public static Cell[][][] computeCellGrid(Gradient[][][] gradientField, int cellSizeX, int cellSizeY, int cellSizeZ) {
		int cellsX = gradientField.length / cellSizeX;
		int cellsY = gradientField[0].length / cellSizeY;
		int cellsZ = gradientField[0][0].length / cellSizeZ;

		Cell[][][] cellGrid = new Cell[cellsX][cellsY][cellsZ];

		for (int x = 0; x < cellGrid.length; x++) {
			for (int y = 0; y < cellGrid[x].length; y++) {
				for (int z = 0; z < cellGrid[x][y].length; z++) {
					cellGrid[x][y][z] = new Cell();

				}
			}
		}

		for (int x = 0; x < gradientField.length; x++) {
			// compute weight and index in x-dimension
			int xleft = getLeftIndex(x, cellSizeX, cellsX);
			double xRightWeight = getRightWeight(x, cellSizeX);
			double xLeftWeight = 1 - xRightWeight;

			for (int y = 0; y < gradientField[x].length; y++) {
				// compute weight and index in y-dimension
				int yleft = getLeftIndex(y, cellSizeY, cellsY);
				double yRightWeight = getRightWeight(y, cellSizeY);
				double yLeftWeight = 1 - yRightWeight;

				for (int z = 0; z < gradientField[x][y].length; z++) {
					// compute weight and index in z-dimension
					int zleft = getLeftIndex(z, cellSizeZ, cellsZ);
					double zRightWeight = getRightWeight(z, cellSizeZ);
					double zLeftWeight = 1 - zRightWeight;

					// trilinear interpolation in the 8 surrounding cells
					if (xleft > -1) {
						if (yleft > -1) {
							if (zleft > -1) {
								cellGrid[xleft][yleft][zleft].insertInterpolated(gradientField[x][y][z], xLeftWeight
										* yLeftWeight * zLeftWeight);
							}
							if (zleft + 1 < cellsZ) {
								cellGrid[xleft][yleft][zleft + 1].insertInterpolated(gradientField[x][y][z],
									xLeftWeight * yLeftWeight * zRightWeight);
							}
						}
						if (yleft + 1 < cellsY) {
							if (zleft > -1) {
								cellGrid[xleft][yleft + 1][zleft].insertInterpolated(gradientField[x][y][z],
									xLeftWeight * yRightWeight * zLeftWeight);
							}
							if (zleft + 1 < cellsZ) {
								cellGrid[xleft][yleft + 1][zleft + 1].insertInterpolated(gradientField[x][y][z],
									xLeftWeight * yRightWeight * zRightWeight);
							}
						}
					}

					if (xleft + 1 < cellsX) {
						if (yleft > -1) {
							if (zleft > -1) {
								cellGrid[xleft + 1][yleft][zleft].insertInterpolated(gradientField[x][y][z],
									xRightWeight * yLeftWeight * zLeftWeight);
							}
							if (zleft + 1 < cellsZ) {
								cellGrid[xleft + 1][yleft][zleft + 1].insertInterpolated(gradientField[x][y][z],
									xRightWeight * yLeftWeight * zRightWeight);
							}
						}
						if (yleft + 1 < cellsY) {
							if (zleft > -1) {
								cellGrid[xleft + 1][yleft + 1][zleft].insertInterpolated(gradientField[x][y][z],
									xRightWeight * yRightWeight * zLeftWeight);
							}
							if (zleft + 1 < cellsZ) {
								cellGrid[xleft + 1][yleft + 1][zleft + 1].insertInterpolated(gradientField[x][y][z],
									xRightWeight * yRightWeight * zRightWeight);
							}
						}
					}
				}
			}
		}

		return cellGrid;
	}

	/**
	 * Returns the right weight of the position specified by pos within the cell of size cellSize.
	 * 
	 * @param pos
	 * @param cellSize
	 * @return the right weight
	 */
	private static double getRightWeight(int pos, int cellSize) {
		int i = (pos + cellSize / 2) % cellSize;
		return (double) (2 * i + 1) / (2 * cellSize);
	}

	/**
	 * Returns the index of the left cell of the position specified by pos in the gradient field.
	 * 
	 * @param pos
	 * @param cellSize
	 * @param cells
	 * @return
	 */
	private static int getLeftIndex(int pos, int cellSize, int cells) {
		if (pos < cellSize / 2) {
			return -1;
		}
		if (pos >= cellSize * cells - cellSize / 2) {
			return cells - 1;
		}

		return (pos - cellSize / 2) / cellSize;

	}

	public static int getPhiBins() {
		return phiBins;
	}

	public static int getThetaBins() {
		return thetaBins;
	}

	// For test purposes.
	public void printHistogram() {
		Matrix m = new Matrix(histogram);
		m.print(2, 4);
	}

	/**
	 * Copies this cell and returns the copy. Mainly for test purposes.
	 * 
	 * @return
	 */
	public Cell copy() {
		double[][] h = new double[getHistogram().length][];
		for (int i = 0; i < h.length; i++) {
			h[i] = Arrays.copyOf(getHistogram()[i], getHistogram()[i].length);
		}
		return new Cell(h);
	}
	
	/**
	 * @return the histogram
	 */
	public double[][] getHistogram() {
		return histogram;
	}
}
