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

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.bridge.AbstractBridge;
import org.citygml4j.core.model.bridge.Bridge;
import org.citygml4j.core.model.bridge.BridgeConstructiveElementProperty;
import org.citygml4j.core.model.bridge.BridgePart;
import org.citygml4j.core.model.bridge.BridgePartProperty;
import org.citygml4j.core.model.building.BuildingInstallationProperty;
import org.citygml4j.core.model.building.BuildingPartProperty;
import org.citygml4j.core.model.construction.AbstractConstruction;
import org.citygml4j.core.model.core.AbstractCityObject;
import org.citygml4j.core.model.core.AbstractFeatureWithLifespan;
import org.citygml4j.core.model.core.AbstractOccupiedSpace;
import org.citygml4j.core.model.core.AbstractPhysicalSpace;
import org.citygml4j.core.model.core.AbstractSpace;
import org.citygml4j.core.model.core.AbstractSpaceBoundary;
import org.citygml4j.core.model.core.AbstractSpaceBoundaryProperty;
import org.citygml4j.core.model.core.AbstractThematicSurface;
import org.citygml4j.core.model.core.AbstractUnoccupiedSpace;
import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.core.model.deprecated.transportation.TransportationComplex;
import org.citygml4j.core.model.landuse.LandUse;
import org.citygml4j.core.model.transportation.AbstractTransportationSpace;
import org.citygml4j.core.model.transportation.AuxiliaryTrafficArea;
import org.citygml4j.core.model.transportation.AuxiliaryTrafficSpace;
import org.citygml4j.core.model.transportation.AuxiliaryTrafficSpaceProperty;
import org.citygml4j.core.model.transportation.Railway;
import org.citygml4j.core.model.transportation.Road;
import org.citygml4j.core.model.transportation.Square;
import org.citygml4j.core.model.transportation.Track;
import org.citygml4j.core.model.transportation.TrafficArea;
import org.citygml4j.core.model.transportation.TrafficSpace;
import org.citygml4j.core.model.transportation.TrafficSpaceProperty;
import org.citygml4j.core.model.vegetation.AbstractVegetationObject;
import org.citygml4j.core.model.vegetation.PlantCover;
import org.citygml4j.core.model.vegetation.SolitaryVegetationObject;
import org.citygml4j.core.model.waterbody.WaterBody;
import org.citygml4j.core.visitor.ObjectWalker;
import org.xmlobjects.gml.model.base.AbstractGML;
import org.xmlobjects.gml.model.feature.AbstractFeature;
import org.xmlobjects.gml.model.geometry.AbstractGeometry;
import org.xmlobjects.gml.model.geometry.GeometryProperty;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
import org.xmlobjects.gml.model.geometry.primitives.AbstractSolid;
import org.xmlobjects.gml.model.geometry.primitives.AbstractSurface;
import org.xmlobjects.gml.model.geometry.primitives.Shell;
import org.xmlobjects.gml.model.geometry.primitives.ShellProperty;
import org.xmlobjects.gml.model.geometry.primitives.Solid;
import org.xmlobjects.gml.model.geometry.primitives.SolidProperty;
import org.xmlobjects.gml.model.geometry.primitives.SurfaceProperty;

import de.hft.stuttgart.citydoctor2.datastructure.AbstractBuilding;
import de.hft.stuttgart.citydoctor2.datastructure.BoundarySurface;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeConstructiveElement;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject;
import de.hft.stuttgart.citydoctor2.datastructure.BridgeObject.BridgeType;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingInstallation;
import de.hft.stuttgart.citydoctor2.datastructure.BuildingPart;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.CityObject;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
import de.hft.stuttgart.citydoctor2.datastructure.GeometryType;
import de.hft.stuttgart.citydoctor2.datastructure.GmlElement;
import de.hft.stuttgart.citydoctor2.datastructure.GmlId;
import de.hft.stuttgart.citydoctor2.datastructure.LandObject;
import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.LinkedPolygon;
import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject;
import de.hft.stuttgart.citydoctor2.datastructure.TransportationObject.TransportationType;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation;
import de.hft.stuttgart.citydoctor2.datastructure.Vegetation.VegetationType;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.datastructure.WaterObject;
import de.hft.stuttgart.citydoctor2.math.graph.KDTree;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.Localization;

