package org.concord.energy2d.math;

import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

/**
 * A blob made of splines (by default, Catmull-Rom) going through a set of points. For efficiency, the splines will not be automatically recalculated when the points are modified.
 * 
 * The update() method must be called before doing anything. It is up to the developer to determine when is the optimal time to call the method.
 * 
 * @author Charles Xie
 * 
 */
public class Blob2D implements TransformableShape {

	private Point2D.Float[] points;
	private GeneralPath path;
	private int steps = 20;
	private float invStep = 1f / steps;
	private float[] px = new float[4];
	private float[] py = new float[4];
	private List<Point2D.Float> pathPoints;

	/** the coordinates of the points */
	public Blob2D(float[] x, float[] y) {
		if (x.length != y.length)
			throw new IllegalArgumentException("the number of x coodinates must be equal to that of the y coordinates.");
		if (x.length < 3)
			throw new IllegalArgumentException("the number of points must be no less than 3.");
		points = new Point2D.Float[x.length];
		for (int i = 0; i < x.length; i++)
			setPoint(i, x[i], y[i]);
		path = new GeneralPath();
		pathPoints = new ArrayList<Point2D.Float>();
		update();
	}

	/** the coordinates of the points */
	public Blob2D(int[] x, int[] y) {
		if (x.length != y.length)
			throw new IllegalArgumentException("the number of x coodinates must be equal to that of the y coordinates.");
		if (x.length < 3)
			throw new IllegalArgumentException("the number of points must be no less than 3.");
		points = new Point2D.Float[x.length];
		for (int i = 0; i < x.length; i++)
			setPoint(i, x[i], y[i]);
		path = new GeneralPath();
		pathPoints = new ArrayList<Point2D.Float>();
		update();
	}

	/** converted from a polygon */
	public Blob2D(Polygon p) {
		points = new Point2D.Float[p.npoints];
		for (int i = 0; i < points.length; i++)
			setPoint(i, p.xpoints[i], p.ypoints[i]);
		path = new GeneralPath();
		pathPoints = new ArrayList<Point2D.Float>();
		update();
	}

	public boolean isClockwise() {
		float sum = 0;
		int n = points.length;
		for (int i = 0; i < n - 1; i++)
			sum += (points[i + 1].x - points[i].x) * (points[i + 1].y + points[i].y);
		sum += (points[0].x - points[n - 1].x) * (points[0].y + points[n - 1].y);
		return sum > 0;
	}

	public Blob2D duplicate() {
		int n = points.length;
		float[] x = new float[n];
		float[] y = new float[n];
		for (int i = 0; i < n; i++) {
			x[i] = points[i].x;
			y[i] = points[i].y;
		}
		return new Blob2D(x, y);
	}

	public Blob2D insertPointBefore(int k) {
		int n = points.length;
		float[] x = new float[n + 1];
		float[] y = new float[n + 1];
		if (k > 0 && k < n) {
			for (int i = 0; i < k; i++) {
				x[i] = points[i].x;
				y[i] = points[i].y;
			}
			x[k] = 0.5f * (points[k].x + points[k - 1].x);
			y[k] = 0.5f * (points[k].y + points[k - 1].y);
			for (int i = k + 1; i < n + 1; i++) {
				x[i] = points[i - 1].x;
				y[i] = points[i - 1].y;
			}
		} else if (k == 0) {
			x[0] = 0.5f * (points[0].x + points[n - 1].x);
			y[0] = 0.5f * (points[0].y + points[n - 1].y);
			for (int i = 1; i < n + 1; i++) {
				x[i] = points[i - 1].x;
				y[i] = points[i - 1].y;
			}
		} else {
			return this;
		}
		return new Blob2D(x, y);
	}

	public Blob2D deletePointBefore(int k) {
		int n = points.length;
		if (n < 4)
			return this;
		float[] x = new float[n - 1];
		float[] y = new float[n - 1];
		if (k > 0 && k < n) {
			for (int i = 0; i < k; i++) {
				x[i] = points[i].x;
				y[i] = points[i].y;
			}
			for (int i = k + 1; i < n; i++) {
				x[i - 1] = points[i].x;
				y[i - 1] = points[i].y;
			}
		} else if (k == 0) {
			for (int i = 1; i < n; i++) {
				x[i - 1] = points[i - 1].x;
				y[i - 1] = points[i - 1].y;
			}
		} else {
			return this;
		}
		return new Blob2D(x, y);
	}

