/*-
 *  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.util.Arrays;

import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;

/**
 * This class is needed for projecting rings or polygons to a 2D axis plane. It
 * uses the longest component of the normal vector to determine which plane is
 * the best plane to project the ring or polygon on while conserving the most
 * area of the original polygon.
 * 
 * @author Matthias Betz
 *
 */
public class ProjectionAxis {
	
	private static final String DIVISOR_IS_0 = "Divisor is 0";

	private int[] axis;

	public static ProjectionAxis of(Polygon p) {
		return getProjectionAxis(p.calculateNormal());
	}

	public static ProjectionAxis of(MovedRing movedRing) {
		// the normal should be the same even if the ring has moved
		return of(movedRing.getOriginal());
	}

	public static ProjectionAxis of(LinearRing lr) {
		return getProjectionAxis(lr.calculateNormal());
	}

	public static ProjectionAxis of(Plane plane) {
		return getProjectionAxis(plane.getNormal());
	}

	/**
	 * Calculates the indices of the two lowest coordinate values of this vector.
	 * This is used to determine on which axis plane a ring or polygon should be
	 * projected.
	 * 
	 * @param normal the normal vector, does not need to be normalized
	 * @return an array with the indices of the coordinate array which should be
	 *         used in a 2d environment.
	 */
	private static ProjectionAxis getProjectionAxis(Vector3d normal) {
		double nx = Math.abs(normal.getX());
		double ny = Math.abs(normal.getY());
		double nz = Math.abs(normal.getZ());
		double biggestValue = nx;
		int[] axis = new int[] { 1, 2 };
		if (ny > biggestValue) {
			biggestValue = ny;
			axis[0] = 0;
			axis[1] = 2;
		}
		if (nz > biggestValue) {
			axis[0] = 0;
			axis[1] = 1;
		}
		return new ProjectionAxis(axis);
	}

	private ProjectionAxis(int[] axis) {
		this.axis = axis;
	}

	public Vector2d project(Vector3d v) {
		return new Vector2d(v.getCoordinate(axis[0]), v.getCoordinate(axis[1]));
	}

	/**
	 * calculates the missing coordinate for 3d vector from the plane and this axis.
	 * @return the projected 3d point.
	 */
	public Vector3d projectToPlane(Plane plane, Vector2d v) {
		return projectToPlane(plane, v.getX(), v.getY());
	}
	
	/**
	 * calculates the missing coordinate for 3d vector from the plane and this axis.
	 * @return the projected 3d point.
	 */
	public Vector3d projectToPlane(Plane plane, double vectorX, double vectorY) {
		UnitVector3d normal = plane.getNormal();
		double a = normal.getX();
		double b = normal.getY();
		double c = normal.getZ();
		double d = plane.getD();
		if (axis[0] == 0 && axis[1] == 1) {
			double x = vectorX;
			double y = vectorY;
			if (c == 0) {
				throw new IllegalStateException(DIVISOR_IS_0);
			}
			double z = (d - a * x - b * y) / c;
			return new Vertex(x, y, z);
		} else if (axis[0] == 0 && axis[1] == 2) {
			double x = vectorX;
			double z = vectorY;
			if (b == 0) {
				throw new IllegalStateException(DIVISOR_IS_0);
			}
			double y = (d - a * x - c * z) / b;
			return new Vertex(x, y, z);
		} else if (axis[0] == 1 && axis[1] == 2) {
			double y = vectorX;
			double z = vectorY;
			if (a == 0) {
				throw new IllegalStateException(DIVISOR_IS_0);
			}
			double x = (d - b * y - c * z) / a;
			return new Vertex(x, y, z);
		} else {
			throw new IllegalStateException("Unknown axis: " + axis);
		}
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(axis);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ProjectionAxis other = (ProjectionAxis) obj;
		return Arrays.equals(axis, other.axis);
	}

	@Override
	public String toString() {
		return "ProjectionAxis [axis=" + Arrays.toString(axis) + "]";
	}

}
