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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

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.math.Triangle3d;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.tesselation.TesselatedPolygon;
import de.hft.stuttgart.citydoctor2.utils.BoundingBoxCalculator;
import de.hft.stuttgart.citydoctor2.utils.CopyHandler;
import de.hft.stuttgart.citydoctor2.utils.Copyable;
import de.hft.stuttgart.citydoctor2.utils.SerializablePair;

/**
 * Representation of a geometry containing the polygons and edges
 * 
 * @author Matthias Betz
 *
 */
public class Geometry extends GmlElement {

	private static final String FAILED_REMOVING_POLYGON = "Removing polygon %s but polygon is not in geometry";

	private static final long serialVersionUID = 2539031030917731575L;

	private static Random r = new Random();

	private GeometryType type;
	private Lod lod;

	private CityObject parent;

	private List<Polygon> polygons = new ArrayList<>(2);
	private List<Edge> edges;
	private Map<SerializablePair<Vertex, Vertex>, Edge> edgeMap;
	private List<Vertex> vertices;

	public Geometry(GeometryType type, Lod lod) {
		this.lod = lod;
		this.type = type;
	}

	void setParent(CityObject parent) {
		this.parent = parent;
	}

	public CityObject getParent() {
		return parent;
	}

	public void setType(GeometryType type) {
		this.type = type;
	}

	public GeometryType getType() {
		return type;
	}

	public Lod getLod() {
		return lod;
	}

	public List<Polygon> getPolygons() {
		return polygons;
	}

	public void addPolygon(Polygon cdPoly) {
		polygons.add(cdPoly);
		cdPoly.setParent(this);
		// set part of boundary surface or building installation or both
		if (parent instanceof BoundarySurface bs) {
			cdPoly.setPartOfSurface(bs);
			if (bs.getParent() instanceof Installation bi) {
				cdPoly.setPartOfInstallation(bi);
			}
		} else if (parent instanceof Installation bi) {
			cdPoly.setPartOfInstallation(bi);
		}
	}

	public void updateEdges() {
		edges = new ArrayList<>();
		edgeMap = new HashMap<>();
		Map<Edge, Edge> duplicacyMap = new HashMap<>();
		for (Polygon p : polygons) {
			LinearRing extRing = p.getExteriorRing();
			createEdgesFromRing(duplicacyMap, p, extRing);
			for (LinearRing inner : p.getInnerRings()) {
				createEdgesFromRing(duplicacyMap, p, inner);
			}
		}
	}

	void anonymize() {
		setGmlId(GmlId.generateId());
		for (Polygon p : polygons) {
			p.anonymize();
		}
		double minX = Double.MAX_VALUE;
		double minY = Double.MAX_VALUE;
		double minZ = Double.MAX_VALUE;
		for (Vertex v : vertices) {
			if (v.getX() < minX) {
				minX = v.getX();
			}
			if (v.getY() < minY) {
				minY = v.getY();
			}
			if (v.getZ() < minZ) {
				minZ = v.getZ();
			}
		}
		double varX = r.nextDouble() * 10;
		double varY = r.nextDouble() * 10;
		double varZ = r.nextDouble() * 10;
		for (Vertex v : vertices) {
			v.setX(v.getX() - minX + varX);
			v.setY(v.getY() - minY + varY);
			v.setZ(v.getZ() - minZ + varZ);
		}
	}

	private void createEdgesFromRing(Map<Edge, Edge> duplicacyMap, Polygon p, LinearRing ring) {
		if (ring == null) {
			return;
		}
		// only go to size -1 as ring should be closed
		for (int i = 0; i < ring.getVertices().size() - 1; i++) {
			Vertex v0 = ring.getVertices().get(i + 0);
			Vertex v1 = ring.getVertices().get(i + 1);
			Edge tempEdge = new Edge(v0, v1);
			Edge e = duplicacyMap.get(tempEdge);
			if (e == null) {
				// no duplicate found, enter edges
				Edge oppositeEdge = new Edge(v1, v0);
				duplicacyMap.put(tempEdge, tempEdge);
				duplicacyMap.put(oppositeEdge, tempEdge);
				e = tempEdge;
				edges.add(e);
				edgeMap.put(new SerializablePair<>(v0, v1), e);
				edgeMap.put(new SerializablePair<>(v1, v0), e);
			} else {
				e.addHalfEdge(v0, v1);
			}
			e.addAdjacentPolygon(p);
			e.addAdjacentRing(ring);
		}
	}

