/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.math;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.math.PlaneSegmentIntersection.Type;

/**
 * A plane described by a normal vector and a point
 * 
 * @author Matthias Betz
 *
 */
public class Plane implements Serializable {

	private static final long serialVersionUID = 1602547402497943364L;

	private static final double EPSILON = 0.0001;
	
	private UnitVector3d normal;
	private Vector3d point;
	private double d;

	/**
	 * Constructs a new plane from a normal vector and a reference point.
	 * 
	 * @param normal the normal vector
	 * @param p      the reference point
	 */
	public Plane(Vector3d normal, Vector3d p) {
		point = p;
		this.normal = normal.normalize();
		d = this.normal.dot(point);
		if (d < 0) {
			this.normal = this.normal.invert();
			d = -d;
		}
	}
	
	public PlaneSegmentIntersection intersects(Segment3d s) {
		return intersects(s, EPSILON);
	}
	
	public PlaneSegmentIntersection intersects(Segment3d s, double intersectionEpsilon) {
		Vector3d dir = s.getPointB().minus(s.getPointA());
		Vector3d w = s.getPointA().minus(point);
		
		double a = normal.dot(dir);
		double n = -1 * normal.dot(w);
		
		if (Math.abs(a) < intersectionEpsilon) {
			// parallel
			if (n == 0) {
				return new PlaneSegmentIntersection(Type.IN_PLANE);
			} else {
				return new PlaneSegmentIntersection(Type.NO_INTERSECTION);
			}
		}
		
		// not parallel
		double si = n / a;
		if (si < intersectionEpsilon || si > 1 - intersectionEpsilon) {
			// intersection not in segment
			return new PlaneSegmentIntersection(Type.NO_INTERSECTION);
		}
		
		// intersection in segment
		// calculate intersection point
		Vector3d intersection = s.getPointA().plus(dir.mult(si));
		PlaneSegmentIntersection psi = new PlaneSegmentIntersection(Type.INTERSECTION_POINT);
		psi.setPoint(intersection);
		return psi;
	}

	/**
	 * calculate the distance of this plane to the given point.
	 * 
	 * @param point the point
	 * @return the distance
	 */
	public double getDistance(Vector3d point) {
		return Math.abs(getSignedDistance(point));
	}
	
	/**
	 * Calculates the signed distance of the point to this plane.
	 * @param point the point
	 * @return the signed distance
	 */
	public double getSignedDistance(Vector3d point) {
		return normal.dot(point) - d;
	}
	
	public double getD() {
		return d;
	}

	/**
	 * returns the intersection point of the line perpendicular to the plane through
	 * the given point.
	 * 
	 * @param point the point to project
	 * @return the projected point
	 */
	public Vector3d projectPointToPlane(Vector3d point) {
		double a = d - normal.dot(point);
		double x = point.getX() + a * normal.getX();
		double y = point.getY() + a * normal.getY();
		double z = point.getZ() + a * normal.getZ();
		return new Vector3d(x, y, z);
	}

	/**
	 * Getter for normal
	 * 
	 * @return the normal vector of the plane
	 */
	public UnitVector3d getNormal() {
		return normal;
	}

	/**
	 * Getter for point
	 * 
	 * @return the reference point of the plane
	 */
	public Vector3d getPoint() {
		return point;
	}

	public static Plane of(Polygon b) {
		List<Vertex> vertices = new ArrayList<>(b.getExteriorRing().getVertices());
		for (LinearRing inner : b.getInnerRings()) {
			vertices.addAll(inner.getVertices());
		}
		return OrthogonalRegressionPlane.calculateOrthogonalRegressionPlane(vertices);
	}
	
	public static Plane of(MovedPolygon p) {
		List<Vector3d> vertices = new ArrayList<>(p.getExteriorRing().getVertices());
		for (MovedRing inner : p.getInnerRings()) {
			vertices.addAll(inner.getVertices());
		}
		return OrthogonalRegressionPlane.calculateOrthogonalRegressionPlane(vertices);
	}
}
