/*-
 *  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.checks.geometry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import Jama.EigenvalueDecomposition;
import de.hft.stuttgart.citydoctor2.check.Check;
import de.hft.stuttgart.citydoctor2.check.CheckError;
import de.hft.stuttgart.citydoctor2.check.CheckId;
import de.hft.stuttgart.citydoctor2.check.CheckResult;
import de.hft.stuttgart.citydoctor2.check.CheckType;
import de.hft.stuttgart.citydoctor2.check.Checkable;
import de.hft.stuttgart.citydoctor2.check.DefaultParameter;
import de.hft.stuttgart.citydoctor2.check.ResultStatus;
import de.hft.stuttgart.citydoctor2.check.Unit;
import de.hft.stuttgart.citydoctor2.check.error.DistanceError;
import de.hft.stuttgart.citydoctor2.check.error.NormalDeviationError;
import de.hft.stuttgart.citydoctor2.check.error.TinyEdgeError;
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.CovarianceMatrix;
import de.hft.stuttgart.citydoctor2.math.OrthogonalRegressionPlane;
import de.hft.stuttgart.citydoctor2.math.Plane;
import de.hft.stuttgart.citydoctor2.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.tesselation.JoglTesselator;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;

/**
 * Check class to check for planarity issues
 * 
 * @author Matthias Betz
 *
 */
public class PlanarCheck extends Check {

	private static final String DISTANCE = "distance";
	private static final String DELTA_NAME = "distanceTolerance";
	private static final String RADIANT = "angleTolerance";
	private static final String TYPE = "type";
	private static final String TINY_EDGE_TOLERANCE = "tinyEdgeTolerance";

	private static final List<CheckId> dependencies;
	private static final List<Class<? extends Checkable>> applicableToClasses;
	private static final List<DefaultParameter> defaultParameters;

	static {
		ArrayList<CheckId> deps = new ArrayList<>(4);
		deps.add(CheckId.C_GE_R_TOO_FEW_POINTS);
		deps.add(CheckId.C_GE_R_NOT_CLOSED);
		deps.add(CheckId.C_GE_R_DUPLICATE_POINT);
		deps.add(CheckId.C_GE_R_SELF_INTERSECTION);
		dependencies = Collections.unmodifiableList(deps);

		ArrayList<Class<? extends Checkable>> classes = new ArrayList<>(1);
		classes.add(Polygon.class);
		applicableToClasses = Collections.unmodifiableList(classes);

		ArrayList<DefaultParameter> defParameters = new ArrayList<>(3);
		defParameters.add(new DefaultParameter(TYPE, DISTANCE, Unit.NONE));
		defParameters.add(new DefaultParameter(DELTA_NAME, "0.01", Unit.METER));
		defParameters.add(new DefaultParameter(RADIANT, "0.1", Unit.RADIAN));
		defParameters.add(new DefaultParameter(TINY_EDGE_TOLERANCE, "0.00000", Unit.METER));
		defaultParameters = Collections.unmodifiableList(defParameters);

	}

	private String planarCheckType = DISTANCE;

	private double rad = 0.1;
	private double delta = 0.01;
	private double tinyEdgeTolerance = 0.00000;

	public PlanarCheck() {
		super(CheckId.C_GE_P_NON_PLANAR);
	}

	@Override
	public void init(Map<String, String> parameters, ParserConfiguration config) {
		if (parameters.containsKey(TYPE)) {
			planarCheckType = parameters.get(TYPE);
		} else {
			planarCheckType = DISTANCE;
		}
		if (parameters.containsKey(RADIANT)) {
			rad = Double.parseDouble(parameters.get(RADIANT));
		} else {
			rad = 0.1;
		}
		if (parameters.containsKey(DELTA_NAME)) {
			delta = Double.parseDouble(parameters.get(DELTA_NAME));
		} else {
			delta = 0.01;
		}
		if (parameters.containsKey(TINY_EDGE_TOLERANCE)) {
			tinyEdgeTolerance = Double.parseDouble(parameters.get(TINY_EDGE_TOLERANCE));
		} else {
			tinyEdgeTolerance = 0.00002;
		}
	}

	@Override
	public List<DefaultParameter> getDefaultParameter() {
		return defaultParameters;
	}