public class Citygml3FeatureMapper extends ObjectWalker {

	private static final Logger logger = LogManager.getLogger(Citygml3FeatureMapper.class);

	private CityDoctorModel model;

	private Map<String, ConcretePolygon> polygonMap = new HashMap<>();
	private List<ResolvableReference> references = new ArrayList<>();
	private Map<Vertex, Vertex> vertexMap = new HashMap<>();
	private ParserConfiguration config;

	private double neighborDistance;

	public Citygml3FeatureMapper(ParserConfiguration config, Path path) {
		this.config = config;
		model = new CityDoctorModel(config, path.toFile());
		neighborDistance = 1.8d / Math.pow(10, config.getNumberOfRoundingPlaces());
	}

	@Override
	public void visit(AbstractSpace space) {
		// if we are here, an AbstractSpace thing was read that is not handled in the
		// other methods

	}

	@Override
	public void visit(org.citygml4j.core.model.building.Building gmlBuilding) {
		Building cdBuilding = new Building();
		readAbstractBuilding(gmlBuilding, cdBuilding);
		for (BuildingPartProperty bpProp : gmlBuilding.getBuildingParts()) {
			if (!bpProp.isSetObject()) {
				continue;
			}
			BuildingPart part = new BuildingPart(cdBuilding);
			readAbstractBuilding(bpProp.getObject(), part);
		}
		model.addBuilding(cdBuilding);
	}

	@Override
	public void visit(WaterBody waterBody) {
		WaterObject wo = new WaterObject();
		wo.setGmlObject(waterBody);
		mapAbstractOccupiedSpace(waterBody, wo);
		parseAndAddMultiSurface(waterBody.getDeprecatedProperties().getLod1MultiSurface(), Lod.LOD1, wo);
		parseAndAddSolid(waterBody.getDeprecatedProperties().getLod4Solid(), Lod.LOD4, wo);

		SurfaceMapper surfaceMapper = new SurfaceMapper(polygonMap, references, vertexMap, config);
		for (AbstractSpaceBoundaryProperty surfaceProp : waterBody.getBoundaries()) {
			if (!surfaceProp.isSetObject()) {
				continue;
			}
			AbstractSpaceBoundary surface = surfaceProp.getObject();
			surface.accept(surfaceMapper);
		}
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			wo.addBoundarySurface(bs);
		}

