package net.returnvoid.graphics.shape;

import java.util.ArrayList;

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

import processing.core.PVector;

/**
 * A class for building and transforming line segements. LineSegments is a
 * continuous sequence of lines defined by an input set of coordinates.
 * 
 * @author Diana Lange
 *
 */
public class LineSegments implements RShape {

	/**
	 * The coordinates of this shapes.
	 */
	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 LineSegments(PApplet parent) {
		this.parent = parent;
	}

	/**
	 * Builds a new LineSegments shape with the given parameters.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param coordinates
	 *            The coordinates of this shapes. The first line will have its
	 *            start at coordinates[0] and its end at coordinates[1]. The
	 *            second line will have its start at coordinates[1] and its end
	 *            at coordinates[2] and so on. The number of coordinates should
	 *            be at least 2.
	 */
	public LineSegments(PApplet parent, PVector[] coordinates) {
		if (coordinates.length < 2) {
			System.err.println("You can't build a LineSegments with less than 2 points");
		}

		this.parent = parent;
		this.coordinates = coordinates;
	}

	/**
	 * Builds a new LineSegments shape with the given parameters.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param coordinates
	 *            The coordinates of this shapes. The first line will have its
	 *            start at coordinates.get(0) and its end at coordinates.get(1).
	 *            The second line will have its start at coordinates.get(1) and
	 *            its end at coordinates.get(2) and so on. The number of
	 *            coordinates should be at least 2.
	 */
	public LineSegments(PApplet parent, ArrayList<PVector> coordinates) {
		if (coordinates.size() < 2) {
			System.err.println("You can't build a LineSegments with less than 2 points");
		}

		this.parent = parent;
		this.coordinates = new PVector[coordinates.size()];
		for (int i = 0; i < coordinates.size(); i++) {
			this.coordinates[i] = coordinates.get(i);
		}
	}

	// Getters

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

	/**
	 * Returns the number of lines for this LineSegments shape.
	 * 
	 * @return The number of lines.
	 */
	public int lineSize() {
		return coordinates.length - 1;
	}

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

	/**
	 * Returns the line with the given index. Make sure the given index is
	 * within bounds. Example: <br>
	 * 
	 * <pre>
	 * for (int i = 0; i < myLineSegmentsShape.lineSize(); i++) {
	 * 	myLineSegmentsShape.getLine(i).draw();
	 * }
	 * </pre>
	 * 
	 * @param i
	 *            The index of the line [0, lineSize()).
	 * @return The line object.
	 */
	public Line getLine(int i) {
		return new Line(parent, coordinates[i], coordinates[i + 1]);
	}

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

	/*
	 * (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 end coordinate for this shape.
	 * 
	 * @return The end location.
	 */
	public PVector getEnd() {
		return coordinates[coordinates.length - 1];
	}

	/**
	 * Returns the length (distance from start to end) of this shape.
	 * 
	 * @return The length of the shape.
	 */
	public float getLength() {
		return PVector.dist(getStart(), getEnd());
	}

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

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

	/**
	 * Returns the arc length (sum of line lengths) of this shape.
	 * 
	 * @return The length of the line.
	 */
	public float getArcLength() {
		float length = 0;
		for (int i = 1; i < coordinates.length; i++) {

			length += coordinates[i - 1].dist(coordinates[i]);
		}

		return length;
	}

	/*
	 * (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#copy()
	 */
	@Override
	public LineSegments copy() {
		LineSegments copySeg = new LineSegments(parent);
		copySeg.coordinates = new PVector[this.coordinates.length];
		for (int i = 0; i < this.coordinates.length; i++) {
			copySeg.coordinates[i] = new PVector(this.coordinates[i].x, this.coordinates[i].y);
		}

		return copySeg;
	}

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

	// Setter

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

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

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

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

	/**
	 * Sets the length of this shape. The shape will be scaled around
	 * <b>getStart()</b>.
	 * 
	 * @param length
	 *            The new length of the line (> 0).
	 * @return The current shape.
	 */
	public LineSegments setLength(float length) {

		return scale(coordinates[0], length / getLength(), 1);
	}

	/**
	 * Sets the length of this shape. The shape will be scaled around
	 * <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param length
	 *            The new length of the line (> 0).
	 * @return The current shape.
	 */
	public LineSegments setLength(PVector center, float length) {

		return scale(center, length / getLength());
	}