	public Vector3d getCenter() {
		return calculateBoundingBox().getCenter();
	}

	/**
	 * This will calculate two points where every other point in the geometry is
	 * between those two. If the geometry does not contain points or is null, an
	 * empty array is returned. This is considered an axis aligned bounding box.
	 * Only the exterior rings of the polygons is used as inner rings should be
	 * within the exterior or they are faulty.
	 * 
	 * @return an array of two points. The first contains the lowest coordinates,
	 *         the second contains the highest coordinates
	 */
	public BoundingBox calculateBoundingBox() {
		return BoundingBoxCalculator.calculateBoundingBox(polygons);
	}

	public double calculateVolume() {
		double sum = 0;
		Vector3d center = calculateBoundingBox().getCenter();
		for (Polygon p : polygons) {
			TesselatedPolygon tesselatedPolygon = p.tesselate();
			for (Triangle3d t : tesselatedPolygon.getTriangles()) {
				Vector3d p1 = t.getP1().minus(center);
				Vector3d p2 = t.getP2().minus(center);
				Vector3d p3 = t.getP3().minus(center);
				sum += det(p1.getCoordinates(), p2.getCoordinates(), p3.getCoordinates());
			}
		}
		return sum / 6d;
	}

	private double det(double[] a, double[] b, double[] c) {
		return -c[0] * b[1] * a[2] + b[0] * c[1] * a[2] + c[0] * a[1] * b[2] - a[0] * c[1] * b[2] - b[0] * a[1] * c[2]
				+ a[0] * b[1] * c[2];
	}

	@Override
	public boolean containsError(CheckId checkIdentifier) {
		boolean hasError = super.containsError(checkIdentifier);
		if (hasError) {
			return true;
		}
		for (Polygon p : polygons) {
			if (p.containsError(checkIdentifier)) {
				return true;
			}
		}
		return false;
	}

	@Override
	public void clearAllContainedCheckResults() {
		super.clearCheckResults();
		for (Polygon p : polygons) {
			p.clearAllContainedCheckResults();
		}
	}

	@Override
	public void collectContainedErrors(List<CheckError> errors) {
		super.collectContainedErrors(errors);
		for (Polygon p : polygons) {
			p.collectContainedErrors(errors);
		}
	}

	@Override
	public boolean containsAnyError() {
		boolean hasError = super.containsAnyError();
		if (hasError) {
			return true;
		}
		for (Polygon p : polygons) {
			if (p.containsAnyError()) {
				return true;
			}
		}
		return false;
	}

	@Override
	public void accept(Check c) {
		super.accept(c);
		if (c.canExecute(this)) {
			c.check(this);
		}
		for (Polygon p : polygons) {
			p.accept(c);
		}
	}

	@Override
	public String toString() {
		return "Geometry [type=" + type + ", lod=" + lod + ", id=" + getGmlId() + "]";
	}

	public List<Vertex> getVertices() {
		return vertices;
	}

	public List<Edge> getEdges() {
		return edges;
	}

	public List<Edge> getEdgesAdjacentTo(Vertex v) {
		List<Edge> adjacentEdges = new ArrayList<>();
		for (Edge e : edges) {
			if (e.getFrom() == v || e.getTo() == v) {
				adjacentEdges.add(e);
			}
		}
		return adjacentEdges;
	}

	public Edge getEdge(Vertex v1, Vertex v2) {
		return edgeMap.get(new SerializablePair<>(v1, v2));
	}

	public void updateEdgesAndVertices() {
		updateVertices();
		updateEdges();
	}

	public void updateVertices() {
		Set<Vertex> vertexSet = new HashSet<>();
		for (Polygon p : getPolygons()) {
			updateRing(vertexSet, p.getExteriorRing());
			for (LinearRing inner : p.getInnerRings()) {
				updateRing(vertexSet, inner);
			}
		}
		vertices = new ArrayList<>(vertexSet);
	}

	private void updateRing(Set<Vertex> vertexSet, LinearRing ring) {
		if (ring == null) {
			return;
		}
		for (Vertex v : ring.getVertices()) {
			if (vertexSet.add(v)) {
				// new vertex, clear adjacent rings in case new rings have been added
				v.clearAdjacentRings(this);
			}
			// add ring to adjacent rings of vertex
			v.addAdjacentRing(ring, this);
		}
	}

