package net.returnvoid.graphics.shape;

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

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

	/**
	 * The coordinates (of the corners) of this rectangle.
	 */
	private PVector[] coordinates;

	/**
	 * 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 an empty shape. The coordinates have to be set manually.
	 * 
	 * @param parent
	 *            The PApplet object.
	 */
	private Rect(PApplet parent) {
		this.parent = parent;
	}

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

		minX = x;
		minY = y;
		maxX = x + width;
		maxY = y + height;

		coordinates = new PVector[] { new PVector(minX, minY), new PVector(maxX, minY), new PVector(maxX, maxY),
				new PVector(minX, maxY) };
	}

	/**
	 * Builds a new rectangle with the given parameters. The rotation for any
	 * newly built shape will be 0.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param p
	 *            The location of the rectangle (top-left corner).
	 * @param dimension
	 *            The width and height of the rectangle.
	 */
	public Rect(PApplet parent, PVector p, PVector dimension) {
		this.parent = parent;

		minX = p.x;
		minY = p.y;
		maxX = p.x + dimension.x;
		maxY = p.x + dimension.y;

		coordinates = new PVector[] { new PVector(minX, minY), new PVector(maxX, minY), new PVector(maxX, maxY),
				new PVector(minX, maxY) };
	}

	// Getters

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

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

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

	/*
	 * (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 coordinates[0].x;
	}

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

	/**
	 * Returns the width of the rectangle.
	 * 
	 * @return The width.
	 */
	public float getWidth() {
		return coordinates[0].dist(coordinates[1]);
	}

	/**
	 * Returns the height of the rectangle.
	 * 
	 * @return The height.
	 */
	public float getHeight() {
		return coordinates[1].dist(coordinates[2]);
	}

	/**
	 * Returns the area (width * height).
	 * 
	 * @return The area.
	 */
	public float getArea() {
		return getWidth() * getHeight();
	}

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#getRotation()
	 */
	@Override
	public Float getRotation() {
		return PApplet.atan2(coordinates[1].y - coordinates[0].y, coordinates[1].x - coordinates[0].x);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#copy()
	 */
	@Override
	public Rect copy() {
		Rect copyRect = new Rect(parent);

		PVector[] copyCoordinates = new PVector[4];
		copyCoordinates[0] = new PVector(this.coordinates[0].x, this.coordinates[0].y);
		copyCoordinates[1] = new PVector(this.coordinates[1].x, this.coordinates[1].y);
		copyCoordinates[2] = new PVector(this.coordinates[2].x, this.coordinates[2].y);
		copyCoordinates[3] = new PVector(this.coordinates[3].x, this.coordinates[3].y);
		copyRect.coordinates = copyCoordinates;

		return copyRect;
	}

	// setter

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setRotation(float)
	 */
	@Override
	public Rect setRotation(float angle) {

		return rotate(coordinates[0], angle - getRotation());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * net.returnvoid.graphics.shape.RShape#setRotation(processing.core.PVector,
	 * float)
	 */
	@Override
	public Rect setRotation(PVector rotationCenter, float angle) {

		return rotate(rotationCenter, angle - getRotation());
	}

	/**
	 * Sets the width and height of the rectangle. The rectangle will be scaled
	 * around <b>getStart()</b>.
	 * 
	 * @param width
	 *            The new width of the rectangle (> 0).
	 * @param height
	 *            The new height of the rectangle (> 0).
	 * @return The current shape.
	 */
	public Rect setDimension(float width, float height) {

		return scale(coordinates[0], width / getWidth(), height / getHeight());
	}

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

		return scale(center, width / getWidth(), height / getHeight());
	}

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

		return setWidth(this.getStart(), width);
	}

	/**
	 * Sets the width of the rectangle. The rectangle will be scaled around
	 * <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param width
	 *            The new width of the rectangle (> 0).
	 * @return The current shape.
	 */
	public Rect setWidth(PVector center, float width) {
		
		return scale(center, width / getWidth(), 1);
	}

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

		return scale(coordinates[0], 1, height / getHeight());
	}

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

		return scale(center, 1, height / getHeight());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setX(float)
	 */
	@Override
	public Rect setX(float x) {

		return translate(x - getCenter().x + getWidth() * 0.5f, 0);
	}

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

		return translate(0, y - getCenter().y + getHeight() * 0.5f);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setLocation(float, float)
	 */
	@Override
	public Rect setLocation(float x, float y) {

		return translate(x - getCenter().x + getWidth() * 0.5f, y - getCenter().y + getHeight() * 0.5f);
	}

	// calculus

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#translate(float, float)
	 */
	@Override
	public Rect translate(float x, float y) {
		for (int i = 0; i < coordinates.length; i++) {
			PVector p = coordinates[i];
			p.x += x;
			p.y += y;
		}

		minX = null;
		boundingBox = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(float)
	 */
	@Override
	public Rect rotate(float angle) {
		return rotate(coordinates[0], angle);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(processing.core.PVector,
	 * float)
	 */
	@Override
	public Rect rotate(PVector rotationCenter, float angle) {
		PVector center = rotationCenter;
		for (int i = 0; i < coordinates.length; i++) {
			PVector p = coordinates[i];
			float a = angle + PApplet.atan2(p.y - center.y, p.x - center.x);
			float d = center.dist(p);
			p.x = center.x + PApplet.cos(a) * d;
			p.y = center.y + PApplet.sin(a) * d;
		}

		minX = null;
		boundingBox = null;
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(float)
	 */
	@Override
	public Rect scale(float scale) {
		return scale(coordinates[0], scale, scale);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(processing.core.PVector,
	 * float)
	 */
	@Override
	public Rect scale(PVector scaleCenter, float scale) {
		return scale(scaleCenter, scale, scale);
	}

	/**
	 * Scales 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 Rect scale(PVector scaleCenter, float scaleX, float scaleY) {
		if (RMath.abs(scaleX) > 0.01 && RMath.abs(scaleY) > 0.01) {

			if (scaleX == 1 && scaleY == 1) {
				return this;
			}
			
			
			// there shouldn't happen any shape deformation (i.e. angles should
			// not change but with and height of shape). To achieve this
			// the shape is rotate to 0 and the scaling is done
			// afterwards the original rotation is reseted
			PVector center = scaleCenter;
			float rotation = this.getRotation();
			if (rotation != 0) {
				this.setRotation(0);
			}
			
			for (int i = 0; i < coordinates.length; i++) {
				PVector p = coordinates[i];
				float a = PApplet.atan2(p.y - center.y, p.x - center.x);
				float dis = center.dist(p);
				float dx = dis * scaleX;
				float dy = dis * scaleY;
				p.x = center.x + PApplet.cos(a) * dx;
				p.y = center.y + PApplet.sin(a) * dy;
			}

			// bounds needs to be re-calculated
			minX = null;
			boundingBox = null;
			if (rotation != 0) {
				this.setRotation(rotation);
			}
		}

		return this;
	}

	/**
	 * Forces to calculate the bounds of this shape and store them. The bounds
	 * can be accessed by getBoundingBox().
	 */
	public void computeMinMax() {
		float[] vals = RMath.range(coordinates);
		minX = vals[0];
		maxX = vals[2];
		minY = vals[1];
		maxY = vals[3];
	}

	// drawing

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#draw()
	 */
	@Override
	public Rect draw() {
		parent.beginShape(PConstants.QUADS);
		parent.vertex(coordinates[0].x, coordinates[0].y);
		parent.vertex(coordinates[1].x, coordinates[1].y);
		parent.vertex(coordinates[2].x, coordinates[2].y);
		parent.vertex(coordinates[3].x, coordinates[3].y);
		parent.endShape();

		return this;
	}

	// conversion

	/**
	 * Converts the rectangle to four lines. Does a real copy of the coordinates
	 * of this shape.
	 * 
	 * @return The four lines.
	 */
	public Line[] toLines() {
		PVector[] org = this.copy().coordinates;
		PVector[] coordCopy = new PVector[coordinates.length + 1];

		coordCopy[0] = org[0].copy();
		coordCopy[1] = org[1].copy();
		coordCopy[2] = org[2].copy();
		coordCopy[3] = org[3].copy();
		coordCopy[4] = org[0].copy();
		
		return Line.toLines(parent, coordCopy);
	}

	/**
	 * Converts the rectangle to four lines. Does a shallow copy of the
	 * coordinates of this shape.
	 * 
	 * @return The four lines.
	 */
	public Line[] getLines() {
		PVector[] coordCopy = { this.coordinates[0], this.coordinates[1], this.coordinates[2],
				this.coordinates[3], this.coordinates[0].copy() };
		return Line.toLines(parent, coordCopy);
	}

	/**
	 * Converts the rectangle to LineSegments. Does a real copy of the
	 * coordinates of this shape.
	 * 
	 * @return The LineSegments shape.
	 */
	public LineSegments toLineSegments() {
		PVector[] org = this.copy().coordinates;
		PVector[] coordCopy = new PVector[coordinates.length + 1];
		
		coordCopy[0] = org[0].copy();
		coordCopy[1] = org[1].copy();
		coordCopy[2] = org[2].copy();
		coordCopy[3] = org[3].copy();
		coordCopy[4] = org[0].copy();
		return new LineSegments(parent, coordCopy);
	}

	/**
	 * Converts the rectangle to LineSegments. Does a shallow copy of the
	 * coordinates of this shape.
	 * 
	 * @return The four lines.
	 */
	public LineSegments getLineSegments() {
		PVector[] org = this.copy().coordinates;
		PVector[] coordCopy =  new PVector[coordinates.length + 1];
		coordCopy[0] = org[0];
		coordCopy[1] = org[1];
		coordCopy[2] = org[2];
		coordCopy[3] = org[3];
		coordCopy[4] = org[0].copy();
		
		return new LineSegments(parent, coordCopy);
	}
}
