package net.returnvoid.graphics.grid;

import net.returnvoid.tools.RMath;
import processing.core.PVector;

/*
 * This code is copyright (c) Diana Lange 2017
 *
 * The library is published under the Creative Commons license NonCommercial 4.0.
 * Please check https://creativecommons.org/licenses/by-nc/4.0/ for more information.
 * 
 * This program is distributed in the hope that it will be useful, but without any warranty.
 */

/**
 * This class helps to build an table grid (a grid that is defined by rows and
 * columns). The elements will be distributed evenly.
 * 
 * @author Diana Lange
 *
 */
public class TableGrid implements RGrid {

	/**
	 * The x-location of the grid (top-left corner).
	 */
	private Float x;

	/**
	 * The y-location of the grid (top-left corner).
	 */
	private Float y;

	/**
	 * The width of the grid.
	 */
	private Float w;

	/**
	 * The height of the grid.
	 */
	private Float h;

	/**
	 * The horizontal padding of the elements of the grid.
	 */
	private Float paddingX;

	/**
	 * The vertical padding of the elements of the grid.
	 */
	private Float paddingY;

	/**
	 * The number of columns of the grid.
	 */
	private Integer columns;

	/**
	 * The number of rows of the grid.
	 */
	private Integer rows;

	/**
	 * Keeps track if some value has been changed. If any value has been
	 * changed, the coordinates will be re-calculated.
	 */
	private boolean wasUpdated;

	/**
	 * An array with all the coordinates of each field in the grid.
	 */
	private PVector[] coordinates;

	/**
	 * Builds a new grid. Please provide reasonable value for each parameter or
	 * use the GridMaker.
	 * 
	 * @param x
	 *            The x-location of the grid (top-left corner).
	 * @param y
	 *            The y-location of the grid (top-left corner).
	 * @param w
	 *            The width of the grid.
	 * @param h
	 *            The height of the grid.
	 * @param columns
	 *            The number of columns of the grid.
	 * @param rows
	 *            The number of rows of the grid.
	 * @param paddingX
	 *            The horizontal padding of the elements of the grid (>= 0).
	 * @param paddingY
	 *            The vertical padding of the elements of the grid (>= 0).
	 */
	public TableGrid(float x, float y, float w, float h, int columns, int rows, float paddingX, float paddingY) {
		this.x = x;
		this.y = y;
		this.w = w < 0 ? w * -1 : w < columns ? columns : w;
		this.h = h < 0 ? h * -1 : h < rows ? rows : h;
		this.columns = columns;
		this.rows = rows;
		this.paddingX = paddingX;
		this.paddingY = paddingY;
		this.wasUpdated = true;
	}

