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

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

	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<>();
	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);
	}

	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) {
		// 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);
				for (Vertex v0Neighbor : v0.getNeighbors()) {
					Edge alternativeEdge = new Edge(v0Neighbor, v1);
					Edge oppositeAlternativeEdge = new Edge(v1, v0Neighbor);
					duplicacyMap.put(alternativeEdge, tempEdge);
					duplicacyMap.put(oppositeAlternativeEdge, tempEdge);
					for (Vertex v1Neighbor : v1.getNeighbors()) {
						alternativeEdge = new Edge(v0Neighbor, v1Neighbor);
						oppositeAlternativeEdge = new Edge(v1Neighbor, v0Neighbor);
						duplicacyMap.put(alternativeEdge, tempEdge);
						duplicacyMap.put(oppositeAlternativeEdge, tempEdge);
					}
				}
				for (Vertex v : v1.getNeighbors()) {
					Edge alternativeEdge = new Edge(v0, v);
					Edge oppositeAlternativeEdge = new Edge(v, v0);
					duplicacyMap.put(alternativeEdge, tempEdge);
					duplicacyMap.put(oppositeAlternativeEdge, 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 void setVertices(List<Vertex> vertices) {
		this.vertices = 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) {
		Edge edge = edgeMap.get(new SerializablePair<>(v1, v2));
		if (edge != null) {
			return edge;
		}
		for (Vertex neighborV1 : v1.getNeighbors()) {
			edge = edgeMap.get(new SerializablePair<>(neighborV1, v2));
			if (edge != null) {
				return edge;
			}
			for (Vertex neighborV2 : v2.getNeighbors()) {
				edge = edgeMap.get(new SerializablePair<>(neighborV1, neighborV2));
				if (edge != null) {
					return edge;
				}
			}
		}
		for (Vertex neighborV2 : v2.getNeighbors()) {
			edge = edgeMap.get(new SerializablePair<>(neighborV2, v1));
			if (edge != null) {
				return edge;
			}
		}
		return null;
	}

	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) {
		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);
		}
	}

	public void removePolygon(Polygon p) {
		p.removeRings();
		polygons.remove(p);
		if (p.isLink()) {
			Polygon original = p.getOriginal();
			original.getParent().polygons.remove(original);
		} else if (p.isLinkedTo()) {
			Polygon link = p.getLinkedFromPolygon();
			link.getParent().polygons.remove(link);
		}
	}

	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, this));
			}
		} 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 Geometry copy() {
		return (Geometry) super.copy();
	}
	
	@Override
	public void prepareForChecking() {
		updateEdgesAndVertices();
	}
	
	@Override
	public void clearMetaInformation() {
		if (vertices != null) {
			for (Vertex v : vertices) {
				v.clearAdjacentRings();
			}
			vertices = null;
		}
		edges = null;
		edgeMap = null;
	}
}
