package net.returnvoid.graphics.grid;

import java.util.ArrayList;

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 spiral grid. The elements of this grid will be
 * located on a spiral with a given start and end radius. The elements will be
 * distributed visually evenly (approx.).
 * 
 * @author Diana Lange
 *
 */
public class EquiSpiralGrid 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 maximal radius this spiral will reach - intentionally. But this value
	 * can actually be smaller than startRadius. If endRadius < startRadius the
	 * first element will be positioned on the bounds of the spiral. Otherwise
	 * the first element will be positioned in the center of the grid.
	 */
	private Float endRadius;

	/**
	 * The minimal radius this spiral will reach - intentionally. But this value
	 * actually can be higher than endRadius. If endRadius < startRadius the
	 * first element will be positioned on the bounds of the spiral. Otherwise
	 * the first element will be positioned in the center of the grid.
	 */
	private Float startRadius;

	/**
	 * The distance each element will have to its neighbors.
	 */
	private Float elementDistance;

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

	/**
	 * The direction of the rotation of the spiral. For dir=1 the spiral will
	 * rotate clockwise, for dir -1 anti-clockwise.
	 */
	private Integer dir;

	/**
	 * The number of rotations of this spiral.
	 */
	private Float rotations;

	/**
	 * Keeps track if some value has been changed. If any value has been
	 * changed, the coordinates will be re-calculated.
	 */
	private boolean wasUpdated;
	
	/**
	 * The padding of each element to its neighbors.
	 */
	private Float padding;

	/**
	 * An array with all the coordinates of each field in the grid.
	 */
	private ArrayList<PVector> coordinates;

	/**
	 * 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 startRadius
	 *            The radius where the spiral starts.
	 * @param endRadius
	 *            The radius where the spiral ends.
	 * @param elementDistance
	 *            The distance for each element on the grid.
	 * @param rotations
	 *            The number of rotations of this spiral.
	 * @param startAngle
	 *            The angle where the grid starts (in radians). The startAngle
	 *            defines where the first element will positioned.
	 * @param dir
	 *            The direction of the rotation of the spiral. For dir=1 the
	 *            spiral will rotate clockwise, for dir -1 anti-clockwise.
	 */
	public EquiSpiralGrid(float x, float y, float startRadius, float endRadius, float elementDistance, float rotations,
			float startAngle, int dir) {
		this.x = x;
		this.y = y;
		this.padding = 0f;
		this.setStartRadius(startRadius);
		this.setEndRadius(endRadius);
		this.elementDistance = elementDistance;
		if (this.elementDistance < 1) {
			this.elementDistance = 1f;
		}
		
		this.setStartAngle(startAngle);
		this.setRotationDirection(dir);
		this.setRotations(rotations);
		
		this.wasUpdated = true;
	}

	// setter

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

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

	/**
	 * Sets the radius where the spiral starts. The first element of this grid
	 * will be positioned on that radius.
	 * 
	 * @param r
	 *            The starting radius.
	 * @return The grid object.
	 */
	public EquiSpiralGrid setStartRadius(float r) {
		this.startRadius = Math.abs(r);
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the radius where the spiral ends. The last element of this grid will
	 * be positioned on that radius.
	 * 
	 * @param r
	 *            The ending radius.
	 * @return The grid object.
	 */
	public EquiSpiralGrid setEndRadius(float r) {
		this.endRadius = Math.abs(r);
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the distance of each element to its neighbors of this grid.
	 * 
	 * @param elementDistance
	 *            The distance of each element to its neighbors (>= 1).
	 * @return The grid object.
	 */
	public EquiSpiralGrid setElementDistance(float elementDistance) {
		elementDistance = Math.abs(elementDistance);	
		
		this.elementDistance = elementDistance < 1 ? 1 : elementDistance;
		this.wasUpdated = true;
		
		
		return this;
	}

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

	/**
	 * Sets the number of rotations the spiral should make.
	 * 
	 * @param r
	 *            The number of rotations (>= 0.01)
	 * @return The current grid object.
	 */
	public EquiSpiralGrid setRotations(float r) {
		r = RMath.abs(r);
		this.rotations = r < 0.01f ? 0.01f : r;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the direction of the rotation.
	 * 
	 * @param dir
	 *            Clockwise for dir >= 0, anti-clockwise for dir < 0.
	 * @return The current grid object.
	 */
	public EquiSpiralGrid setRotationDirection(int dir) {
		this.dir = dir < 0 ? -1 : 1;
		this.wasUpdated = true;
		return this;
	}
	
	/**
	 * Sets the padding (space between elements of the grid).
	 * 
	 * @param padding
	 *            The new padding of this grid (percentage based on elementDistances()) [0, 1].
	 * 
	 * @return The current grid object.
	 */
	public EquiSpiralGrid 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() {
		// Implementation based on:
		// https://stackoverflow.com/questions/13894715/draw-equidistant-points-on-a-spiral

		if (coordinates == null) {
			coordinates = new ArrayList<PVector>();
		}
		coordinates.clear();

		float startAngle = dir * this.startAngle + (dir == 1 ? 0 : -RMath.PI / 2);
		float radius = 0;
		float maxAngel = (float) (rotations * Math.PI * 2);
		float radiusSteps = (endRadius - startRadius) / maxAngel;

		float angle = startAngle;
		float xx = (float) (x + dir * Math.cos(dir * angle) * (startRadius + radius));
		float yy = (float) (y + Math.sin(dir * angle) * (startRadius + radius));

		coordinates.add(new PVector(xx, yy));

		angle += elementDistance / (startRadius + radius < elementDistance ? elementDistance : (startRadius + radius));

		while (angle <= maxAngel + startAngle) {

			radius = radiusSteps * (angle - startAngle);
			
			xx = (float) (x + dir * Math.cos(dir * angle) * (startRadius + radius));
			yy = (float) (y + Math.sin(dir * angle) * (startRadius + radius));

			coordinates.add(new PVector(xx, yy));

			angle += elementDistance / (startRadius + radius);
		}
	}

	// getter

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

		return coordinates.toArray(new PVector[coordinates.size()]);
	}

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

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

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

	/**
	 * Returns the radius which describes the position of the first element of
	 * this grid.
	 * 
	 * @return The starting radius.
	 */
	public Float getStartRadius() {
		return this.startRadius;
	}

	/**
	 * Returns the radius which describes the position of the last element of
	 * this grid.
	 * 
	 * @return The ending radius.
	 */
	public Float getEndRadius() {
		return this.endRadius;
	}

	/**
	 * The distance each element will have to its neighbors.
	 * 
	 * @return The distance.
	 */
	public Float getElementDistance() {
		return this.elementDistance;
	}

	/**
	 * 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 direction of rotation for this grid. 1=clockwise,
	 * -1=anti-clockwise.
	 * 
	 * @return The direction of rotation.
	 */
	public Integer getRotationDirection() {
		return this.dir;
	}

	/**
	 * Returns the number of rotations of this spiral.
	 * 
	 * @return The number of rotations.
	 */
	public Float getRotations() {
		return this.rotations;
	}
	
	/**
	 * 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() {
		if (wasUpdated) {
			initCoordinates();
		}

		return coordinates.size();
	}

	/**
	 * 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) {
		Float x = getX(index);
		Float y = getY(index);
		Float centerX = this.x;
		Float centerY = this.y;

		return (float) Math.atan2(y - centerY, x - centerX);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getX(java.lang.Integer)
	 */
	@Override
	public Float getX(Integer index) {
		if (wasUpdated) {
			initCoordinates();
		}
		
		index = RMath.constrain(index, 0, coordinates.size() - 1);

		return coordinates.get(index).x;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY(java.lang.Integer)
	 */
	@Override
	public Float getY(Integer index) {
		if (wasUpdated) {
			initCoordinates();
		}
		
		index = RMath.constrain(index, 0, coordinates.size() - 1);

		return coordinates.get(index).y;
	}

	/**
	 * Returns the location of the element with the given index. Usage: <br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myGrid.size(); i++) {
	 * 	PVector position = myGrid.get(index);
	 * }
	 * </pre>
	 * 
	 * @param index
	 *            The index of the element.
	 * @return The position of the element.
	 */
	public PVector get(int index) {
		if (wasUpdated) {
			initCoordinates();
		}
		
		index = RMath.constrain(index, 0, coordinates.size() - 1);

		return coordinates.get(index);
	}

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

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

}