	@Override
	public void check(Polygon p) {
		if (DISTANCE.equals(planarCheckType)) {
			planarDistance(p);
		} else if ("angle".equals(planarCheckType)) {
			// check for tiny edge as well
			// store all used points in temporary list
			ArrayList<Vertex> vertices = collectVertices(p);
			Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
			EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
			Vector3d eigenvalues = OrthogonalRegressionPlane.getEigenvalues(ed);
			if (checkEigenvalues(p, eigenvalues)) {
				// found tiny edge error, abort further checking
				return;
			}
			planarNormalDeviation(p);
		} else if ("both".equals(planarCheckType)) {
			planarDistance(p);
			planarNormalDeviation(p);
		} else {
			throw new IllegalStateException("Illegal planar check type was given: " + planarCheckType
					+ "\nChoose one of: distance, angle, both");
		}
	}

	private void planarNormalDeviation(Polygon p) {
		TesselatedPolygon tp = JoglTesselator.tesselatePolygon(p);
		ArrayList<Vector3d> normals = new ArrayList<>();
		for (Triangle3d t : tp.getTriangles()) {
			Vector3d normal = t.getNormal();
			normals.add(normal);
		}
		Vector3d averageNormal = calculateAverageNormal(normals);
		averageNormal.normalize();
		for (Vector3d normal : normals) {
			normal.normalize();
			double deviation = normal.dot(averageNormal);
			double radiant = Math.acos(deviation);
			if (radiant > rad) {
				CheckError err = new NormalDeviationError(p, radiant);
				CheckResult cr = new CheckResult(this, ResultStatus.ERROR, err);
				p.addCheckResult(cr);
				return;
			}
		}
		CheckResult cr = p.getCheckResult(this.getCheckId());
		// only change check result if missing, otherwise might override error from
		// distance check
		if (cr == null) {
			p.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
		}
	}

	private Vector3d calculateAverageNormal(ArrayList<Vector3d> normals) {
		double x = 0D;
		double y = 0D;
		double z = 0D;
		for (Vector3d normal : normals) {
			x += normal.getX();
			y += normal.getY();
			z += normal.getZ();
		}
		x = x / normals.size();
		y = y / normals.size();
		z = z / normals.size();
		return new Vector3d(x, y, z);
	}

	private void planarDistance(Polygon p) {
		// store all used points in temporary list
		ArrayList<Vertex> vertices = collectVertices(p);
		Vector3d centroid = CovarianceMatrix.getCentroid(vertices);
		EigenvalueDecomposition ed = OrthogonalRegressionPlane.decompose(vertices, centroid);
		Vector3d eigenvalues = OrthogonalRegressionPlane.getEigenvalues(ed);

		if (checkEigenvalues(p, eigenvalues)) {
			// found tiny edge error, abort further checking
			return;
		}

		Plane plane = OrthogonalRegressionPlane.calculatePlane(centroid, ed, eigenvalues);
		for (Vertex v : vertices) {
			double distance = plane.getDistance(v);
			if (distance > delta) {
				CheckError err = new DistanceError(p, distance, v, plane);
				p.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
				return;
			}
		}
		CheckResult cr = p.getCheckResult(this.getCheckId());
		// only change check result if missing
		if (cr == null) {
			p.addCheckResult(new CheckResult(this, ResultStatus.OK, null));
		}
	}

	private ArrayList<Vertex> collectVertices(Polygon p) {
		ArrayList<Vertex> vertices = new ArrayList<>();
		// only go to n - 1 points, because last point = first point
		for (int i = 0; i < p.getExteriorRing().getVertices().size() - 1; i++) {
			Vertex v = p.getExteriorRing().getVertices().get(i);
			vertices.add(v);
		}
		for (LinearRing lr : p.getInnerRings()) {
			for (int i = 0; i < lr.getVertices().size() - 1; i++) {
				Vertex v = lr.getVertices().get(i);
				vertices.add(v);
			}
		}
		return vertices;
	}

	private boolean checkEigenvalues(Polygon p, Vector3d eigenvalues) {
		int nrOfEigenvaluesBelowTolerance = 0;
		for (double d : eigenvalues.getCoordinates()) {
			if (d <= tinyEdgeTolerance) {
				nrOfEigenvaluesBelowTolerance++;
			}
		}
		if (nrOfEigenvaluesBelowTolerance >= 2) {
			CheckError err = new TinyEdgeError(p);
			p.addCheckResult(new CheckResult(this, ResultStatus.ERROR, err));
			return true;
		}
		return false;
	}

	@Override
	public List<Class<? extends Checkable>> getApplicableToClasses() {
		return applicableToClasses;
	}

	@Override
	public List<CheckId> getDependencies() {
		return dependencies;
	}

	@Override
	public CheckType getType() {
		return CheckType.GEOMETRY;
	}

	@Override
	public Check createNewInstance() {
		return new PlanarCheck();
	}
}
