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 lenses that a deformed by a curve
 * (Catmull-Rom Curves).
 * 
 * @author Diana Lange
 */
public class CurvedLens implements RShape {

	/**
	 * The coordinates that describe the curve in the middle (the lens is built
	 * around that curve).
	 */
	private LineSegments centerCooridates;

	/**
	 * The thickness of the lens.
	 */
	private float height;

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

	/**
	 * The coordinates of this shapes (outline coordinates).
	 */
	private PVector[] coordinates;

	/**
	 * 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 center curve will be a smoothend version of the centerCooridates.
	 * This factor defines how many coordinates the smoothend curve will have.
	 * For detailScale < 1 : the curve will have less coordinates than
	 * centerCooridates; for detailScale > 1 : the curve will have more
	 * coordinates than centerCooridates.
	 */
	private float detailScale;

	/**
	 * 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 CurvedLens(PApplet parent) {
		this.parent = parent;
	}

	/**
	 * Builds a new CurvedLens shape with the given parameters.
	 * 
	 * @param parent
	 *            The PApplet object.
	 * @param coordinates
	 *            The coordinates of that describe the center curve for this
	 *            lens.
	 * @param height
	 *            The thickness of the lens.
	 */
	public CurvedLens(PApplet parent, PVector[] coordinates, float height) {
		if (coordinates.length < 2) {
			System.err.println("You can't build a lens with less than 2 points");
		}

		this.parent = parent;
		this.detailScale = 1f;
		this.centerCooridates = new LineSegments(parent, coordinates);
		this.height = height / 2;
		this.coordinates = null;
		this.buildOutline();

	}

	// Getters

	/**
	 * Returns the detailsScale factor. The center curve will be a smoothend
	 * version of the LineSegments given by getCoordinates(). The center curve
	 * defines the shape of the lens. The detailScale factor defines how many
	 * coordinates the smoothend curve will have. For detailScale < 1 : the
	 * curve will have less coordinates than centerCooridates; for detailScale >
	 * 1 : the curve will have more coordinates than centerCooridates.
	 * 
	 * @return The scale of details.
	 */
	public float getDetailScale() {
		return detailScale;
	}

	/**
	 * Returns the number of coordinates for the center curve. The center curve
	 * will be a smoothend version of the LineSegments given by
	 * getCoordinates(). The center curve defines the shape of the lens. The
	 * details are the number of coordinates the smoothend curve will have.
	 * 
	 * @return The number of coordinates for the curve.
	 */
	public int getDetails() {
		return (int) (this.detailScale * this.centerCooridates.size());
	}

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

	/**
	 * Returns the number of coordinates that defines this shape (points on
	 * center curve).
	 * 
	 * @return The number of coordinates of the center curve.
	 */
	public int centerSize() {
		return this.centerCooridates.size();
	}

	/*
	 * (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 centerCooridates.getStart();
	}

	/**
	 * Returns the end coordinate for this shape.
	 * 
	 * @return The end location.
	 */
	public PVector getEnd() {
		return centerCooridates.getEnd();
	}

	/*
	 * (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 thickness of this lens.
	 * 
	 * @return The thickness.
	 */
	public float getHeight() {
		return this.height * 2;
	}

	/**
	 * Returns the length (distance from start to end) of this shape.
	 * 
	 * @return The length of the shape.
	 */
	public float getLength() {
		return centerCooridates.getLength();
	}
	
	/**
	 * Returns the area (approx).
	 * 
	 * @return The area.
	 */
	public float getArea() {
		Curve curve = this.toSmoothedCurve();
		return (float) (curve.getArcLength() * this.getHeight() * (1 - Math.cos(1)));
	}

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

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#copy()
	 */
	@Override
	public CurvedLens copy() {

		LineSegments segCopy = centerCooridates.copy();
		CurvedLens clCopy = new CurvedLens(parent);
		clCopy.centerCooridates = segCopy;
		clCopy.height = height;
		clCopy.detailScale = detailScale;
		clCopy.coordinates = new PVector[size()];
		PVector start = segCopy.getStart();
		PVector end = segCopy.getEnd();

		for (int i = 0; i < size(); i++) {
			if (get(i) == getStart()) {
				clCopy.coordinates[i] = start;
			} else if (get(i) == getEnd()) {
				clCopy.coordinates[i] = end;
			} else {
				clCopy.coordinates[i] = new PVector(get(i).x, get(i).y);
			}
		}

		return clCopy;
	}

