/*-
 *  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 de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;

/**
 * A three dimensional triangle described by three points
 * 
 * @author Matthias Betz
 *
 */
public class Triangle3d implements Serializable {

	private static final long serialVersionUID = -6907333357794272435L;

	private static final double EPSILON = 0.0001;
	private Vector3d p1;
	private Vector3d p2;
	private Vector3d p3;

	private TesselatedPolygon partOf;

	public Triangle3d(Vector3d p1, Vector3d p2, Vector3d p3) {
		this(p1, p2, p3, null);
	}

	public Triangle3d(Vector3d p1, Vector3d p2, Vector3d p3, TesselatedPolygon partOf) {
		this.p1 = p1;
		this.p2 = p2;
		this.p3 = p3;
		this.partOf = partOf;
	}

	public TesselatedPolygon getPartOf() {
		return partOf;
	}

	public Vector3d getNormal() {
		Vector3d side1 = p3.minus(p1);
		Vector3d side2 = p2.minus(p1);
		return side1.cross(side2);
	}

	public Vector3d getCentroid() {
		double meanX = (p1.getX() + p2.getX() + p3.getX()) / 3d;
		double meanY = (p1.getY() + p2.getY() + p3.getY()) / 3d;
		double meanZ = (p1.getZ() + p2.getZ() + p3.getZ()) / 3d;
		return new Vector3d(meanX, meanY, meanZ);
	}

	public double getArea() {
		double a = p1.getDistance(p2);
		double b = p2.getDistance(p3);
		double c = p1.getDistance(p3);
		double s = 0.5d * (a + b + c);
		return Math.sqrt(s * (s - a) * (s - b) * (s - c));
	}

	public boolean hasPointAsCorner(Vector3d p) {
		return p1.equals(p) || p2.equals(p) || p3.equals(p);
	}

	public Plane getPlane() {
		Vector3d v1 = p1.minus(p3);
		Vector3d v2 = p2.minus(p3);
		Vector3d n2 = v1.cross(v2);
		return new Plane(n2, p1);
	}

	public boolean doesIntersect(Triangle3d other) {
		// plane of other triangle
		Plane planeT2 = other.getPlane();
		// check if all points are on one side of the plane
		double distanceP1T2 = planeT2.getSignedDistance(p1);
		double distanceP2T2 = planeT2.getSignedDistance(p2);
		double distanceP3T2 = planeT2.getSignedDistance(p3);

		if (Math.abs(distanceP1T2) < EPSILON) {
			distanceP1T2 = 0.0;
		}
		if (Math.abs(distanceP2T2) < EPSILON) {
			distanceP2T2 = 0.0;
		}
		if (Math.abs(distanceP3T2) < EPSILON) {
			distanceP3T2 = 0.0;
		}

		if ((distanceP1T2 * distanceP2T2 >= 0.0) && (distanceP1T2 * distanceP3T2 >= 0.0)) {
			if (distanceP1T2 == 0.0 && distanceP2T2 == 0.0 && distanceP3T2 == 0.0) {
				return doesIntersectCoplanarTriangle(other);
			} else {
				// points are not coplanar
				// all points are on one side -> no intersection possible
				return false;
			}
		}

		// plane of this triangle
		Plane planeT1 = getPlane();
		// check if all points are on one side of the plane
		double distanceP1T1 = planeT1.getSignedDistance(other.getP1());
		double distanceP2T1 = planeT1.getSignedDistance(other.getP2());
		double distanceP3T1 = planeT1.getSignedDistance(other.getP3());

		if (Math.abs(distanceP1T1) < EPSILON) {
			distanceP1T1 = 0.0;
		}
		if (Math.abs(distanceP2T1) < EPSILON) {
			distanceP2T1 = 0.0;
		}
		if (Math.abs(distanceP3T1) < EPSILON) {
			distanceP3T1 = 0.0;
		}

		if (((distanceP1T1 * distanceP2T1) >= 0.0) && ((distanceP1T1 * distanceP3T1) >= 0.0)) {
			// all points are on one side -> no intersection possible
			// coplanar check not necessary, already done previously
			return false;
		}

		return checkTriangleLineIntersection(other, p1, p2) || checkTriangleLineIntersection(other, p1, p3)
				|| checkTriangleLineIntersection(other, p2, p3)
				|| checkTriangleLineIntersection(this, other.p1, other.p2)
				|| checkTriangleLineIntersection(this, other.p1, other.p3)
				|| checkTriangleLineIntersection(this, other.p2, other.p3);
	}

