package net.returnvoid.graphics.shape;

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

/**
 * A class for building and transforming ellipse.
 * 
 * @author Diana Lange
 *
 */
public class Ellipse implements RShape {

	/**
	 * Nearly all calculations will be be done by a rectangle representation of
	 * this ellipse (a rectangle with same dimensions and with same rotation).
	 */
	private Rect rect;

	/**
	 * The PApplet object for drawing thinks.
	 */
	protected PApplet parent;

	/**
	 * The minimal x-location of this shape (defines the bounding box). Can be
	 * null. If this is null, the shape has been updated (e.g. was rotated) and
	 * the min / max locations & bounding box have to be re-calculated.
	 */
	private Float minX = null;

	/**
	 * The maximal x-location of this shape (defines the bounding box).
	 */
	private Float maxX = null;

	/**
	 * The minimal y-location of this shape (defines the bounding box).
	 */
	private Float minY = null;

	/**
	 * The maximal y-location of this shape (defines the bounding box).
	 */
	private Float maxY = null;

	/**
	 * The bounding box of this shape. Will be null until getBoundingBox() is
	 * called.
	 */
	private Rect boundingBox = null;

	/**
	 * Builds a shape with the given coordinates. The coordinates should mark a
	 * bounding rectangle for the ellipse.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param coordinates
	 *            The coordinates for ellipse (corners).
	 */
	public Ellipse(PApplet parent, PVector[] coordinates) {
		this.parent = parent;
		rect = new Rect(parent, 0, 0, 0, 0);
		rect.getCoordinates()[0] = coordinates[0];
		rect.getCoordinates()[1] = coordinates[1];
		rect.getCoordinates()[2] = coordinates[2];
		rect.getCoordinates()[3] = coordinates[3];
		rect.computeMinMax();
	}

	/**
	 * Builds a new ellipse with the given parameters. The rotation for any
	 * newly built shape will be 0.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param x
	 *            The center-x-location of the ellipse.
	 * @param y
	 *            The center-y-location of the ellipse.
	 * @param width
	 *            The width the rectangle.
	 * @param height
	 *            The height the rectangle.
	 */
	public Ellipse(PApplet parent, float x, float y, float width, float height) {
		this.parent = parent;
		rect = new Rect(parent, x - width * 0.5f, y - height * 0.5f, width, height);
	}

	/**
	 * Builds a new ellipse with the given parameters. The rotation for any
	 * newly built shape will be 0.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param p
	 *            The center-location of the ellipse.
	 * @param dimension
	 *            The width and height of the ellipse.
	 */
	public Ellipse(PApplet parent, PVector p, PVector dimension) {
		this.parent = parent;
		PVector loc = new PVector(p.x - dimension.x * 0.5f, p.y - dimension.y * 0.5f);
		rect = new Rect(parent, loc, dimension);
	}