	void removePolygon(Polygon p) {
		p.removeRings();
		if (!polygons.remove(p)) {
			throw new IllegalStateException(String.format(FAILED_REMOVING_POLYGON, p));
		}
		if (p.isLink()) {
			Polygon original = p.getOriginal();
			if (!original.getParent().polygons.remove(original)) {
				throw new IllegalStateException(String.format(FAILED_REMOVING_POLYGON, p));
			}
		} else if (p.isLinkedTo()) {
			Polygon link = p.getLinkedFromPolygon();
			if (!link.getParent().polygons.remove(link)) {
				throw new IllegalStateException(String.format(FAILED_REMOVING_POLYGON, p));
			}
		}
	}

	public void replacePolygon(Polygon p, ConcretePolygon... polygons) {
		removePolygon(p);
		if (p.isLink()) {
			Geometry geom2 = p.getOriginal().getParent();
			for (ConcretePolygon newPoly : polygons) {
				geom2.addPolygon(newPoly);
				addPolygon(new LinkedPolygon(newPoly, this));
			}
		} else if (p.isLinkedTo()) {
			Geometry geom2 = p.getLinkedFromPolygon().getParent();
			for (ConcretePolygon newPoly : polygons) {
				addPolygon(newPoly);
				geom2.addPolygon(new LinkedPolygon(newPoly, geom2));
			}
		} else {
			for (Polygon newPoly : polygons) {
				addPolygon(newPoly);
			}
		}

	}

	public List<Edge> getEdgesAdjacentTo(Polygon p) {
		List<Edge> list = new ArrayList<>();
		for (Edge e : edges) {
			if (e.getAdjacentPolygons().contains(p)) {
				list.add(e);
			}
		}
		return list;
	}

	public boolean containsPolygon(Polygon other) {
		for (Polygon p : polygons) {
			if (p.getGmlId().equals(other.getGmlId())) {
				return true;
			}
		}
		return false;
	}

	@Override
	public void prepareForChecking() {
		updateEdgesAndVertices();
	}

	@Override
	public void clearMetaInformation() {
		if (vertices != null) {
			for (Vertex v : vertices) {
				v.clearAdjacentRings();
			}
			vertices = null;
		}
		edges = null;
		edgeMap = null;
	}

	@Override
	public void collectInstances(CopyHandler handler) {
		handler.addInstance(parent);
		handler.addInstance(polygons);
		handler.addInstance(edges);
		handler.addInstance(vertices);
		if (edgeMap != null) {
			for (Entry<SerializablePair<Vertex, Vertex>, Edge> e : edgeMap.entrySet()) {
				handler.addInstance(e.getKey().getValue0());
				handler.addInstance(e.getKey().getValue1());
				handler.addInstance(e.getValue());
			}
		}
	}

	@Override
	public Copyable createCopyInstance() {
		return new Geometry(type, lod);
	}

	@Override
	public void fillValues(Copyable original, CopyHandler handler) {
		super.fillValues(original, handler);
		Geometry originalGeometry = (Geometry) original;
		for (Polygon originalPoly : originalGeometry.polygons) {
			polygons.add(handler.getCopyInstance(originalPoly));
		}
		if (originalGeometry.edges != null) {
			edges = new ArrayList<>(originalGeometry.edges.size());
			for (Edge originalEdge : originalGeometry.edges) {
				edges.add(handler.getCopyInstance(originalEdge));
			}
		}
		if (originalGeometry.edgeMap != null) {
			edgeMap = new HashMap<>(originalGeometry.edgeMap.size());
			for (Entry<SerializablePair<Vertex, Vertex>, Edge> originalEntry : originalGeometry.edgeMap.entrySet()) {
				Vertex copyV0 = handler.getCopyInstance(originalEntry.getKey().getValue0());
				Vertex copyV1 = handler.getCopyInstance(originalEntry.getKey().getValue1());
				SerializablePair<Vertex, Vertex> pair = new SerializablePair<>(copyV0, copyV1);
				Edge copyEdge = handler.getCopyInstance(originalEntry.getValue());
				edgeMap.put(pair, copyEdge);
			}
		}
		if (originalGeometry.vertices != null) {
			vertices = new ArrayList<>(originalGeometry.vertices.size());
			for (Vertex originalVertex : originalGeometry.vertices) {
				vertices.add(handler.getCopyInstance(originalVertex));
			}
		}
		parent = handler.getCopyInstance(originalGeometry.parent);

	}

}