	/**
	 * Updates the position of the control point with the given index.
	 * 
	 * @param i
	 *            The index of the control point which will be updated [0,
	 *            size()).
	 * @param p
	 *            The new position of the control point.
	 * @return The current shape,
	 */
	public LineSegments set(int i, PVector p) {

		minX = null;
		boundingBox = null;

		this.coordinates[i] = p;

		return this;
	}

	/**
	 * Updates the position of the control point with the given index.
	 * 
	 * @param i
	 *            The index of the control point which will be updated [0,
	 *            size()).
	 * @param x
	 *            The new x-position of the control point.
	 * @param y
	 *            The new y-position of the control point.
	 * @return The current shape,
	 */
	public LineSegments set(int i, float x, float y) {

		this.coordinates[i].x = x;
		this.coordinates[i].y = y;

		return this;
	}

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

		return translate(x - getX(), 0);
	}

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

		return translate(0, y - getY());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setLocation(float, float)
	 */
	@Override
	public LineSegments setLocation(float x, float y) {
		return translate(x - getX(), y - getY());
	}

	// Calculus

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#translate(float, float)
	 */
	@Override
	public LineSegments 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 LineSegments rotate(float angle) {
		return rotate(coordinates[0], angle);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(processing.core.PVector,
	 * float)
	 */
	@Override
	public LineSegments 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 LineSegments scale(float scale) {
		return scale(coordinates[0], scale, scale);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(processing.core.PVector,
	 * float)
	 */
	@Override
	public LineSegments 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 LineSegments 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];
	}

	/**
	 * Removes all lines from the given set of lines that intersect with this
	 * shape.
	 * 
	 * @param toRemove
	 *            A set of lines.
	 */
	public void removeIntersectingLines(ArrayList<Line> toRemove) {
		removeIntersectingLines(toRemove, false);
	}

	/**
	 * Removes all lines from the given set of lines that intersect with this
	 * shape.
	 * 
	 * @param toRemove
	 *            A set of lines.
	 * @param ignoreStartEnd
	 *            If true, intersections that happen directly at the start or
	 *            the end of any line will be ignored (i.e. if the intersection
	 *            happens at start / end then the line will not be removed).
	 */
	public void removeIntersectingLines(ArrayList<Line> toRemove, boolean ignoreStartEnd) {

		for (int i = 0; i < this.lineSize(); i++) {
			Line l = this.getLine(i);
			for (int j = 0; j < toRemove.size(); j++) {
				if (l.hasIntersection(toRemove.get(j), ignoreStartEnd)) {
					toRemove.remove(j);
					j--;
				}
			}
		}

	}

	// Drawing

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#draw()
	 */
	@Override
	public LineSegments draw() {
		parent.beginShape();
		for (PVector p : coordinates) {
			parent.vertex(p.x, p.y);
		}
		parent.endShape();
		return this;
	}

	// Conversion

	/**
	 * Converts this shape to a curve. Does a real copy of the coordinates of
	 * this shape.
	 * 
	 * @return The Curve shape.
	 */
	public Curve toCurve() {
		PVector[] copyCoordinates = new PVector[this.coordinates.length];
		for (int i = 0; i < this.coordinates.length; i++) {
			copyCoordinates[i] = new PVector(this.coordinates[i].x, this.coordinates[i].y);
		}

		return new Curve(parent, copyCoordinates);
	}

	/**
	 * Converts this shape to a curve. Does a shallow copy of the coordinates of
	 * this shape.
	 * 
	 * @return The Curve shape.
	 */
	public Curve getCurve() {
		return new Curve(parent, this.coordinates);
	}

	/**
	 * Converts this shape to a line which shares the same start and end as this
	 * shape. Does a real copy of the coordinates of this shape.
	 * 
	 * @return The Line shape.
	 */
	public Line toLine() {
		return new Line(parent, new PVector(getStart().x, getStart().y), new PVector(getEnd().x, getEnd().y));
	}

	/**
	 * Converts this shape to a line which shares the same start and end as this
	 * shape. Does a shallow copy of the coordinates of this shape.
	 * 
	 * @return The Line shape.
	 */
	public Line getLine() {
		return new Line(parent, getStart(), getEnd());
	}

	/**
	 * Converts this shape to a lines (each segment will be a single line). Does
	 * a real copy of the coordinates of this shape.
	 * 
	 * @return The lines.
	 */
	public Line[] toLines() {
		return Line.toLines(parent, this.copy().coordinates);
	}

	/**
	 * Converts this shape to a lines (each segment will be a single line). Does
	 * a shallow copy of the coordinates of this shape.
	 * 
	 * @return The lines.
	 */
	public Line[] getLines() {
		return Line.toLines(parent, this.coordinates);
	}

}