	public GeneralPath getPath() {
		return path;
	}

	public float getPerimeter() {
		float perimeter = 0;
		int n = pathPoints.size();
		float dx, dy;
		Point2D.Float p1, p2 = null;
		for (int i = 0; i < n - 1; i++) {
			p1 = pathPoints.get(i);
			p2 = pathPoints.get(i + 1);
			dx = p2.x - p1.x;
			dy = p2.y - p1.y;
			perimeter += Math.hypot(dx, dy);
		}
		if (p2 != null) {
			p1 = pathPoints.get(0);
			dx = p2.x - p1.x;
			dy = p2.y - p1.y;
			perimeter += Math.hypot(dx, dy);
		}
		return perimeter;
	}

	public void update() {
		path.reset();
		pathPoints.clear();
		int n = points.length;
		path.moveTo(points[n - 1].x, points[n - 1].y);
		pathPoints.add(new Point2D.Float(points[n - 1].x, points[n - 1].y));
		float u;
		float sx, sy;
		int index;
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < 4; j++) { // Initialize points m-2, m-1, m, m+1
				index = (i + j - 2 + n) % n;
				px[j] = points[index].x;
				py[j] = points[index].y;
			}
			for (int k = 0; k < steps; k++) {
				u = k * invStep;
				sx = catmullrom(-2, u) * px[0] + catmullrom(-1, u) * px[1] + catmullrom(0, u) * px[2] + catmullrom(1, u) * px[3];
				sy = catmullrom(-2, u) * py[0] + catmullrom(-1, u) * py[1] + catmullrom(0, u) * py[2] + catmullrom(1, u) * py[3];
				sx *= 0.5f;
				sy *= 0.5f;
				path.lineTo(sx, sy);
				pathPoints.add(new Point2D.Float(sx, sy));
			}
		}
		path.closePath();
	}

	public int getPathPointCount() {
		return pathPoints.size();
	}

	public Point2D.Float getPathPoint(int i) {
		if (i < 0 || i >= pathPoints.size())
			throw new IllegalArgumentException("index is out of bound.");
		return pathPoints.get(i);
	}

	public void setPoints(List<Point2D.Float> p) {
		if (p.size() < 3)
			throw new IllegalArgumentException("the number of points must be no less than 3.");
		if (points == null || p.size() != points.length)
			path = new GeneralPath();
		points = new Point2D.Float[p.size()];
		for (int i = 0; i < points.length; i++) {
			Point2D.Float pi = p.get(i);
			setPoint(i, pi.x, pi.y);
		}
		update();
	}

	public void setPoint(int i, float x, float y) {
		if (i < 0 || i >= points.length)
			throw new IllegalArgumentException("index is out of bound.");
		if (points[i] == null)
			points[i] = new Point2D.Float(x, y);
		else
			points[i].setLocation(x, y);
	}

	public Point2D.Float getPoint(int i) {
		if (i < 0 || i >= points.length)
			throw new IllegalArgumentException("index is out of bound.");
		return points[i];
	}

	public int getPointCount() {
		return points.length;
	}

	public void translateBy(float dx, float dy) {
		for (Point2D.Float p : points) {
			p.x += dx;
			p.y += dy;
		}
	}

	public void rotateBy(float degree) {
		Rectangle2D r = path.getBounds2D();
		double cx = r.getCenterX();
		double cy = r.getCenterY();
		double a = Math.toRadians(degree);
		double sin = Math.sin(a);
		double cos = Math.cos(a);
		double dx = 0;
		double dy = 0;
		for (Point2D.Float v : points) {
			dx = v.x - cx;
			dy = v.y - cy;
			v.x = (float) (dx * cos - dy * sin + cx);
			v.y = (float) (dx * sin + dy * cos + cy);
		}
	}

	public void scale(float scale) {
		Rectangle2D r = path.getBounds2D();
		double cx = r.getCenterX();
		double cy = r.getCenterY();
		for (Point2D.Float v : points) {
			v.x = (float) ((v.x - cx) * scale + cx);
			v.y = (float) ((v.y - cy) * scale + cy);
		}
	}

	public void scaleX(float scale) {
		Rectangle2D r = path.getBounds2D();
		double cx = r.getCenterX();
		for (Point2D.Float v : points) {
			v.x = (float) ((v.x - cx) * scale + cx);
		}
	}

	public void scaleY(float scale) {
		Rectangle2D r = path.getBounds2D();
		double cy = r.getCenterY();
		for (Point2D.Float v : points) {
			v.y = (float) ((v.y - cy) * scale + cy);
		}
	}

	public void shearX(float shear) {
		Rectangle2D r = path.getBounds2D();
		double cy = r.getCenterY();
		for (Point2D.Float v : points) {
			v.x += (float) (v.y - cy) * shear;
		}
	}

	public void shearY(float shear) {
		Rectangle2D r = path.getBounds2D();
		double cx = r.getCenterX();
		for (Point2D.Float v : points) {
			v.y += (float) (v.x - cx) * shear;
		}
	}

	public void flipX() {
		float cx = (float) path.getBounds2D().getCenterX();
		float dx = 0;
		for (Point2D.Float v : points) {
			dx = v.x - cx;
			v.x = cx - dx;
		}
	}

	public void flipY() {
		float cy = (float) path.getBounds2D().getCenterY();
		float dy = 0;
		for (Point2D.Float v : points) {
			dy = v.y - cy;
			v.y = cy - dy;
		}
	}

	public boolean contains(Point2D p) {
		return contains(p.getX(), p.getY());
	}

	public boolean intersects(Rectangle r) {
		return path.intersects(r);
	}

	public boolean contains(double x, double y) {
		return path.contains(x, y);
	}

	public Point2D.Float getBoundCenter() {
		Rectangle2D r = path.getBounds2D();
		return new Point2D.Float((float) r.getCenterX(), (float) r.getCenterY());
	}

	public void translateCenterTo(float x, float y) {
		Point2D.Float center = getCenter();
		translateBy(x - center.x, y - center.y);
	}

	public Point2D.Float getCenter() {
		float xc = 0;
		float yc = 0;
		for (Point2D.Float v : points) {
			xc += v.x;
			yc += v.y;
		}
		return new Point2D.Float(xc / points.length, yc / points.length);
	}

	// TODO: Should use the smooth path
	public float getArea() {
		float area = 0;
		int n = points.length;
		Point2D.Float v1, v2;
		for (int i = 0; i < n - 1; i++) {
			v1 = points[i];
			v2 = points[i + 1];
			area += v1.getX() * v2.getY() - v2.getX() * v1.getY();
		}
		v1 = points[n - 1];
		v2 = points[0];
		area += v1.getX() * v2.getY() - v2.getX() * v1.getY();
		return area * 0.5f;
	}

	public Rectangle getBounds() {
		return path.getBounds();
	}

	public Rectangle2D getBounds2D() {
		return path.getBounds2D();
	}

	public boolean contains(Rectangle2D r) {
		return path.contains(r);
	}

	public boolean contains(double x, double y, double w, double h) {
		return path.contains(x, y, w, h);
	}

	public PathIterator getPathIterator(AffineTransform at) {
		return path.getPathIterator(at);
	}

	public PathIterator getPathIterator(AffineTransform at, double flatness) {
		return path.getPathIterator(at, flatness);
	}

	public boolean intersects(Rectangle2D r) {
		return path.intersects(r);
	}

	public boolean intersects(double x, double y, double w, double h) {
		return path.intersects(x, y, w, h);
	}

	// Catmull-Rom spline function
	private static float catmullrom(int i, float u) {
		switch (i) {
		case -2:
			return u * (u * (2 - u) - 1);
		case -1:
			return u * u * (3 * u - 5) + 2;
		case 0:
			return u * ((4 - 3 * u) * u + 1);
		case 1:
			return u * u * (u - 1);
		}
		return 0;
	}

}