	// setters

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#setX(float)
	 */
	@Override
	public TableGrid setX(float x) {
		this.wasUpdated = true;
		this.x = x;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#setY(float)
	 */
	@Override
	public TableGrid setY(float y) {
		this.wasUpdated = true;
		this.y = y;
		return this;
	}

	/**
	 * Sets the width of this grid.
	 * 
	 * @param w
	 *            The new grid width.
	 * @return The current grid object.
	 */
	public TableGrid setWidth(float w) {

		this.w = w < columns ? columns : w;

		return this;
	}

	/**
	 * Sets the height of this grid.
	 * 
	 * @param h
	 *            The new grid height.
	 * @return The current grid object.
	 */
	public TableGrid setHeight(float h) {


		this.h = h < rows ? rows : h;

		return this;
	}

	/**
	 * Sets the width and height of this grid (width and height will have the
	 * same value).
	 * 
	 * @param s
	 *            The new grid dimension (width and height).
	 * @return The current grid object.
	 */
	public TableGrid setDimension(float s) {
		return setWidth(s).setHeight(s);
	}

	/**
	 * Sets the width and height of this grid.
	 * 
	 * @param w
	 *            The new grid width.
	 * @param h
	 *            The new grid height.
	 * @return The current grid object.
	 */
	public TableGrid setDimension(float w, float h) {
		return setWidth(w).setHeight(h);
	}

	/**
	 * Sets the width of the grid elements. The maximum width for the elements
	 * is getWidth() / columns(). The horizontal padding will be changed after
	 * this method call.
	 * 
	 * @param cellWidth
	 *            The new width of the cells.
	 * @return The current grid object.
	 */
	public TableGrid setCellWidth(float cellWidth) {
		float maxW = w / columns;
		float newW = cellWidth < 1 ? 1 : cellWidth > maxW ? maxW : cellWidth;
		float newPaddingX = 1 - newW / (w / columns);

		return this.setPaddingX(newPaddingX);
	}

	/**
	 * Sets the height of the grid elements. The maximum height for the elements
	 * is getHeight() / rows(). The vertical padding will be changed after this
	 * method call.
	 * 
	 * @param cellHeight
	 *            The new width of the cells.
	 * @return The current grid object.
	 */
	public TableGrid setCellHeight(float cellHeight) {
		float maxH = h / rows;
		float newH = cellHeight < 1 ? 1 : cellHeight > maxH ? maxH : cellHeight;
		float newPaddingY = 1 - newH / (h / rows);

		return this.setPaddingY(newPaddingY);
	}

	/**
	 * Sets the horizontal and vertical padding. The paddings will be equal to
	 * the width / height of the cells.
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPadding() {
		return setPaddingX().setPaddingY();
	}

	/**
	 * Sets the horizontal padding. The padding will be equal to the width of
	 * the cells.
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPaddingX() {
		return setPaddingX(0.5f);
	}

	/**
	 * Sets the vertical padding. The paddings will be equal to the height of
	 * the cells.
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPaddingY() {
		return setPaddingY(0.5f);
	}

	/**
	 * Sets the horizontal and vertical padding.
	 * 
	 * @param padding
	 *            The new (horizontal / vertical) padding of this grid (>= 0).
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPadding(float padding) {
		return setPaddingX(padding).setPaddingY(padding);
	}

	/**
	 * Sets the horizontal padding.
	 * 
	 * @param paddingX
	 *            The new horizontal padding of this grid (>= 0).
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPaddingX(float paddingX) {
		this.paddingX = paddingX;
		return this;
	}

	/**
	 * Sets the vertical padding.
	 * 
	 * @param paddingY
	 *            The new vertical padding of this grid (>= 0).
	 * 
	 * @return The current grid object.
	 */
	public TableGrid setPaddingY(float paddingY) {

		this.paddingY = paddingY;
		return this;
	}

	/**
	 * Sets the number of elements for this grid.
	 * 
	 * @param size
	 *            The new number of elements for this grid (>= 1).
	 * @return The current grid object.
	 */
	public TableGrid setSize(Integer size) {
		size = RMath.abs(size);

		float aspectRatio = w / h;

		int numX = (int) (Math.pow(size * aspectRatio, 0.5));
		int numY = (int) (size / (float) numX);

		if (numX < 1) {
			numX = 1;
		}

		if (numY < 1) {
			numY = 1;
		}

		this.rows = numY;
		this.columns = numX;
		this.wasUpdated = true;

		return this;
	}
	
	/**
	 * Sets the number of columns for this grid.
	 * 
	 * @param columns
	 *            The new number of columns for this grid (>= 1).
	 * @return The current grid object.
	 */
	public TableGrid setColumns(Integer columns) {
		columns = RMath.abs(columns);
		
		if (columns < 1) {
			columns = 1;
		}

		this.columns = columns;
		this.wasUpdated = true;

		return this;
	}
	
	/**
	 * Sets the number of rows for this grid.
	 * 
	 * @param rows
	 *            The new number of rows for this grid (>= 1).
	 * @return The current grid object.
	 */
	public TableGrid setRows(Integer rows) {
		rows = RMath.abs(rows);
		
		if (rows < 1) {
			rows = 1;
		}
		
		this.rows = rows;
		this.wasUpdated = true;

		return this;
	}

	/**
	 * Calculate the coordinates (just do it, when the coordinates haven't been
	 * calculated yet or wasUpdated is true).
	 */
	private void initCoordinates() {
		coordinates = new PVector[size()];

		for (int i = 0; i < coordinates.length; i++) {
			coordinates[i] = new PVector(getX(i), getY(i));
		}
	}

	// getters

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getCoordinates()
	 */
	@Override
	public PVector[] getCoordinates() {
		if (wasUpdated) {
			initCoordinates();
		}

		return coordinates;
	}

	/**
	 * Returns the number of columns of this grid.
	 * 
	 * @return The number of columns.
	 */
	public Integer columns() {
		return columns;
	}

	/**
	 * Returns the number of rows of this grid.
	 * 
	 * @return The number of rows.
	 */
	public Integer rows() {
		return rows;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#size()
	 */
	@Override
	public Integer size() {
		return columns * rows;
	}

	/**
	 * Returns the horizontal padding of this grid.
	 * 
	 * @return The horizontal padding.
	 */
	public Float getPaddingX() {
		return paddingX;
	}

	/**
	 * Returns the vertical padding of this grid.
	 * 
	 * @return The vertical padding.
	 */
	public Float getPaddingY() {
		return paddingY;
	}
	
	/**
	 * Returns the space between the elements of the grid.
	 * 
	 * @return The padding.
	 */
	public Float getPadding() {
		return 0.5f * (this.getPaddingX() + this.getPaddingY());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getWidth()
	 */
	@Override
	public Float getWidth() {
		return w;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getHeight()
	 */
	@Override
	public Float getHeight() {
		return h;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getX()
	 */
	@Override
	public Float getX() {
		return x;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY()
	 */
	@Override
	public Float getY() {
		return y;
	}

	/**
	 * Returns the x-location of the row with a given row index.
	 * 
	 * @param rowIndex
	 *            The row index [0, rows()].
	 * @return The x-location of the row.
	 */
	public Float getRowX(Integer rowIndex) {
		return getX(index(rowIndex, 0));
	}

	/**
	 * Returns the y-location of the row with a given row index.
	 * 
	 * @param rowIndex
	 *            The row index [0, rows()].
	 * @return The y-location of the row.
	 */
	public Float getRowY(Integer rowIndex) {
		return getY(index(rowIndex, 0));
	}

	/**
	 * Returns the x-location of the column with a given column index.
	 * 
	 * @param columnIndex
	 *            The columnIndex index [0, columns()].
	 * @return The x-location of the row.
	 */
	public Float getColumnX(Integer columnIndex) {
		return getX(index(0, columnIndex));
	}

	/**
	 * Returns the y-location of the column with a given column index.
	 * 
	 * @param columnIndex
	 *            The columnIndex index [0, columns()].
	 * @return The y-location of the row.
	 */
	public Float getColumnY(Integer columnIndex) {
		return getY(index(0, columnIndex));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getX(java.lang.Integer)
	 */
	@Override
	public Float getX(Integer index) {

		int j = index % columns;

		return x + (j + 0.5f) * w / columns;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY(java.lang.Integer)
	 */
	@Override
	public Float getY(Integer index) {

		int i = (index - index % columns) / columns;

		return y + (i + 0.5f) * h / rows;
	}

	/**
	 * Returns the row index [0, rows()] with a given element index.
	 * 
	 * @param index
	 *            The index of any element [0, size()].
	 * @return The row number of the given element.
	 */
	public Integer getRow(Integer index) {
		return (index - index % columns) / columns;
	}

	/**
	 * Returns the column index [0, rows()] with a given element index.
	 * 
	 * @param index
	 *            The index of any element [0, size()].
	 * @return The column number of the given element.
	 */
	public Integer getColumn(int index) {
		return index % columns;
	}

	/**
	 * Returns if the element with the given index is member of an even row.
	 * 
	 * @param index
	 *            The index of the element.
	 * @return True, if the element is member of an even row.
	 */
	public boolean isEvenRow(int index) {
		return getRow(index) % 2 == 0;
	}

	/**
	 * Returns if the element with the given index is member of an even column.
	 * 
	 * @param index
	 *            The index of the element.
	 * @return True, if the element is member of an even column.
	 */
	public boolean isEvenColumn(int index) {
		return getColumn(index) % 2 == 0;
	}

	/**
	 * Returns the element index with given column and row index.
	 * 
	 * @param row
	 *            The row index [0, rows()].
	 * @param column
	 *            The column index [0, columns()].
	 * @return The index [0, size()].
	 */
	public Integer index(Integer row, Integer column) {
		return row * columns + column;
	}

	/**
	 * Returns the width of any element in this grid.
	 * 
	 * @return The width of the element.
	 */
	public Float cellWidth() {
		return (1 - paddingX) * w / columns;
	}

	/**
	 * Returns the height of any element in this grid.
	 * 
	 * @return The height of the element.
	 */
	public Float cellHeight() {
		return (1 - paddingY) * h / rows;
	}

	/* (non-Javadoc)
	 * @see net.returnvoid.graphics.grid.RGrid#getWidth(java.lang.Integer)
	 */
	@Override
	public Float getWidth(Integer i) {
		return this.cellWidth();
	}

	/* (non-Javadoc)
	 * @see net.returnvoid.graphics.grid.RGrid#getHeight(java.lang.Integer)
	 */
	@Override
	public Float getHeight(Integer i) {
		return this.cellHeight();
	}

}