	private boolean checkTriangleLineIntersection(Triangle3d other, Vector3d a, Vector3d b) {
		Segment3d seg = new Segment3d(a, b);
		Vector3d intersection1 = seg.intersection(other);
		return intersection1 != null;
	}

	public boolean containsPoint(Vector3d point) {
		Vector3d a = p1;
		Vector3d b = p2;
		Vector3d c = p3;
		Vector3d p = point;
		return sameSide(p, a, b, c) && sameSide(p, b, a, c) && sameSide(p, c, a, b);
	}

	private boolean sameSide(Vector3d p1, Vector3d p2, Vector3d a, Vector3d b) {
		Vector3d dir = b.minus(a);
		Vector3d test1 = p1.minus(a);
		Vector3d test2 = p2.minus(a);
		Vector3d c1 = dir.cross(test1);
		Vector3d c2 = dir.cross(test2);
		return c1.dot(c2) > 0;
	}

	private boolean doesIntersectCoplanarTriangle(Triangle3d other) {
		Triangle2d t1 = projectTo2d();
		Triangle2d t2 = other.projectTo2d();
		return t1.intersects(t2);
	}

	public Triangle2d projectTo2d() {
		Vector3d normal = getNormal();
		double x = Math.abs(normal.getX());
		double y = Math.abs(normal.getY());
		double z = Math.abs(normal.getZ());
		if (x > y && x > z) {
			// x is the largest value -> project to y-z plane
			Vector2d a = new Vector2d(p1.getY(), p1.getZ());
			Vector2d b = new Vector2d(p2.getY(), p2.getZ());
			Vector2d c = new Vector2d(p3.getY(), p3.getZ());
			return new Triangle2d(a, b, c);
		} else if (y > x && y > z) {
			// y is the largest value -> project to x-z plane
			Vector2d a = new Vector2d(p1.getX(), p1.getZ());
			Vector2d b = new Vector2d(p2.getX(), p2.getZ());
			Vector2d c = new Vector2d(p3.getX(), p3.getZ());
			return new Triangle2d(a, b, c);
		} else {
			// z is the largest value -> project to x-y plane
			Vector2d a = new Vector2d(p1.getX(), p1.getY());
			Vector2d b = new Vector2d(p2.getX(), p2.getY());
			Vector2d c = new Vector2d(p3.getX(), p3.getY());
			return new Triangle2d(a, b, c);
		}
	}

	public Vector3d getP1() {
		return p1;
	}

	public Vector3d getP2() {
		return p2;
	}

	public Vector3d getP3() {
		return p3;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((p1 == null) ? 0 : p1.hashCode());
		result = prime * result + ((p2 == null) ? 0 : p2.hashCode());
		result = prime * result + ((p3 == null) ? 0 : p3.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (getClass() != obj.getClass()) {
			return false;
		}
		Triangle3d other = (Triangle3d) obj;
		if (p1 == null) {
			if (other.p1 != null) {
				return false;
			}
		} else if (!p1.equals(other.p1)) {
			return false;
		}
		if (p2 == null) {
			if (other.p2 != null) {
				return false;
			}
		} else if (!p2.equals(other.p2)) {
			return false;
		}
		if (p3 == null) {
			if (other.p3 != null) {
				return false;
			}
		} else if (!p3.equals(other.p3)) {
			return false;
		}
		return true;
	}

	@Override
	public String toString() {
		return "Triangle3d [p1=" + p1 + ", p2=" + p2 + ", p3=" + p3 + "]";
	}

	public void setPartOf(TesselatedPolygon p) {
		partOf = p;
	}

}
