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 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 not visually evenly. This means, the elements will be closer to
 * each other in the center of the spiral than the ones at the bounds.
 * 
 * @author Diana Lange
 *
 */
public class SpiralGrid 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 angle distance for each element.
	 */
	private Float angleSteps;

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

	// setter

	/**
	 * Sets the number of rotations the spiral should make. If you want to set
	 * up the number of elements for this grid, you have to call setSize() after
	 * this.
	 * 
	 * @param r
	 *            The number of rotations (>= 0.01)
	 * @return The current grid object.
	 */
	public SpiralGrid setRotations(float r) {
		r = RMath.abs(r);
		this.rotations = r > 0 ? r : 0.1f;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Set the position of the first element of the grid.
	 * 
	 * @param radians
	 *            The angle which describes the position of the first element
	 *            (in radian).
	 * @return The current grid object.
	 */
	public SpiralGrid setStartAngle(float radians) {

		this.startAngle = radians;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Set the position of the first element of the grid.
	 * 
	 * @param degrees
	 *            The angle which describes the position of the first element
	 *            (in degrees).
	 * @return The current grid object.
	 */
	public SpiralGrid setStartAngleDeg(float degrees) {

		return setStartAngle(RMath.TWO_PI * degrees / 360);
	}

	/**
	 * Sets the direction of the rotation.
	 * 
	 * @param dir
	 *            Clockwise for dir >= 0, anti-clockwise for dir < 0.
	 * @return The current grid object.
	 */
	public SpiralGrid setRotationDirection(Integer dir) {
		this.dir = dir < 0 ? -1 : 1;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the angle distance between each element (in radians).
	 * 
	 * @param radians
	 *            The angle.
	 * @return The current grid object.
	 */
	public SpiralGrid setAngleSteps(float radians) {
		this.angleSteps = RMath.abs(radians) < 0.001f ? 0.001f : radians;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the angle distance between each element (in degrees).
	 * 
	 * @param degrees
	 *            The angle.
	 * @return The current grid object.
	 */
	public SpiralGrid setAngleStepsDeg(float degrees) {
		return setAngleSteps(RMath.TWO_PI * degrees / 360);
	}

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#setY(float)
	 */
	@Override
	public SpiralGrid setY(float y) {
		this.y = y;
		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 SpiralGrid setEndRadius(float r) {
		this.endRadius = r;
		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 SpiralGrid setStartRadius(float r) {
		this.startRadius = r;
		this.wasUpdated = true;
		return this;
	}

	/**
	 * Sets the number of elements which will be placed on the grid. This
	 * function should be called after the number of rotations is set.
	 * 
	 * @param size
	 *            The number of elements for this grid.
	 * @return The current grid object.
	 */
	public SpiralGrid setSize(Integer size) {
		size = RMath.abs(size);
		if (size < 1) {
			size = 1;
		}

		return this.setAngleSteps(rotations * RMath.TWO_PI / (size - 1));
	}
	
	/**
	 * Sets the padding (space between elements of the grid).
	 * 
	 * @param padding
	 *            The new padding of this grid (percentage based on elementSize(Integer)) [0, 1].
	 * 
	 * @return The current grid object.
	 */
	public SpiralGrid 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#size()
	 */
	@Override
	public Integer size() {
		return (int) (rotations * RMath.TWO_PI / angleSteps) + 1;
	}

	/**
	 * Returns the number of rotations of this spiral.
	 * 
	 * @return The number of rotations.
	 */
	public Float getRotations() {
		return this.rotations;
	}

	/**
	 * 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 angle distance for each element.
	 * 
	 * @return The angle (in radians).
	 */
	public Float getAngleSteps() {
		return this.angleSteps;
	}

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY()
	 */
	@Override
	public Float getY() {
		return this.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 last element of
	 * this grid.
	 * 
	 * @return The ending radius.
	 */
	public Float getEndRadius() {
		return this.endRadius;
	}

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

	// getter for element informations

	/**
	 * 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 * angleSteps + (dir == 1 ? 0 : -RMath.PI / 2);
	}

	/**
	 * Returns the the radius (= distance from center) of an element with the
	 * given index.
	 * 
	 * @param index
	 *            The index of the element [0, size()]
	 * @return The distance to the center of the element with that index.
	 */
	public Float getRadius(Integer index) {
		return startRadius + index * (endRadius - startRadius) / size();
	}

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.grid.RGrid#getY(java.lang.Integer)
	 */
	@Override
	public Float getY(Integer index) {
		Float angle = getAngle(index);
		Float radius = getRadius(index);
		return (float) (y + dir * Math.sin(angle) * radius);
	}
	
	/**
	 * Returns the space between the elements of the grid.
	 * 
	 * @return The padding.
	 */
	public Float getPadding() {
		return this.padding;
	}

	/**
	 * Calculates and returns the size of an element of the grid. The size
	 * equals to the distance of the element to its neighbors.
	 * 
	 * @param index
	 *            The index of the element [0, size()]
	 * @return The size of the element at the given index.
	 */
	public Float elementSize(Integer index) {
		Float x = getX(index);
		Float y = getY(index);
		Float prevX;
		Float prevY;
		Float nextX;
		Float nextY;

		if (index == 0) {
			prevX = nextX = getX(index + 1);
			prevY = nextY = getY(index + 1);
		} else {
			prevX = getX(index - 1);
			prevY = getY(index - 1);
			nextX = getX(index + 1);
			nextY = getY(index + 1);
		}

		return 0.5f * (float) (Math.pow(Math.pow(prevX - x, 2) + Math.pow(prevY - y, 2), 0.5)
				+ Math.pow(Math.pow(nextX - x, 2) + Math.pow(nextY - y, 2), 0.5)) * (1 - padding);
	}

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

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