	// Setter

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

	/**
	 * Sets the length of this shape. The lens will be scaled around
	 * <b>center</b>.
	 * 
	 * @param center
	 *            The control point for the scaling.
	 * @param newLength
	 *            The new length of the line (> 0).
	 * @return The current shape.
	 */
	public CurvedLens setLength(PVector center, float newLength) {
		if (newLength > this.size() * 2) {
			this.centerCooridates.setLength(center, newLength);
			this.buildOutline();
			return this;
		} else {
			return this;
		}
	}

	/**
	 * Sets the thickness of the lens. The outline will be re-build.
	 * 
	 * @param height
	 *            The thickness of the lens.
	 * @return The current shape.
	 */
	public CurvedLens setHeight(float height) {
		if (height > 1) {
			this.height = height / 2;
			this.buildOutline();
		}

		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setRotation(float)
	 */
	@Override
	public CurvedLens setRotation(float angle) {
		return rotate(getStart(), angle - getRotation());
	}

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setX(float)
	 */
	@Override
	public CurvedLens setX(float x) {
		return translate(x - getX(), 0);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#setY(float)
	 */
	@Override
	public CurvedLens setY(float y) {
		return translate(0, y - getY());
	}

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

		/*
		 * Rect box = this.getBoundingBox();
		 * 
		 * return translate(x - box.getStart().x * (this.getStart() <
		 * this.getE., y - box.getStart().y); return translate(x - getX(), y -
		 * getY());
		 */
		return translate(x - getCenter().x + this.getBoundingBox().getWidth() * 0.5f,
				y - getCenter().y + this.getBoundingBox().getHeight() * 0.5f);
	}

	/**
	 * Sets the detailsScale factor. The center curve will be a smoothend
	 * version of the LineSegments given by getCoordinates(). The center curve
	 * defines the shape of the lens. The detailScale factor defines how many
	 * coordinates the smoothend curve will have. For detailScale < 1 : the
	 * curve will have less coordinates than centerCooridates; for detailScale >
	 * 1 : the curve will have more coordinates than centerCooridates. The
	 * outline will be re-build.
	 * 
	 * @param detailScale
	 *            The new detailScale (> 0).
	 * @return The current shape.
	 */
	public CurvedLens setDetailScale(float detailScale) {
		if (detailScale > 0.1 && centerCooridates.size() * detailScale >= 3) {
			this.detailScale = detailScale;
			this.buildOutline();
			return this;
		}

		return this;
	}

	/**
	 * Sets the number of coordinates for the center curve. The center curve
	 * will be a smoothend version of the LineSegments given by
	 * getCoordinates(). The center curve defines the shape of the lens. The
	 * details are the number of coordinates the smoothend curve will have.
	 * 
	 * @param details
	 *            The number of coordinates for the curve (>= 2).
	 * @return The current shape.
	 */
	public CurvedLens setDetails(int details) {
		this.detailScale = (float) details / centerCooridates.size();
		this.buildOutline();
		return this;
	}

	// Calculus

	/**
	 * 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(this.coordinates);
		minX = vals[0];
		maxX = vals[2];
		minY = vals[1];
		maxY = vals[3];
	}

	/**
	 * Computes the thickness of the lens on a certain point in the shape.
	 * 
	 * @param tplusi
	 *            A parameter that describes the position in the center curve. i
	 *            is the index of the coordinate, t is the parameter that
	 *            describes how near the returned normal is to that coordinate.
	 *            If t is 0 the normal will be directly at the coordinate, if t
	 *            is 1 the normal will be directly at the following coordinate.
	 *            For t=0.5 the normal will be in the middle of the coordinate
	 *            and the following coordinate. t and i should be summed for
	 *            this parameter, e.g. for the normal which is in the middle
	 *            (t=0.5) of coordinate[1] (i=1) and coordinate[2] tplusi should
	 *            be 1+0.5=0.5. i should be in range of [0, centerSize()).
	 * @return The thickness at this point.
	 */
	public float computeHeight(float tplusi) {
		return PApplet.sin(
				RMath.constrain(RMath.map(tplusi, 0, this.centerCooridates.size() - 1, 0, PApplet.PI), 0, PApplet.PI))
				* this.height;
	}

	/**
	 * Computes the thickness of the lens on a certain point in the shape.
	 * 
	 * @param curve
	 *            The center curve (with getDetails() number of coordinates).
	 * @param tplusi
	 *            A parameter that describes the position in the center curve. i
	 *            is the index of the coordinate, t is the parameter that
	 *            describes how near the returned normal is to that coordinate.
	 *            If t is 0 the normal will be directly at the coordinate, if t
	 *            is 1 the normal will be directly at the following coordinate.
	 *            For t=0.5 the normal will be in the middle of the coordinate
	 *            and the following coordinate. t and i should be summed for
	 *            this parameter, e.g. for the normal which is in the middle
	 *            (t=0.5) of coordinate[1] (i=1) and coordinate[2] tplusi should
	 *            be 1+0.5=0.5. i should be in range of [0, getDetails()).
	 * @return The thickness at this point.
	 */
	private float computeHeight(Curve curve, float tplusi) {
		return PApplet.sin(RMath.constrain(RMath.map(tplusi, 0, curve.size() - 1, 0, PApplet.PI), 0, PApplet.PI))
				* this.height;
	}

	/**
	 * Builds the outline of the lens (the coordinates) with the current set
	 * coordinates, detailScale and height.
	 */
	private void buildOutline() {

		// get a smooth curve version of the input with control points that all
		// have the same distance to their neighbors
		Curve curve = this.toSmoothedCurve();

		int s = curve.size();

		ArrayList<Line> l1 = new ArrayList<Line>(s);
		ArrayList<Line> l2 = new ArrayList<Line>(s);

		// i is the index of l1 / l2 arraylist, j is the index of the coordinate
		// in curve
		// since end and start of curve don't need a normal they are skipped
		for (int i = 0, j = 1; i < s - 1; i++, j++) {
			float tplusi = RMath.map(j, 0, s - 1, 0, curve.size() - 1);

			// 0 = x, 1 = y, 2 = angle
			// float tplusi = curve.arcLengthToTPlusI(arcLength);
			float[] normal = curve.getNormal(tplusi);
			float length = computeHeight(curve, tplusi);
			PVector start = new PVector(normal[0], normal[1]);
			PVector end1 = RMath.polarToCartesian(start, normal[2], length);
			PVector end2 = RMath.polarToCartesian(start, -PApplet.PI + normal[2], length);

			Line line1 = new Line(parent, start, end1);
			Line line2 = new Line(parent, start, end2);

			l1.add(line1);
			l2.add(line2);
		}

		// remove the normals that are intersection with the center curve
		// Line.removeIntersectingLines(lLocations, l1, true);
		curve.removeIntersectingLines(l1, true);
		curve.removeIntersectingLines(l2, true);

		// remove the normals that are intersection with other normals
		Line.removeIntersectingLinesHierarchical(l1, false);
		Line.removeIntersectingLinesHierarchical(l2, false);

		// build outline from the two normal line arrays
		coordinates = new PVector[l1.size() + l2.size() + 6];

		coordinates[0] = centerCooridates.getStart();
		coordinates[1] = centerCooridates.getStart();

		int coordIndex = 2;
		for (int i = 0; i < l2.size(); i++, coordIndex++) {
			coordinates[coordIndex] = l2.get(i).getEnd();
		}
		coordinates[coordIndex] = centerCooridates.getEnd();
		coordinates[coordIndex + 1] = centerCooridates.getEnd();
		coordIndex += 2;

		for (int i = l1.size() - 1; i >= 0; i--, coordIndex++) {
			coordinates[coordIndex] = l1.get(i).getEnd();
		}
		coordinates[coordIndex] = centerCooridates.getStart();
		coordinates[coordIndex + 1] = centerCooridates.getStart();

		this.minX = null;
		this.boundingBox = null;
	}

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

		for (int i = 0; i < coordinates.length; i++) {

			PVector p = coordinates[i];

			// transformation already done by the centerCoordinates
			// transformation for these points
			if (p == getStart() || p == getEnd()) {
				continue;
			}

			p.x += x;
			p.y += y;
		}

		this.centerCooridates.translate(x, y);

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

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

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#rotate(processing.core.PVector,
	 * float)
	 */
	@Override
	public CurvedLens rotate(PVector rotationCenter, float angle) {
		this.centerCooridates.rotate(rotationCenter, angle);
		PVector center = rotationCenter;
		for (int i = 0; i < coordinates.length; i++) {
			PVector p = coordinates[i];

			// transformation already done by the centerCoordinates
			// transformation for these points
			if (p == getStart() || p == getEnd()) {
				continue;
			}

			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 CurvedLens scale(float scale) {
		return scale(coordinates[0], scale, scale);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#scale(processing.core.PVector,
	 * float)
	 */
	@Override
	public CurvedLens scale(PVector scaleCenter, float scale) {
		return scale(scaleCenter, scale, scale);
	}
	
	// TODO rotation around scaleCenter doesn't work yet.

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

			// 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 = this.getStart();
			float rotation = this.getRotation();
			if (rotation != 0) {
				this.setRotation(0);
			}

			for (int i = 0; i < coordinates.length; i++) {
				PVector p = coordinates[i];
				
				// transformation already done by the centerCoordinates
				// transformation for these points
				if (p == getStart() || p == getEnd()) {
					continue;
				}
				
				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;
	}

	// Drawing

	/*
	 * (non-Javadoc)
	 * 
	 * @see net.returnvoid.graphics.shape.RShape#draw()
	 */
	@Override
	public CurvedLens draw() {
		boolean endDrawn = false;
		parent.beginShape();
		for (PVector p : this.coordinates) {
			parent.curveVertex(p.x, p.y);

			if (!endDrawn && p == centerCooridates.getEnd()) {
				parent.vertex(p.x, p.y);
				endDrawn = true;
			}
		}

		parent.endShape();
		return this;
	}

	/**
	 * Draws outline with lines not curves.
	 * 
	 * @return The current shape.
	 */
	public CurvedLens drawRaw() {
		parent.beginShape();
		for (PVector p : this.coordinates) {
			parent.vertex(p.x, p.y);
		}

		parent.endShape();
		return this;
	}

	/**
	 * Draws the center curve.
	 * 
	 * @return The current shape.
	 */
	public CurvedLens drawCenter() {
		centerCooridates.draw();
		return this;
	}

	// Conversion

	/**
	 * Returns the center curve. Does a shallow copy of the coordinates of the
	 * center and this shape.
	 * 
	 * @return The curve.
	 */
	public Curve getCurve() {
		return this.centerCooridates.getCurve();
	}

	/**
	 * Returns the center curve. Does a real copy of the coordinates of the
	 * center curve and this shape.
	 * 
	 * @return The curve.
	 */
	public Curve toCurve() {
		return this.centerCooridates.copy().getCurve();
	}

	/**
	 * Returns the center LineSegments (raw version of the center curve). Does a
	 * shallow copy of the coordinates of the center and this shape.
	 * 
	 * @return The LineSegments.
	 */
	public LineSegments getLineSegments() {
		return this.centerCooridates;
	}

	/**
	 * Returns the center LineSegments (raw version of the center curve). Does a
	 * real copy of the coordinates of the center and this shape.
	 * 
	 * @return The LineSegments.
	 */
	public LineSegments toLineSegments() {
		return this.centerCooridates.copy();
	}

	/**
	 * Returns the a smoothend version of the center curve (smoothness depends
	 * on detailScale).
	 * 
	 * @return The smoothend center curve.
	 */
	public Curve toSmoothedCurve() {
		return this.centerCooridates.getCurve().toSmoothedCurve(this.detailScale);
	}

}
