/*-
 *  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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;

import org.citygml4j.core.model.construction.AbstractConstructionSurface;
import org.citygml4j.core.model.construction.WallSurface;
import org.citygml4j.core.util.geometry.GeometryFactory;
import org.junit.Test;

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.ResultStatus;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType;
import de.hft.stuttgart.citydoctor2.math.Vector3d;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;

/**
 * 
 * @author Matthias Betz
 *
 */
public class GeometryTest {

	@Test
	public void testCreate() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD0);

		assertNotNull(geom);
		assertSame(GeometryType.SOLID, geom.getType());
		assertSame(Lod.LOD0, geom.getLod());
	}

	@Test
	public void testAddPolygon() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD0);

		Polygon p = new ConcretePolygon();
		geom.addPolygon(p);

		assertEquals(1, geom.getPolygons().size());
		assertSame(geom, p.getParent());
		assertSame(geom.getPolygons().get(0), p);
	}

	@Test
	public void testRemovePolygonConcreteWithLink() {
		ParserConfiguration config = new ParserConfiguration(4, false);
		AbstractConstructionSurface abs = new WallSurface();
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, BoundarySurfaceType.WALL, abs);
		var gmlBi = new org.citygml4j.core.model.building.BuildingInstallation();
		Installation bi = new Installation();
		bi.setGmlObject(gmlBi);
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);

		ConcretePolygon p = new ConcretePolygon();
		LinearRing lr = new LinearRing(LinearRingType.EXTERIOR);
		p.setExteriorRing(lr);
		lr.addVertex(new Vertex(0, 0, 0));

		geom.addPolygon(p);
		p.setPartOfSurface(bs);
		p.setPartOfInstallation(bi);

		bs.addGeometry(geom);

		Geometry biGeom = new Geometry(GeometryType.COMPOSITE_SURFACE, Lod.LOD2);
		biGeom.addPolygon(new LinkedPolygon(p, biGeom));
		bi.addGeometry(biGeom);

		geom.removePolygon(p);

		assertEquals(0, geom.getPolygons().size());
		assertEquals(0, biGeom.getPolygons().size());

		bs.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(abs.getLod2MultiSurface());

		bi.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(gmlBi.getDeprecatedProperties().getLod2Geometry());
	}

	@Test
	public void testRemovePolygonOnlyConcrete() {
		ParserConfiguration config = new ParserConfiguration(4, false);
		AbstractConstructionSurface abs = new WallSurface();
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, BoundarySurfaceType.WALL, abs);
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);

		ConcretePolygon p = new ConcretePolygon();
		LinearRing lr = new LinearRing(LinearRingType.EXTERIOR);
		p.setExteriorRing(lr);
		lr.addVertex(new Vertex(0, 0, 0));

		geom.addPolygon(p);
		p.setPartOfSurface(bs);

		bs.addGeometry(geom);

		geom.removePolygon(p);

		bs.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(abs.getLod2MultiSurface());
	}

	@Test
	public void testRemovePolygonLinked() {
		ParserConfiguration config = new ParserConfiguration(4, false);
		AbstractConstructionSurface abs = new WallSurface();
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, BoundarySurfaceType.WALL, abs);
		var gmlBi = new org.citygml4j.core.model.building.BuildingInstallation();
		Installation bi = new Installation();
		bi.setGmlObject(gmlBi);
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);

		ConcretePolygon p = new ConcretePolygon();
		LinearRing lr = new LinearRing(LinearRingType.EXTERIOR);
		p.setExteriorRing(lr);
		lr.addVertex(new Vertex(0, 0, 0));
		bs.addGeometry(geom);

		geom.addPolygon(p);
		p.setPartOfSurface(bs);
		p.setPartOfInstallation(bi);

		Geometry biGeom = new Geometry(GeometryType.COMPOSITE_SURFACE, Lod.LOD2);
		LinkedPolygon linkedPolygon = new LinkedPolygon(p, biGeom);
		biGeom.addPolygon(linkedPolygon);
		bi.addGeometry(biGeom);

		biGeom.removePolygon(linkedPolygon);

		assertEquals(0, geom.getPolygons().size());
		assertEquals(0, biGeom.getPolygons().size());

		bs.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(abs.getLod2MultiSurface());

		bi.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(gmlBi.getDeprecatedProperties().getLod2Geometry());
	}

	@Test
	public void testReplacePolygon() {
		ParserConfiguration config = new ParserConfiguration(4, false);
		AbstractConstructionSurface abs = new WallSurface();
		BoundarySurface bs = new BoundarySurface(SurfaceFeatureType.BUILDING, BoundarySurfaceType.WALL, abs);
		var gmlBi = new org.citygml4j.core.model.building.BuildingInstallation();
		Installation bi = new Installation();
		bi.setGmlObject(gmlBi);
		bi.addBoundarySurface(bs);
		Geometry geom2 = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);
		ConcretePolygon p = new ConcretePolygon();
		geom2.addPolygon(p);
		LinearRing lr = new LinearRing(LinearRingType.EXTERIOR);
		p.setExteriorRing(lr);
		lr.addVertex(new Vertex(0, 0, 0));
		lr.addVertex(new Vertex(1, 0, 0));
		lr.addVertex(new Vertex(2, 0, 0));
		lr.addVertex(new Vertex(0, 0, 0));
		p.setPartOfSurface(bs);
		p.setPartOfInstallation(bi);
		bs.addGeometry(geom2);

		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD2);
		Polygon linkedPoly = new LinkedPolygon(p, geom);
		geom.addPolygon(linkedPoly);

		ConcretePolygon p2 = new ConcretePolygon();
		p2.setExteriorRing(lr);

		ConcretePolygon p3 = new ConcretePolygon();
		p3.setExteriorRing(lr);

		geom.replacePolygon(linkedPoly, p2, p3);

		assertEquals(2, geom.getPolygons().size());
		assertEquals(2, geom2.getPolygons().size());

		bi.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNull(gmlBi.getDeprecatedProperties().getLod2Geometry());
		assertNull(gmlBi.getDeprecatedProperties().getLod3Geometry());
		assertNull(gmlBi.getDeprecatedProperties().getLod4Geometry());

		bs.reCreateGeometries(GeometryFactory.newInstance(), config);
		assertNotNull(abs.getLod2MultiSurface());
		assertNotNull(abs.getLod2MultiSurface().getObject());
		assertNotNull(abs.getLod2MultiSurface().getObject().getSurfaceMember());
		assertFalse(abs.getLod2MultiSurface().getObject().getSurfaceMember().isEmpty());
		assertEquals(2, abs.getLod2MultiSurface().getObject().getSurfaceMember().size());
		assertNull(gmlBi.getDeprecatedProperties().getLod2Geometry());
	}

	@Test
	public void testSetParent() {
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);
		CityObject co = mock(CityObject.class);
		geom.setParent(co);
		assertEquals(co, geom.getParent());
	}

	@Test
	public void testUpdateEdges() {
		Geometry geom = GeometryTestUtils.createDummyGeometryWithInnerRingWithNeighboringPolygon(GeometryType.SOLID,
				Lod.LOD2);
		geom.clearMetaInformation();
		assertNull(geom.getEdges());
		geom.updateEdges();
		List<Edge> edges = geom.getEdges();
		assertNotNull(edges);
		assertFalse(edges.isEmpty());
		assertEquals(10, edges.size());
	}

	@Test
	public void testSetType() {
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, Lod.LOD2);
		geom.setType(GeometryType.SOLID);
		assertEquals(GeometryType.SOLID, geom.getType());
	}

	@Test
	public void testGetCenter() {
		Geometry geom = GeometryTestUtils.createDummyGeometry(GeometryType.SOLID);
		Vector3d center = geom.getCenter();
		assertEquals(427583.3025, center.getX(), 0.000001);
		assertEquals(6003502.5725, center.getY(), 0.000001);
		assertEquals(6.905, center.getZ(), 0.000001);
	}

	@Test
	public void testContainsError() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1);
		assertFalse(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		geom.addCheckResult(new CheckResult(CheckId.C_GE_P_HOLE_OUTSIDE, ResultStatus.ERROR, mock(CheckError.class)));
		assertTrue(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		ConcretePolygon p = new ConcretePolygon();
		p.setExteriorRing(mock(LinearRing.class));
		geom.addPolygon(p);
		assertFalse(geom.containsError(CheckId.C_GE_P_INNER_RINGS_NESTED));
		p.addCheckResult(
				new CheckResult(CheckId.C_GE_P_INNER_RINGS_NESTED, ResultStatus.ERROR, mock(CheckError.class)));
		assertTrue(geom.containsError(CheckId.C_GE_P_INNER_RINGS_NESTED));
	}

	@Test
	public void testClearAllContainedCheckResults() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1);
		assertFalse(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		geom.addCheckResult(new CheckResult(CheckId.C_GE_P_HOLE_OUTSIDE, ResultStatus.ERROR, mock(CheckError.class)));
		assertTrue(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		Polygon p = mock(Polygon.class);
		geom.addPolygon(p);
		geom.clearAllContainedCheckResults();
		verify(p).clearAllContainedCheckResults();
		assertFalse(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
	}

	@Test
	public void testCollectContainedErrors() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1);
		assertFalse(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		geom.addCheckResult(new CheckResult(CheckId.C_GE_P_HOLE_OUTSIDE, ResultStatus.ERROR, mock(CheckError.class)));
		assertTrue(geom.containsError(CheckId.C_GE_P_HOLE_OUTSIDE));
		Polygon p = mock(Polygon.class);
		geom.addPolygon(p);
		List<CheckError> errors = new ArrayList<>();
		geom.collectContainedErrors(errors);
		verify(p).collectContainedErrors(errors);
		assertEquals(1, errors.size());
	}

	@Test
	public void testContainsAnyError() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1);
		assertFalse(geom.containsAnyError());
		geom.addCheckResult(new CheckResult(CheckId.C_GE_P_HOLE_OUTSIDE, ResultStatus.ERROR, mock(CheckError.class)));
		assertTrue(geom.containsAnyError());
		Polygon p = mock(Polygon.class);
		geom.addPolygon(p);
		geom.clearAllContainedCheckResults();
		assertFalse(geom.containsAnyError());
		verify(p).containsAnyError();
		when(p.containsAnyError()).thenReturn(true);
		assertTrue(geom.containsAnyError());
	}

	@Test
	public void testAccept() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD1);
		Check c = mock(Check.class);
		geom.accept(c);
		when(c.canExecute(geom)).thenReturn(true);
		geom.accept(c);
		verify(c).check(geom);
		Polygon p = mock(Polygon.class);
		geom.addPolygon(p);
		geom.accept(c);
		verify(p).accept(c);
	}

	@Test
	public void testGetVertices() {
		Geometry geom = GeometryTestUtils.createDummyGeometry(GeometryType.SOLID);
		List<Vertex> vertices = geom.getVertices();
		assertEquals(4, vertices.size());
	}

	@Test
	public void testGetEdgesAdjacentToVertex() {
		Geometry geom = GeometryTestUtils.createDummyGeometryWithInnerRingWithNeighboringPolygon(GeometryType.SOLID,
				Lod.LOD1);
		Polygon polygon = geom.getPolygons().get(1);
		Vertex vertex = polygon.getExteriorRing().getVertices().get(0);
		List<Edge> edges = geom.getEdgesAdjacentTo(vertex);
		assertEquals(3, edges.size());
	}

	@Test
	public void testGetEdge() {
		Geometry geom = GeometryTestUtils.createDummyGeometry(GeometryType.SOLID);
		Polygon poly = geom.getPolygons().get(0);
		LinearRing ext = poly.getExteriorRing();
		Vertex v0 = ext.getVertices().get(0);
		Vertex v1 = ext.getVertices().get(1);
		Edge e = geom.getEdge(v0, v1);
		assertNotNull(e);
		assertEquals(v0, e.getFrom());
		assertEquals(v1, e.getTo());

		Edge e2 = geom.getEdge(v1, v0);
		assertEquals(e, e2);

		Vertex v2 = ext.getVertices().get(2);
		Edge e3 = geom.getEdge(v0, v2);
		assertNull(e3);
	}

	@Test
	public void testUpdateVertices() {
		Geometry geom = GeometryTestUtils.createDummyGeometryWithInnerRingWithNeighboringPolygon(GeometryType.SOLID,
				Lod.LOD0);
		geom.updateVertices();
		Polygon polygon = geom.getPolygons().get(0);
		Vertex v1 = polygon.getExteriorRing().getVertices().get(0);
		assertTrue(v1.getAdjacentRings(geom).contains(geom.getPolygons().get(0).getExteriorRing()));
		assertTrue(v1.getAdjacentRings(geom).contains(geom.getPolygons().get(1).getExteriorRing()));
		assertEquals(9, geom.getVertices().size());
	}

	@Test
	public void testGetEdgesAdjacentToPolygon() {
		Geometry geom = GeometryTestUtils.createDummyGeometryWithInnerRingWithNeighboringPolygon(GeometryType.SOLID,
				Lod.LOD0);
		Polygon polygon = geom.getPolygons().get(0);

		List<Edge> edges = new ArrayList<>();
		for (int i = 0; i < polygon.getExteriorRing().getVertices().size() - 1; i++) {
			Vertex v1 = polygon.getExteriorRing().getVertices().get(i);
			Vertex v2 = polygon.getExteriorRing().getVertices().get(i + 1);
			Edge e = geom.getEdge(v1, v2);
			edges.add(e);
		}

		for (LinearRing lr : polygon.getInnerRings()) {
			for (int i = 0; i < lr.getVertices().size() - 1; i++) {
				Vertex v1 = lr.getVertices().get(i);
				Vertex v2 = lr.getVertices().get(i + 1);
				Edge e = geom.getEdge(v1, v2);
				edges.add(e);
			}
		}
		List<Edge> edgesAdjacentTo = geom.getEdgesAdjacentTo(polygon);
		for (Edge e : edges) {
			assertTrue(edgesAdjacentTo.contains(e));
		}
	}

	@Test
	public void testContainsPolygon() {
		Geometry geom = new Geometry(GeometryType.SOLID, Lod.LOD0);
		ConcretePolygon cdPoly = new ConcretePolygon();
		geom.addPolygon(cdPoly);
		cdPoly.setGmlId(new GmlId("test1"));

		ConcretePolygon cdPoly2 = new ConcretePolygon();
		geom.addPolygon(cdPoly2);
		cdPoly2.setGmlId(new GmlId("test2"));

		ConcretePolygon cdPoly3 = new ConcretePolygon();
		cdPoly3.setGmlId(new GmlId("test1"));

		ConcretePolygon cdPoly4 = new ConcretePolygon();
		cdPoly4.setGmlId(new GmlId("test3"));

		assertTrue(geom.containsPolygon(cdPoly3));
		assertFalse(geom.containsPolygon(cdPoly4));
	}

	@Test
	public void testClearMetaInformation() {
		Geometry geom = GeometryTestUtils.createDummyGeometryWithInnerRingWithNeighboringPolygon(GeometryType.SOLID,
				Lod.LOD0);
		assertNotNull(geom.getVertices());
		geom.clearMetaInformation();
		assertNull(geom.getVertices());
		assertNull(geom.getEdges());
		geom.clearMetaInformation();
		assertNull(geom.getVertices());
		assertNull(geom.getEdges());
	}

}