		resolveAndClearReferences();
		wo.unsetGmlGeometries();
		updateEdgesAndVertices(wo);
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			updateEdgesAndVertices(bs);
		}
		model.addWater(wo);
	}

	@Override
	public void visit(LandUse landUse) {
		LandObject lo = new LandObject(landUse);

		mapAbstractThematicSurface(landUse, lo);

		finishCityObjectConstruction(lo);
		model.addLand(lo);
	}

	@Override
	public void visit(PlantCover plantCover) {
		Vegetation veg = new Vegetation(VegetationType.PLANT_COVER);
		veg.setGmlObject(plantCover);
		mapAbstractVegetationObject(plantCover, veg);

		parseAndAddMultiSurface(plantCover.getDeprecatedProperties().getLod1MultiSurface(), Lod.LOD1, veg);
		parseAndAddMultiSurface(plantCover.getDeprecatedProperties().getLod4MultiSurface(), Lod.LOD4, veg);

		finishCityObjectConstruction(veg);
		model.addVegetation(veg);
	}
	
	@Override
	public void visit(SolitaryVegetationObject solitaryVegetationObject) {
		Vegetation veg = new Vegetation(VegetationType.SOLITARY_VEGETATION_OBJECT);
		veg.setGmlObject(solitaryVegetationObject);
		mapAbstractVegetationObject(solitaryVegetationObject, veg);
		
		parseAndAddAbstractGeometry(solitaryVegetationObject.getDeprecatedProperties().getLod1Geometry(), Lod.LOD1, veg);
		parseAndAddAbstractGeometry(solitaryVegetationObject.getDeprecatedProperties().getLod2Geometry(), Lod.LOD2, veg);
		parseAndAddAbstractGeometry(solitaryVegetationObject.getDeprecatedProperties().getLod3Geometry(), Lod.LOD3, veg);
		parseAndAddAbstractGeometry(solitaryVegetationObject.getDeprecatedProperties().getLod4Geometry(), Lod.LOD4, veg);

		finishCityObjectConstruction(veg);
		model.addVegetation(veg);
	}
	
	private void mapAbstractVegetationObject(AbstractVegetationObject avo, Vegetation veg) {
		mapAbstractOccupiedSpace(avo, veg);
	}

	private void mapAbstractOccupiedSpace(AbstractOccupiedSpace aos, CityObject co) {
		mapAbstractPhysicalSpace(aos, co);
	}

	private void mapAbstractPhysicalSpace(AbstractPhysicalSpace aps, CityObject co) {
		mapAbstractSpace(aps, co);
	}

	private void mapAbstractSpace(AbstractSpace as, CityObject co) {
		mapAbstractFeature(as, co);
		parseAndAddMultiSurface(as.getLod0MultiSurface(), Lod.LOD0, co);
		parseAndAddMultiSurface(as.getLod2MultiSurface(), Lod.LOD2, co);
		parseAndAddMultiSurface(as.getLod3MultiSurface(), Lod.LOD3, co);
		parseAndAddSolid(as.getLod1Solid(), Lod.LOD1, co);
		parseAndAddSolid(as.getLod2Solid(), Lod.LOD2, co);
		parseAndAddSolid(as.getLod3Solid(), Lod.LOD3, co);
	}

	@Override
	public void visit(Bridge bridge) {
		BridgeObject bo = new BridgeObject(BridgeType.BRIDGE, bridge);
		for (BridgePartProperty bPartProperty : bridge.getBridgeParts()) {
			if (!bPartProperty.isSetObject()) {
				continue;
			}
			BridgePart gmlBridgePart = bPartProperty.getObject();
			BridgeObject bPart = new BridgeObject(BridgeType.BRIDGE_PART, gmlBridgePart);
			mapAbstractBridge(gmlBridgePart, bPart);
			bo.addBridgePart(bPart);
		}
		mapAbstractBridge(bridge, bo);
		resolveAndClearReferences();
		updateEdgesAndVertices(bo);
		for (BridgeObject part : bo.getParts()) {
			updateEdgesAndVertices(part);
		}
		for (BridgeConstructiveElement ele : bo.getConstructiveElements()) {
			updateEdgesAndVertices(ele);
		}
		model.addBridge(bo);
	}

	private void mapAbstractBridge(AbstractBridge ab, BridgeObject bo) {
		mapAbstractConstruction(ab, bo);
		for (BridgeConstructiveElementProperty eleProp : ab.getBridgeConstructiveElements()) {
			if (!eleProp.isSetObject()) {
				continue;
			}
			org.citygml4j.core.model.bridge.BridgeConstructiveElement constructiveElement = eleProp.getObject();
			BridgeConstructiveElement cdEle = new BridgeConstructiveElement(constructiveElement);
			mapConstructiveElement(constructiveElement, cdEle);
			bo.addConstructiveElement(cdEle);
		}
		
		
		SurfaceMapper surfaceMapper = new SurfaceMapper(polygonMap, references, vertexMap, config);
		for (AbstractSpaceBoundaryProperty surfaceProp : ab.getBoundaries()) {
			if (!surfaceProp.isSetObject()) {
				continue;
			}
			AbstractSpaceBoundary surface = surfaceProp.getObject();
			surface.accept(surfaceMapper);
		}
		updatePartOfSurface(bo, surfaceMapper);
		
		for (BoundarySurface bs : bo.getBoundarySurfaces()) {
			updateEdgesAndVertices(bs);
		}
		
		bo.unsetGmlGeometries();
	}

	private void updatePartOfSurface(BridgeObject bo, SurfaceMapper surfaceMapper) {
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			bo.addBoundarySurface(bs);
			for (Geometry geom : bs.getGeometries()) {
				for (Polygon p : geom.getPolygons()) {
					p.setPartOfSurface(bs);
				}
			}
		}
	}

	private void mapConstructiveElement(org.citygml4j.core.model.bridge.BridgeConstructiveElement ele, BridgeConstructiveElement bce) {
		mapAbstractConstructiveElement(ele, bce);
	}

	private void mapAbstractConstructiveElement(org.citygml4j.core.model.bridge.BridgeConstructiveElement ele, BridgeConstructiveElement bce) {
		mapAbstractOccupiedSpace(ele, bce);
	}

	private void mapAbstractConstruction(AbstractConstruction ac, BridgeObject bo) {
		mapAbstractOccupiedSpace(ac, bo);
	}

	@Override
	public void visit(AuxiliaryTrafficArea ata) {
		TransportationObject to = new TransportationObject(TransportationType.AUXILLIARY_TRAFFIC_AREA);
		mapAbstractThematicSurface(ata, to);
		finishTransportationMapping(to);
	}

	@Override
	public void visit(TrafficArea ta) {
		TransportationObject to = new TransportationObject(TransportationType.TRAFFIC_AREA);
		mapAbstractThematicSurface(ta, to);
		finishTransportationMapping(to);
	}

	@Override
	public void visit(Road road) {
		TransportationObject to = new TransportationObject(TransportationType.ROAD);
		mapAbstractTransportationSpace(road, to);
		finishTransportationMapping(to);
	}

	private void mapAbstractThematicSurface(AbstractThematicSurface ats, CityObject co) {
		mapAbstractSpaceBoundary(ats, co);
		parseAndAddMultiSurface(ats.getLod0MultiSurface(), Lod.LOD0, co);
		parseAndAddMultiSurface(ats.getLod1MultiSurface(), Lod.LOD1, co);
		parseAndAddMultiSurface(ats.getLod2MultiSurface(), Lod.LOD2, co);
		parseAndAddMultiSurface(ats.getLod3MultiSurface(), Lod.LOD3, co);
		parseAndAddMultiSurface(ats.getDeprecatedProperties().getLod4MultiSurface(), Lod.LOD4, co);
	}

	private void mapAbstractSpaceBoundary(AbstractSpaceBoundary asb, CityObject co) {
		mapAbstractCityObject(asb, co);
	}

	private void mapAbstractCityObject(AbstractCityObject aco, CityObject to) {
		mapAbstractFeatureWithLifespan(aco, to);
	}

	private void mapAbstractFeatureWithLifespan(AbstractFeatureWithLifespan afwl, CityObject to) {
		mapAbstractFeature(afwl, to);
	}

	private void mapAbstractFeature(AbstractFeature af, CityObject to) {
		mapAbstractGML(af, to);
	}

	private void mapAbstractGML(AbstractGML ag, CityObject to) {
		parseId(ag, to);
	}

	@Override
	public void visit(Railway railway) {
		TransportationObject to = new TransportationObject(TransportationType.RAILWAY);
		mapAbstractTransportationSpace(railway, to);
		finishTransportationMapping(to);
	}

	@Override
	public void visit(Square square) {
		TransportationObject to = new TransportationObject(TransportationType.SQUARE);
		mapAbstractTransportationSpace(square, to);
		finishTransportationMapping(to);
	}

	@Override
	public void visit(Track track) {
		TransportationObject to = new TransportationObject(TransportationType.TRACK);
		mapAbstractTransportationSpace(track, to);
		finishTransportationMapping(to);
	}

	@Override
	public void visit(TransportationComplex transportationComplex) {
		TransportationObject to = new TransportationObject(TransportationType.TRANSPORTATION_COMPLEX);
		mapAbstractTransportationSpace(transportationComplex, to);
		finishTransportationMapping(to);
	}

	private void finishTransportationMapping(TransportationObject to) {
		finishCityObjectConstruction(to);
		model.addTransportation(to);
	}

	private void finishCityObjectConstruction(CityObject co) {
		co.unsetGmlGeometries();
		resolveAndClearReferences();
		updateEdgesAndVertices(co);
	}

	private void mapAbstractTransportationSpace(AbstractTransportationSpace ats, TransportationObject to) {
		to.setGmlObject(ats);
		parseAbstractTransportationSpaceGeometries(ats, to);
		for (TrafficSpaceProperty tsp : ats.getTrafficSpaces()) {
			if (tsp.isSetObject()) {
				TransportationObject trafficSpace = new TransportationObject(TransportationType.TRAFFIC_SPACE);
				mapTrafficSpace(tsp.getObject(), trafficSpace);
				finishTransportationMapping(trafficSpace);
				to.getChildren().add(trafficSpace);
			}
		}
		for (AuxiliaryTrafficSpaceProperty auxTrafficProp : ats.getAuxiliaryTrafficSpaces()) {
			if (auxTrafficProp.isSetObject()) {
				TransportationObject trafficSpace = parseAuxiliaryTrafficSpace(auxTrafficProp.getObject());
				finishTransportationMapping(trafficSpace);
				to.addChild(trafficSpace);
			}
		}
	}

	private void mapTrafficSpace(TrafficSpace ts, TransportationObject to) {
		mapAbstractUnoccupiedSpace(ts, to);
	}

	private void mapAbstractUnoccupiedSpace(AbstractUnoccupiedSpace aus, TransportationObject to) {
		mapAbstractPhysicalSpace(aus, to);
	}

	private TransportationObject parseAuxiliaryTrafficSpace(AuxiliaryTrafficSpace ats) {
		TransportationObject to = new TransportationObject(TransportationType.AUXILLIARY_TRAFFIC_SPACE);
		parseAbstractSpaceGeometries(ats, to);
		finishCityObjectConstruction(to);
		return to;
	}

	private void parseAbstractTransportationSpaceGeometries(AbstractTransportationSpace space, CityObject co) {
		parseAndAddMultiSurface(space.getDeprecatedProperties().getLod1MultiSurface(), Lod.LOD1, co);
		parseAndAddMultiSurface(space.getDeprecatedProperties().getLod4MultiSurface(), Lod.LOD4, co);
	}

	private void parseAbstractSpaceGeometries(AbstractSpace as, CityObject co) {
		parseAndAddMultiSurface(as.getLod0MultiSurface(), Lod.LOD0, co);
		parseAndAddMultiSurface(as.getLod2MultiSurface(), Lod.LOD2, co);
		parseAndAddMultiSurface(as.getLod3MultiSurface(), Lod.LOD3, co);
		parseAndAddSolid(as.getLod1Solid(), Lod.LOD1, co);
		parseAndAddSolid(as.getLod2Solid(), Lod.LOD2, co);
		parseAndAddSolid(as.getLod3Solid(), Lod.LOD3, co);
	}

	private void parseAndAddMultiSurface(MultiSurfaceProperty msp, Lod lod, CityObject co) {
		if (msp == null || msp.getObject() == null) {
			return;
		}
		Geometry geom = parseMultiSurface(msp.getObject(), lod);
		co.addGeometry(geom);
	}

	private void parseAndAddSolid(SolidProperty sp, Lod lod, CityObject co) {
		if (sp == null || sp.getObject() == null) {
			return;
		}
		Geometry geom = parseSolid(sp.getObject(), lod);
		if (geom != null) {
			co.addGeometry(geom);
		}
	}

	private void resolveAndClearReferences() {
		for (ResolvableReference ref : references) {
			String href = ref.href();
			if (href.startsWith("#")) {
				href = href.substring(1);
			}
			Geometry geom = ref.geometry();
			ConcretePolygon concPoly = polygonMap.get(href);
			if (concPoly == null) {
				if (logger.isWarnEnabled()) {
					logger.warn(Localization.getText("FeatureMapper.polygonUnreferenced"), href);
				}
				continue;
			}
			LinkedPolygon lPoly = new LinkedPolygon(concPoly, geom);
			if (geom.getParent() instanceof BoundarySurface bs) {
				lPoly.setPartOfSurface(bs);
				if (bs.getParent() instanceof BuildingInstallation bi) {
					lPoly.setPartOfInstallation(bi);
				}
			}
			geom.addPolygon(lPoly);
		}

		// clear storage for polygons and vertices
		// probably faster than .clear() ?
		references = new ArrayList<>();
		vertexMap = new HashMap<>();
		polygonMap = new HashMap<>();
	}

	private void readAbstractBuilding(org.citygml4j.core.model.building.AbstractBuilding gmlAb,
			AbstractBuilding cdBuilding) {
		mapAbstractOccupiedSpace(gmlAb, cdBuilding);
		cdBuilding.setGmlObject(gmlAb);

		// parse deprecated geometries
		parseAndAddMultiSurface(gmlAb.getDeprecatedProperties().getLod1MultiSurface(), Lod.LOD1, cdBuilding);
		parseAndAddMultiSurface(gmlAb.getDeprecatedProperties().getLod4MultiSurface(), Lod.LOD4, cdBuilding);
		parseAndAddSolid(gmlAb.getDeprecatedProperties().getLod4Solid(), Lod.LOD4, cdBuilding);

		for (BuildingInstallationProperty biProp : gmlAb.getBuildingInstallations()) {
			var gmlBi = biProp.getObject();
			if (gmlBi == null) {
				// ignore empty properties
				continue;
			}
			BuildingInstallation bi = mapBuildingInstallation(gmlBi);
			cdBuilding.addBuildingInstallation(bi);
		}

		SurfaceMapper surfaceMapper = new SurfaceMapper(polygonMap, references, vertexMap, config);
		for (AbstractSpaceBoundaryProperty surfaceProp : gmlAb.getBoundaries()) {
			if (!surfaceProp.isSetObject()) {
				continue;
			}
			AbstractSpaceBoundary surface = surfaceProp.getObject();
			surface.accept(surfaceMapper);
		}
		updatePartOfSurface(cdBuilding, surfaceMapper);

		cdBuilding.unsetGmlGeometries();
		resolveAndClearReferences();
		updateEdgesAndVertices(cdBuilding);
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			updateEdgesAndVertices(bs);
		}
		for (BuildingInstallation bi : cdBuilding.getBuildingInstallations()) {
			updateEdgesAndVertices(bi);
			for (BoundarySurface bs : bi.getBoundarySurfaces()) {
				updateEdgesAndVertices(bs);
			}
		}
	}

	private void updatePartOfSurface(AbstractBuilding cdBuilding, SurfaceMapper surfaceMapper) {
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			cdBuilding.addBoundarySurface(bs);
			for (Geometry geom : bs.getGeometries()) {
				for (Polygon p : geom.getPolygons()) {
					p.setPartOfSurface(bs);
				}
			}
		}
	}

	public static void parseId(AbstractGML gml, GmlElement gmlElement) {
		String id = gml.getId();
		if (id != null) {
			gmlElement.setGmlId(new GmlId(id));
		}
	}

	private BuildingInstallation mapBuildingInstallation(org.citygml4j.core.model.building.BuildingInstallation gmlBi) {
		BuildingInstallation bi = new BuildingInstallation();
		bi.setGmlObject(gmlBi);
		mapAbstractOccupiedSpace(gmlBi, bi);
		GeometryProperty<?> lod2Prop = gmlBi.getDeprecatedProperties().getLod2Geometry();
		parseAndAddAbstractGeometry(lod2Prop, Lod.LOD2, bi);
		GeometryProperty<?> lod3Prop = gmlBi.getDeprecatedProperties().getLod3Geometry();
		parseAndAddAbstractGeometry(lod3Prop, Lod.LOD3, bi);
		GeometryProperty<?> lod4Prop = gmlBi.getDeprecatedProperties().getLod4Geometry();
		parseAndAddAbstractGeometry(lod4Prop, Lod.LOD4, bi);
		bi.unsetGmlGeometries();

		SurfaceMapper surfaceMapper = new SurfaceMapper(polygonMap, references, vertexMap, config);
		for (AbstractSpaceBoundaryProperty surfaceProp : gmlBi.getBoundaries()) {
			if (!surfaceProp.isSetObject()) {
				continue;
			}
			AbstractSpaceBoundary surface = surfaceProp.getObject();
			surface.accept(surfaceMapper);
		}
		for (BoundarySurface bs : surfaceMapper.getSurfaces()) {
			bi.addBoundarySurface(bs);
			for (Geometry geom : bs.getGeometries()) {
				for (Polygon p : geom.getPolygons()) {
					p.setPartOfSurface(bs);
					p.setPartOfInstallation(bi);
				}
			}
		}
		for (Geometry geom : bi.getGeometries()) {
			for (Polygon p : geom.getPolygons()) {
				p.setPartOfInstallation(bi);
			}
		}

		return bi;
	}

	private void parseAndAddAbstractGeometry(GeometryProperty<?> geomProp, Lod lod, CityObject co) {
		if (geomProp == null || geomProp.getObject() == null) {
			return;
		}
		AbstractGeometry abstractGeometry = geomProp.getObject();
		if (abstractGeometry instanceof MultiSurface ms) {
			Geometry geom = parseMultiSurface(ms, lod);
			co.addGeometry(geom);
		} else if (abstractGeometry instanceof Solid solid) {
			Geometry geom = parseSolid(solid, lod);
			if (geom != null) {
				co.addGeometry(geom);
			}
		} else {
			logger.warn("Cannot handle geometry type {}, ignoring", abstractGeometry.getClass().getSimpleName());
		}
	}

	private Geometry parseSolid(AbstractSolid abstractSolid, Lod lod) {
		if (abstractSolid instanceof Solid s) {
			return handleSolidGeometry(s, lod);
		} else {
			logger.warn("Cannot handle solid class {}, can only process pure solids",
					abstractSolid.getClass().getSimpleName());
			return null;
		}
	}

	private Geometry handleSolidGeometry(Solid solid, Lod lod) {
		ShellProperty exteriorProperty = solid.getExterior();
		if (exteriorProperty == null || exteriorProperty.getObject() == null) {
			logger.warn("Found solid {} without exterior hull, ignoring", solid.getId());
			return null;
		}
		Geometry geom = new Geometry(GeometryType.SOLID, lod);
		Shell exterior = solid.getExterior().getObject();
		Citygml3GeometryMapper geometryMapper = new Citygml3GeometryMapper(config, vertexMap);
		readSurfaceMember(geom, geometryMapper, exterior.getSurfaceMembers());
		return geom;
	}

	private Geometry parseMultiSurface(MultiSurface ms, Lod lod) {
		Geometry geom = new Geometry(GeometryType.MULTI_SURFACE, lod);
		Citygml3GeometryMapper geometryMapper = new Citygml3GeometryMapper(config, vertexMap);
		readSurfaceMember(geom, geometryMapper, ms.getSurfaceMember());
		return geom;
	}

	private void readSurfaceMember(Geometry geom, Citygml3GeometryMapper geometryMapper,
			List<SurfaceProperty> surfaceMember) {
		for (SurfaceProperty prop : surfaceMember) {
			if (prop.getHref() != null) {
				references.add(new ResolvableReference(prop.getHref(), geom));
				continue;
			}
			if (prop.getObject() != null) {
				AbstractSurface as = prop.getObject();
				as.accept(geometryMapper);
			}
		}

		List<ConcretePolygon> polygons = geometryMapper.getPolygons();

		for (ConcretePolygon concretePoly : polygons) {
			geom.addPolygon(concretePoly);
			if (concretePoly.hasExistingGmlId()) {
				polygonMap.put(concretePoly.getGmlId().getGmlString(), concretePoly);
			}
		}
	}

	private void updateEdgesAndVertices(CityObject co) {
		// searching for neighboring vertices, replacing them with one single vertex to
		// avoid later problems with edges and manifold problems
		for (Geometry geom : co.getGeometries()) {
			KDTree tree = new KDTree();
			for (Polygon poly : geom.getPolygons()) {
				LinearRing lr = poly.getExteriorRing();
				updateRing(tree, lr);
				for (LinearRing innerRing : poly.getInnerRings()) {
					updateRing(tree, innerRing);
				}
			}
			if (!config.useLowMemoryConsumption()) {
				// no low memory consumption mode meaning create all meta information in
				// geometry
				geom.updateEdgesAndVertices();
			}
		}
	}

	private void updateRing(KDTree tree, LinearRing lr) {
		for (int i = 0; i < lr.getVertices().size(); i++) {
			Vertex v = lr.getVertices().get(i);
			List<Vertex> nodesInRange = tree.getNodesInRange(v, neighborDistance);
			if (nodesInRange.isEmpty()) {
				tree.add(v);
			} else {
				// replace other vertex with any neighboring one
				Vertex original = nodesInRange.get(0);
				lr.setVertex(i, original);
			}
		}
	}

	public void setCityModel(CityModel cModel) {
		model.setCityModel(cModel);
	}

	public CityDoctorModel getModel() {
		return model;
	}

	public void setCityGMLVersion(CityGMLVersion cityGMLVersion) {
		model.setParsedCityGMLVersion(cityGMLVersion);
	}
}