	// Getters

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#size()
	 */
	@Override
	public int size() {
		return this.rect.size();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#get(int)
	 */
	@Override
	public PVector get(int i) {
		return this.rect.get(i);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getStart()
	 */
	@Override
	public PVector getStart() {
		return this.rect.getStart();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getBoundingBox()
	 */
	@Override
	public Rect getBoundingBox() {
		if (boundingBox == null) {
			if (minX == null) {
				computeMinMax();
			}

			boundingBox = new Rect(parent, minX, minY, maxX - minX, maxY - minY);
		}

		return boundingBox;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getCenter()
	 */
	@Override
	public PVector getCenter() {
		if (minX == null) {
			computeMinMax();
		}

		return new PVector(0.5f * (minX + maxX), 0.5f * (minY + maxY));
	}

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

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

	/**
	 * Returns the width of the ellipse.
	 * 
	 * @return The width.
	 */
	public float getWidth() {
		return this.rect.getWidth();
	}

	/**
	 * Returns the height of the ellipse.
	 * 
	 * @return The height.
	 */
	public float getHeight() {
		return this.rect.getHeight();
	}

	/**
	 * Returns the area.
	 * 
	 * @return The area.
	 */
	public float getArea() {
		return RMath.PI * 0.5f * getWidth() * 0.5f * getHeight();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getCoordinates()
	 */
	@Override
	public PVector[] getCoordinates() {
		return this.rect.getCoordinates();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getRotation()
	 */
	@Override
	public Float getRotation() {
		return this.rect.getRotation();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#copy()
	 */
	@Override
	public Ellipse copy() {
		PVector[] copyCoordinates = new PVector[4];
		copyCoordinates[0] = new PVector(this.get(0).x, this.get(0).y);
		copyCoordinates[1] = new PVector(this.get(1).x, this.get(1).y);
		copyCoordinates[2] = new PVector(this.get(2).x, this.get(2).y);
		copyCoordinates[3] = new PVector(this.get(3).x, this.get(3).y);

		return new Ellipse(parent, copyCoordinates);
	}

	// setter

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setRotation(float)
	 */
	@Override
	public Ellipse setRotation(float angle) {
		rect.rotate(getStart(), angle - getRotation());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * net.returnvoid.graphics.shape.RShape#setRotation(processing.core.PVector,
	 * float)
	 */
	@Override
	public Ellipse setRotation(PVector rotationCenter, float angle) {
		rect.rotate(rotationCenter, angle - getRotation());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the width and height of the ellipse. The ellipse will be scaled
	 * around <b>getStart()</b>.
	 * 
	 * @param width
	 *            The new width of the ellipse (> 0).
	 * @param height
	 *            The new height of the ellipse (> 0).
	 * @return The current shape.
	 */
	public Ellipse setDimension(float width, float height) {
		rect.scale(getStart(), width / getWidth(), height / getHeight());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the width and height of the ellipse. The ellipse will be scaled
	 * around <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param width
	 *            The new width of the ellipse (> 0).
	 * @param height
	 *            The new height of the ellipse (> 0).
	 * @return The current shape.
	 */
	public Ellipse setDimension(PVector center, float width, float height) {
		rect.scale(center, width / getWidth(), height / getHeight());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the width of the ellipse. The ellipse will be scaled around
	 * <b>getStart()</b>.
	 * 
	 * @param width
	 *            The new width of the rectangle (> 0).
	 * @return The current shape.
	 */
	public Ellipse setWidth(float width) {

		rect.scale(getStart(), width / getWidth(), 1);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the width of the ellipse. The ellipse will be scaled around
	 * <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param width
	 *            The new width of the ellipse (> 0).
	 * @return The current shape.
	 */
	public Ellipse setWidth(PVector center, float width) {

		rect.scale(center, width / getWidth(), 1);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the height of the ellipse. The ellipse will be scaled around
	 * <b>getStart()</b>.
	 * 
	 * @param height
	 *            The new height of the ellipse (> 0).
	 * @return The current shape.
	 */
	public Ellipse setHeight(float height) {

		rect.scale(getStart(), 1, height / getHeight());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Sets the height of the ellipse. The ellipse will be scaled around
	 * <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param height
	 *            The new height of the ellipse (> 0).
	 * @return The current shape.
	 */
	public Ellipse setHeight(PVector center, float height) {

		rect.scale(center, 1, height / getHeight());
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setX(float)
	 */
	@Override
	public Ellipse setX(float x) {
		rect.setX(x - this.getBoundingBox().getWidth() * 0.5f);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setY(float)
	 */
	@Override
	public Ellipse setY(float y) {

		rect.setY(y - this.getBoundingBox().getHeight() * 0.5f);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setLocation(float, float)
	 */
	@Override
	public Ellipse setLocation(float x, float y) {
		rect.setLocation(x, y);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	// calculus

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#translate(float, float)
	 */
	@Override
	public Ellipse translate(float x, float y) {
		rect.translate(x, y);
		this.boundingBox = null;
		this.minX = null;

		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(float)
	 */
	@Override
	public Ellipse rotate(float angle) {
		rect.rotate(getStart(), angle);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(processing.core.PVector,
	 * float)
	 */
	@Override
	public Ellipse rotate(PVector rotationCenter, float angle) {
		rect.rotate(rotationCenter, angle);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(float)
	 */
	@Override
	public Ellipse scale(float scale) {
		rect.scale(getStart(), scale, scale);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(processing.core.PVector,
	 * float)
	 */
	@Override
	public Ellipse scale(PVector scaleCenter, float scale) {
		rect.scale(scaleCenter, scale, scale);
		this.boundingBox = null;
		this.minX = null;
		return this;
	}

	/**
	 * Scale the shape by the given scaling factors. For scaling factor < 1 the
	 * shape will shrink. For scaling factors > 1 the shape will expand. The
	 * scaling center will be the given location <b>scaleCenter</b>.
	 * 
	 * @param scaleCenter
	 *            The scaling center.
	 * @param scaleX
	 *            The horizontal scaling.
	 * @param scaleY
	 *            The vertical scaling.
	 * @return The current shape.
	 */
	public Ellipse scale(PVector scaleCenter, float scaleX, float scaleY) {
		rect.scale(scaleCenter, scaleX, scaleY);
		this.boundingBox = null;
		this.minX = null;

		return this;
	}

	/**
	 * Forces to calculate the bounds of this shape and store them. The bounds
	 * can be accessed by getBoundingBox().
	 */
	public void computeMinMax() {
		// https://math.stackexchange.com/questions/91132/how-to-get-the-limits-of-rotated-ellipse
		
		// Basically this calculates points on an ellipse and then estimates
		// the distance from found x / y to the center
		PVector p = rect.getCenter();
		float a = this.getRotation();
		float r1 = getWidth() / 2;
		float r2 = getHeight() / 2;
		double cc = Math.pow(PApplet.cos(a), 2);
		double ss = Math.pow(PApplet.sin(a), 2);
		double r1r1 = Math.pow(r1, 2);
		double r2r2 = Math.pow(r2, 2);
		float x = (float) Math.pow(r1r1 * cc + r2r2 * ss, 0.5);
		float y = (float) Math.pow(r1r1 * ss + r2r2 * cc, 0.5);

		this.minX = p.x - x;
		this.maxX = p.x + x;
		this.minY = p.y - y;
		this.maxY = p.y + y;
	}

	// drawing

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#draw()
	 */
	@Override
	public Ellipse draw() {
		float angle = this.getRotation();
		PVector center = getCenter();
		if (angle != 0) {
			parent.pushMatrix();
			parent.translate(center.x, center.y);
			parent.rotate(angle);
			parent.ellipse(0, 0, getWidth(), getHeight());
			parent.popMatrix();
		} else {

			parent.ellipse(center.x, center.y, getWidth(), getHeight());
		}

		return this;
	}

}
