/*
 * Copyright (c) 2016 Martin Davis.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */

package org.locationtech.jts.operation.distance3d;

import org.locationtech.jts.algorithm.RayCrossingCounter;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.math.Plane3D;
import org.locationtech.jts.math.Vector3D;

/**
 * Models a polygon lying in a plane in 3-dimensional Cartesian space.
 * The polygon representation is supplied
 * by a {@link Polygon},
 * containing coordinates with XYZ ordinates.
 * 3D polygons are assumed to lie in a single plane.
 * The plane best fitting the polygon coordinates is
 * computed and is represented by a {@link Plane3D}.
 * 
 * @author mdavis
 *
 */
public class PlanarPolygon3D {

	private Plane3D plane;
	private Polygon poly;
	private int facingPlane = -1;

	public PlanarPolygon3D(Polygon poly) {
		this.poly = poly;
		plane = findBestFitPlane(poly);
		facingPlane = plane.closestAxisPlane();
	}

	/**
	 * Finds a best-fit plane for the polygon, 
	 * by sampling a few points from the exterior ring.
	 * <p>
	 * The algorithm used is Newell's algorithm:
	 * - a base point for the plane is determined from the average of all vertices
	 * - the normal vector is determined by
	 *   computing the area of the projections on each of the axis planes
	 * 
	 * @param poly the polygon to determine the plane for
	 * @return the best-fit plane
	 */
	private Plane3D findBestFitPlane(Polygon poly) 
	{
		CoordinateSequence seq = poly.getExteriorRing().getCoordinateSequence();
		Coordinate basePt = averagePoint(seq);
		Vector3D normal = averageNormal(seq);
		return new Plane3D(normal, basePt);
	}

	/**
	 * Computes an average normal vector from a list of polygon coordinates.
	 * Uses Newell's method, which is based
	 * on the fact that the vector with components
	 * equal to the areas of the projection of the polygon onto 
	 * the Cartesian axis planes is normal.
	 * 
	 * @param seq the sequence of coordinates for the polygon
	 * @return a normal vector
	 */
	private Vector3D averageNormal(CoordinateSequence seq) 
	{
		int n = seq.size();
		Coordinate sum = new Coordinate(0,0,0);
		Coordinate p1 = new Coordinate(0,0,0);
		Coordinate p2 = new Coordinate(0,0,0);
		for (int i = 0; i < n - 1; i++) {
			seq.getCoordinate(i, p1);
			seq.getCoordinate(i+1, p2);
			sum.x += (p1.y - p2.y)*(p1.getZ() + p2.getZ());
			sum.y += (p1.getZ() - p2.getZ())*(p1.x + p2.x);
			sum.setZ(sum.getZ() + (p1.x - p2.x)*(p1.y + p2.y));
		}
		sum.x /= n;
		sum.y /= n;
		sum.setZ(sum.getZ() / n);
		Vector3D norm = Vector3D.create(sum).normalize();
		return norm;
	}

	/**
	 * Computes a point which is the average of all coordinates
	 * in a sequence.
	 * If the sequence lies in a single plane,
	 * the computed point also lies in the plane.
	 * 
	 * @param seq a coordinate sequence
	 * @return a Coordinate with averaged ordinates
	 */
	private Coordinate averagePoint(CoordinateSequence seq) {
		Coordinate a = new Coordinate(0,0,0);
		int n = seq.size();
		for (int i = 0; i < n; i++) {
			a.x += seq.getOrdinate(i, CoordinateSequence.X);
			a.y += seq.getOrdinate(i, CoordinateSequence.Y);
			a.setZ(a.getZ() + seq.getOrdinate(i, CoordinateSequence.Z));
		}
		a.x /= n;
		a.y /= n;
		a.setZ(a.getZ() / n);
		return a;
	}

	public Plane3D getPlane() {
		return plane;
	}

	public Polygon getPolygon() {
		return poly;
	}

	public boolean intersects(Coordinate intPt) {
		if (Location.EXTERIOR == locate(intPt, poly.getExteriorRing()))
			return false;
		
		for (int i = 0; i < poly.getNumInteriorRing(); i++) {
			if (Location.INTERIOR == locate(intPt, poly.getInteriorRingN(i)))
				return false;
		}
		return true;
	}

	private int locate(Coordinate pt, LineString ring) {
		CoordinateSequence seq = ring.getCoordinateSequence();
		CoordinateSequence seqProj = project(seq, facingPlane);
		Coordinate ptProj = project(pt, facingPlane);
		return RayCrossingCounter.locatePointInRing(ptProj, seqProj);
	}
	
	public boolean intersects(Coordinate pt, LineString ring) {
		CoordinateSequence seq = ring.getCoordinateSequence();
		CoordinateSequence seqProj = project(seq, facingPlane);
		Coordinate ptProj = project(pt, facingPlane);
		return Location.EXTERIOR != RayCrossingCounter.locatePointInRing(ptProj, seqProj);
	}
	
	private static CoordinateSequence project(CoordinateSequence seq, int facingPlane)
	{
		switch (facingPlane) {
		case Plane3D.XY_PLANE: return AxisPlaneCoordinateSequence.projectToXY(seq);
		case Plane3D.XZ_PLANE: return AxisPlaneCoordinateSequence.projectToXZ(seq);
		default: return AxisPlaneCoordinateSequence.projectToYZ(seq);
		}
	}
	
	private static Coordinate project(Coordinate p, int facingPlane)
	{
		switch (facingPlane) {
		case Plane3D.XY_PLANE: return new Coordinate(p.x, p.y);
		case Plane3D.XZ_PLANE: return new Coordinate(p.x, p.getZ());
		// Plane3D.YZ
		default: return new Coordinate(p.y, p.getZ());
		}
	}
	

}
