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 ellipsoid grid. The elements of this grid will
 * be located on an ellipse with a given radius. The elements will be
 * distributed evenly.
 * 
 * @author Diana Lange
 *
 */
public class EllipseGrid implements RGrid {

	/**
	 * The x-location of the center of the grid.
	 */
	private Float x;

	/**
	 * The y-location of the center of the grid.
	 */
	private Float y;

	/**
	 * The horizontal radius of the grid.
	 */
	private Float radiusX;

	/**
	 * The vertical radius of the grid.
	 */
	private Float radiusY;

	/**
	 * The angle where the grid starts. The startAngle defines where the first
	 * element will positioned.
	 */
	private Float startAngle;

	/**
	 * The number of elements / locations in this grid.
	 */
	private Integer size;

	/**
	 * 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;

	/**
	 * The padding of each element to its neighbors.
	 */
	private Float padding;

	/**
	 * Builds a new grid. Please provide reasonable value for each parameter or
	 * use the GridMaker.
	 * 
	 * @param x
	 *            The center x-location of this grid.
	 * @param y
	 *            The center y-location of this grid.
	 * @param radiusX
	 *            The vertical radius of this grid (2 * radiusX = width of this
	 *            grid).
	 * @param radiusY
	 *            The horizontal radius of this grid (2 * radiusY = height of
	 *            this grid)
	 * @param size
	 *            The number of elements if this grid.
	 * @param startAngle
	 *            The startAngle defines where the first element will positioned
	 *            (in radian).
	 */
	EllipseGrid(float x, float y, float radiusX, float radiusY, Integer size, float startAngle) {
		this.x = x;
		this.y = y;
		this.radiusX = radiusX;
		this.radiusY = radiusY;
		this.startAngle = startAngle;
		this.padding = 0f;
		this.size = RMath.abs(size);
		this.wasUpdated = true;
		
		if (this.size < 1) {
			size = 1;
		}
	}

	// setter

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

		return this;
	}

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

		return this;
	}

	/**
	 * Sets the radius for this grid. The width and height of the grid will be
	 * equal.
	 * 
	 * @param r
	 *            The new radius of this grid.
	 * @return The current grid object.
	 */
	public EllipseGrid setRadius(float r) {
		r = RMath.abs(r);
		

		if (r > 1) {
		
			this.radiusX = r;
			this.radiusY = r;
			this.wasUpdated = true;
			
		}
		

		return this;
	}

	/**
	 * Sets the horizontal radius for this grid.
	 * 
	 * @param r
	 *            The new horizontal radius of this grid.
	 * @return The current grid object.
	 */
	public EllipseGrid setRadiusX(float r) {
		r = RMath.abs(r);
		if (r > 1) {
			
			this.radiusX = r;
			this.wasUpdated = true;
			
		}

		return this;
	}

	/**
	 * Sets the vertical radius for this grid.
	 * 
	 * @param r
	 *            The new vertical radius of this grid.
	 * @return The current grid object.
	 */
	public EllipseGrid setRadiusY(float r) {
		r = RMath.abs(r);
		if (r > 1) {
			
			this.radiusY = r;
			this.wasUpdated = true;
			
		}

		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 EllipseGrid setSize(Integer size) {
		size = RMath.abs(size);
		
		this.size = size < 1 ? 1 : size;
		this.wasUpdated = true;

		return this;
	}

	/**
	 * Set the position of the first element of the grid.
	 * 
	 * @param startAngle
	 *            The angle which describes the position of the first element
	 *            (in radian).
	 * @return The current grid object.
	 */
	public EllipseGrid setStartAngle(float startAngle) {
		this.startAngle = startAngle;
		return this;
	}

	/**
	 * Sets the padding (space between elements of the grid).
	 * 
	 * @param padding
	 *            The new padding of this grid [0, 1].
	 * 
	 * @return The current grid object.
	 */
	public EllipseGrid setPadding(float padding) {

		this.padding = padding;

		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));
		}
	}

	// getter for whole GridMaker

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

		return coordinates;
	}

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

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

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

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

	/**
	 * Returns the angle that describes the position of the first element of
	 * this grid.
	 * 
	 * @return The angle.
	 */
	public Float getStartAngle() {
		return this.startAngle;
	}

	/**
	 * Returns the radius of this grid (use this function, when this grid is a
	 * circle and not an ellipse).
	 * 
	 * @return The value of the radius.
	 */
	public Float getRadius() {
		return this.radiusX;
	}

	/**
	 * Returns the horizontal radius of this grid.
	 * 
	 * @return The value of the horizontal radius.
	 */
	public Float getRadiusX() {
		return this.radiusX;
	}

	/**
	 * Returns the vertical radius of this grid.
	 * 
	 * @return The value of the vertical radius.
	 */
	public Float getRadiusY() {
		return this.radiusY;
	}

	/**
	 * Returns the space between the elements of the grid.
	 * 
	 * @return The padding.
	 */
	public Float getPadding() {
		return this.padding;
	}

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

	// getter for Elements

	/**
	 * Returns the angle of an element with the given index.
	 * 
	 * @param index
	 *            The index of the element [0, size()]
	 * @return The angle the element with that index.
	 */
	public Float getAngle(Integer index) {
		return startAngle + index * RMath.TWO_PI / size;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getX(java.lang.Integer)
	 */
	@Override
	public Float getX(Integer index) {
		return (float) (x + Math.cos(getAngle(index)) * radiusX);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY(java.lang.Integer)
	 */
	@Override
	public Float getY(Integer index) {
		return (float) (y + Math.sin(getAngle(index)) * radiusY);
	}

	/**
	 * Calculates and returns the size of any element of the grid. The size
	 * equals to the distance of any element to their neighbors.
	 * 
	 * @return The size of any element of this grid.
	 */
	public Float elementSize() {
		Float x1 = getX(0);
		Float y1 = getY(0);
		Float x2;
		Float y2;

		if (size() == 1) {
			x2 = this.getRadiusX();
			y2 = this.getRadiusY();
		} else {
			x2 = getX(1);
			y2 = getY(1);
		}

		return (float) Math.pow(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2), 0.5) *  (1 - padding);
	}

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

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