package de.hft.stuttgart.citygml.green.osm;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.building.Building;
import org.citygml4j.core.model.core.AbstractCityObject;
import org.citygml4j.core.model.core.AbstractCityObjectProperty;
import org.citygml4j.core.model.core.AbstractFeature;
import org.citygml4j.core.model.core.AbstractGenericAttribute;
import org.citygml4j.core.model.core.AbstractGenericAttributeProperty;
import org.citygml4j.core.model.core.CityModel;
import org.citygml4j.core.model.generics.StringAttribute;
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.citygml4j.xml.CityGMLContext;
import org.citygml4j.xml.CityGMLContextException;
import org.citygml4j.xml.module.citygml.CoreModule;
import org.citygml4j.xml.reader.CityGMLInputFactory;
import org.citygml4j.xml.reader.CityGMLReadException;
import org.citygml4j.xml.reader.CityGMLReader;
import org.citygml4j.xml.writer.CityGMLOutputFactory;
import org.citygml4j.xml.writer.CityGMLWriteException;
import org.citygml4j.xml.writer.CityGMLWriter;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureSource;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.proj4j.BasicCoordinateTransform;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.ProjCoordinate;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xmlobjects.gml.model.geometry.DirectPosition;
import org.xmlobjects.gml.model.geometry.DirectPositionList;
import org.xmlobjects.gml.model.geometry.Envelope;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
import org.xmlobjects.gml.model.geometry.primitives.AbstractRingProperty;
import org.xmlobjects.gml.model.geometry.primitives.LinearRing;
import org.xmlobjects.gml.model.geometry.primitives.SurfaceProperty;

public class GreenEnricher {
	
	private static final double TRUNK_PERCENTAGE = 0.2;
	private static final double CROWN_PERCENTAGE = 1 - TRUNK_PERCENTAGE;

	private static final int BOUNDING_BOX_INCREASE_IN_M = 100;

	// in degrees
	// add around 30m ( 1" = 30m -> 0.00277 = 900m)
	private static final double BOUNDING_BOX_INCREASE = 0.00277;

	private static final String OSM_STRING = """
						[bbox:{{bbox}}];
						(
			nwr["leisure"="garden"];
			nwr["landuse"="forest"];
			nwr["natural"="tree"];
			nwr["leisure"="park"];
			nwr["natural"="tree_row"];
			nwr["natural"="wood"];
			nwr["landuse"="grass"];
			nwr["landuse"="orchard"];
			nwr["natural"="scrub"];
			nwr["landuse"="meadow"];
			nwr["landuse"="farmland"];
			nwr["landuse"="allotments"];
			nwr["natural"="water"];
			nwr["water"="river"];
			nwr["type"="waterway"];
			nwr["waterway"="stream"];
			nwr["water"="pond"];
						);
						out geom;""";

	private static final URI OVERPASS_API_URI = URI.create("http://www.overpass-api.de/api/interpreter");
	private static final CRSFactory CRS_FACTORY = new CRSFactory();
	private static CoordinateReferenceSystem sourceCRS;
	private static CoordinateReferenceSystem targetCRS = CRS_FACTORY.createFromName("EPSG:4326");
	private static BasicCoordinateTransform transform;
	private static BasicCoordinateTransform backTransform;
	public static GeometryFactory geomFactory = new GeometryFactory();

	public static void main(String[] args) throws IOException, CityGMLContextException, CityGMLReadException,
			InterruptedException, ParserConfigurationException, SAXException, CityGMLWriteException {
		System.out.println("Reading CityGML file");
		Path inFile = Paths.get(args[0]);
		CityModel cityModel = readCityGml(inFile);
		
		

		String epsgCode = extractEpsgCode(cityModel);
		sourceCRS = CRS_FACTORY.createFromName(epsgCode);
		transform = new BasicCoordinateTransform(sourceCRS, targetCRS);
		backTransform = new BasicCoordinateTransform(targetCRS, sourceCRS);

		OsmData osmData = new OsmData();
		String boundingBoxString = extractAndConvertBoundingBox(cityModel, epsgCode, osmData);
//		HttpResponse<String> response = getOsmData(boundingBoxString);
//		Files.write(Path.of("osm_response.xml"), response.body().getBytes(StandardCharsets.UTF_8));
//		String osmResponse = response.body();
		String osmResponse = Files.readString(Paths.get("data", "osm_response.xml"));

		System.out.println("Parsing OSM response");
		parseOsmResponse(osmResponse, osmData);
		createBoundingBox(cityModel, osmData);

//		List<GreenArea> newGreenAreas = new ArrayList<>();	

		System.out.println("Fit data in bounding box");
		fitToBoundingBox(osmData);

		System.out.println("Filter intersecting areas");
		List<GreenArea> greenAreas = osmData.getGreenAreas();
		removeDuplicateAreas(greenAreas);

		convertGreenAreasToCityGML(cityModel, greenAreas);
		convertWaterAreasToCityGML(cityModel, osmData);

		for (Waterway waterWay : osmData.getWaterways()) {
		}
		
		// trees
		
		TreeKatasterData katasterData = parseTreeKatasterData(Paths.get("data", "Baum.shp"));
		for (Iterator<TreePoint> iterator = osmData.getTreePoints().iterator(); iterator.hasNext();) {
			TreePoint tp = iterator.next();
			// if another tree from kataster is within 3m ignore it
			for (Tree tree : katasterData.getTrees()) {
				if (tp.getPoint().distance(tree.getPoint()) < 3) {
					iterator.remove();
				}
			}
		}
		
		for (Tree tree : katasterData.getTrees()) {
			double trunkRadius = tree.getTrunkRadius();
			double trunkHeight = tree.getTrunkHeight();
			double crownHeight = tree.getCrownHeight();
			double crownRadius = tree.getCrownRadius();

			Coordinate coordinate = tree.getPoint().getCoordinate();
			if (Double.isNaN(coordinate.z)) {
				coordinate.z = 0;
			}
			MultiSurface generatedTree = TreeGenerator.generateTree(coordinate, trunkRadius, trunkHeight, crownRadius, crownRadius, crownHeight);
			SolitaryVegetationObject cover = new SolitaryVegetationObject();
			StringAttribute typeAttribute = new StringAttribute("type", tree.getType());
			AbstractGenericAttributeProperty attributeProp = new AbstractGenericAttributeProperty(typeAttribute);
			cover.getGenericAttributes().add(attributeProp);
			cover.setId(UUID.randomUUID().toString());
			cover.setLod2MultiSurface(new MultiSurfaceProperty(generatedTree));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(cover));

		}
		
		
		for (TreePoint tp : osmData.getTreePoints()) {
			// standard tree
			double trunkRadius = 0.89 / (2 * Math.PI);
			double trunkHeight = TRUNK_PERCENTAGE * 11.46;
			double crownHeight = CROWN_PERCENTAGE * 11.46;
			double crownRadius = 8 / 2d;
			Coordinate coordinate = tp.getPoint().getCoordinate();
			if (Double.isNaN(coordinate.z)) {
				coordinate.z = 0;
			}
			MultiSurface generatedTree = TreeGenerator.generateTree(coordinate, trunkRadius, trunkHeight, crownRadius, crownRadius, crownHeight);
			SolitaryVegetationObject cover = new SolitaryVegetationObject();
			StringAttribute typeAttribute = new StringAttribute("type", "Acer campestre");
			AbstractGenericAttributeProperty attributeProp = new AbstractGenericAttributeProperty(typeAttribute);
			cover.getGenericAttributes().add(attributeProp);
			cover.setId(UUID.randomUUID().toString());
			cover.setLod2MultiSurface(new MultiSurfaceProperty(generatedTree));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(cover));
		}
		
		
		
		
//
//		for (TreeRow tr : osmData.getTreeRows()) {
//			System.out.println(tr);
//		}

		clampToGround(cityModel);

		String inputString = inFile.getFileName().toString();
		String inputPathWithoutFileEnding = inputString.substring(0, inputString.lastIndexOf('.'));
		Path outputPath = Paths.get("data", inputPathWithoutFileEnding + "_with_greens.gml");
		System.out.println("Writing output file.");
		writeCityGML(cityModel, outputPath);
		System.out.println("Done");

	}

	private static TreeKatasterData parseTreeKatasterData(Path path) throws IOException {
		TreeKatasterData result = new TreeKatasterData();
        Map<String, Object> readParameters = new HashMap<>();
        readParameters.put("url", path.toUri().toURL());
        readParameters.put("charset", StandardCharsets.UTF_8);
        DataStore dataStore = DataStoreFinder.getDataStore(readParameters);
        String typeName = dataStore.getTypeNames()[0];

        FeatureSource<SimpleFeatureType, SimpleFeature> source =
                dataStore.getFeatureSource(typeName);

        FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures();
        try (FeatureIterator<SimpleFeature> features = collection.features()) {
            while (features.hasNext()) {
                SimpleFeature feature = features.next();
                Tree tree = new Tree();
                
                Point p = (Point) feature.getAttribute("the_geom");
                tree.setPoint(p);
                
                String type = feature.getAttribute("Bezeichnun").toString();
                tree.setType(type);
                
                Object treeHeightObject = feature.getAttribute("Baumhöhe");
                if (treeHeightObject == null) {
                	continue;
                }
                int treeHeight = Integer.parseInt(treeHeightObject.toString());
                double crownHeight = CROWN_PERCENTAGE * treeHeight;
                double trunkHeight = TRUNK_PERCENTAGE * treeHeight;
                tree.setCrownHeight(crownHeight);
                tree.setTrunkHeight(trunkHeight);
                
                Object crownWidth = feature.getAttribute("Kronenbrei");
                if (crownWidth == null) {
                	continue;
                }
                tree.setCrownRadius(Float.parseFloat(crownWidth.toString()) / 2);
                Object trunkCirc = feature.getAttribute("Stammumfan");
                if (trunkCirc == null) {
                	continue;
                }
                tree.setTrunkRadius(Integer.parseInt(trunkCirc.toString()) / (2 * Math.PI) / 100);
                
                result.getTrees().add(tree);
            }
        }
        return result;
	}

	private static void convertWaterAreasToCityGML(CityModel cityModel, OsmData osmData) {
		for (WaterArea waterArea : osmData.getWaterAreas()) {
			WaterBody wb = new WaterBody();
			org.xmlobjects.gml.model.geometry.primitives.Polygon poly = convertToCityGmlPoly(waterArea.getArea());
			if (poly == null) {
				System.out.println("Skipping WaterBody " + waterArea.getArea());
				continue;
			}
			MultiSurface ms = new MultiSurface();
			wb.setId(UUID.randomUUID().toString());
			ms.getSurfaceMember().add(new SurfaceProperty(poly));
			wb.setLod0MultiSurface(new MultiSurfaceProperty(ms));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(wb));
		}
	}

	private static void convertGreenAreasToCityGML(CityModel cityModel, List<GreenArea> greenAreas) {
		for (GreenArea ga : greenAreas) {
			org.xmlobjects.gml.model.geometry.primitives.Polygon poly = convertToCityGmlPoly(ga.getArea());
			if (poly == null) {
				System.out.println("Skipping " + ga.getArea());
				continue;
			}
			PlantCover cover = new PlantCover();
			MultiSurface ms = new MultiSurface();
			cover.setId(UUID.randomUUID().toString());
			ms.getSurfaceMember().add(new SurfaceProperty(poly));
			cover.setLod2MultiSurface(new MultiSurfaceProperty(ms));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(cover));
		}
	}

	private static void fitToBoundingBox(OsmData osmData) {
		List<GreenArea> greenAreas = osmData.getGreenAreas();
		List<GreenArea> newGreenAreas = new ArrayList<>();
		for (GreenArea greenArea : greenAreas) {
			Polygon area = greenArea.getArea();
			Geometry intersection = area.intersection(osmData.getBoundingBox());
			if (intersection instanceof MultiPolygon multi) {
				Polygon poly1 = (Polygon) multi.getGeometryN(0);
				greenArea.setArea(poly1);
				for (int k = 1; k < multi.getNumGeometries(); k++) {
					GreenArea newGreenArea = new GreenArea();
					newGreenArea.setArea((Polygon) multi.getGeometryN(k));
					newGreenAreas.add(newGreenArea);
				}
			} else {
				greenArea.setArea((Polygon) intersection);
			}
		}
		greenAreas.addAll(newGreenAreas);
		
		clipWaterAreasToBoundingBox(osmData);
		
		for (Iterator<TreePoint> iterator = osmData.getTreePoints().iterator(); iterator.hasNext();) {
			TreePoint tp = iterator.next();
			if (!osmData.getBoundingBox().contains(tp.getPoint())) {
				iterator.remove();
			}
		}
		
	}

	private static void clipWaterAreasToBoundingBox(OsmData osmData) {
		List<WaterArea> newWaterAreas = new ArrayList<>();
		for (WaterArea waterArea : osmData.getWaterAreas()) {
			Polygon area = waterArea.getArea();
			Geometry intersection = area.intersection(osmData.getBoundingBox());
			if (intersection instanceof MultiPolygon multi) {
				Polygon poly1 = (Polygon) multi.getGeometryN(0);
				waterArea.setArea(poly1);
				for (int k = 1; k < multi.getNumGeometries(); k++) {
					WaterArea newWaterArea = new WaterArea();
					newWaterArea.setArea((Polygon) multi.getGeometryN(k));
					newWaterAreas.add(newWaterArea);
				}
			} else {
				waterArea.setArea((Polygon) intersection);
			}
		}
	}

	private static void removeDuplicateAreas(List<GreenArea> greenAreas) {
		for (int i = 0; i < greenAreas.size(); i++) {
			for (int j = i + 1; j < greenAreas.size(); j++) {
				GreenArea area1 = greenAreas.get(i);
				GreenArea area2 = greenAreas.get(j);
				if (area1.getArea().intersects(area2.getArea())) {
					Geometry difference = area1.getArea().difference(area2.getArea());
					System.out.println(difference);
					if (difference instanceof MultiPolygon) {
						MultiPolygon multi = (MultiPolygon) difference;
						Polygon poly1 = (Polygon) multi.getGeometryN(0);
						area1.setArea(poly1);
						for (int k = 1; k < multi.getNumGeometries(); k++) {
							GreenArea newGreenArea = new GreenArea();
							newGreenArea.setArea((Polygon) multi.getGeometryN(k));
							greenAreas.add(newGreenArea);
						}
					} else {
						area1.setArea((Polygon) difference);
					}

				}
			}
		}
	}

	private static void createBoundingBox(CityModel cityModel, OsmData osmData) {
		// TODO Auto-generated method stub

	}

//	private static MultiSurface generateTree(Point p) {
//		double radiusTrunk = 0.2;
//		
//		MultiSurface result = new MultiSurface();
//		
//		// trunk
//		Coordinate center = p.getCoordinate();
//		List<Coordinate> coords = new ArrayList<>();
//		coords.add(new Coordinate(center.x, center.y + radiusTrunk));
//		coords.add(new Coordinate(center.x + radiusTrunk / 2, center.y + Math.cos(0.5) * radiusTrunk));
//	}

	private static void clampToGround(CityModel cityModel) {
		for (AbstractCityObjectProperty afp : cityModel.getCityObjectMembers()) {
			AbstractCityObject af = afp.getObject();
			if (af instanceof Building b) {
				Envelope envelope = b.computeEnvelope();
				Double lowestZ = envelope.getLowerCorner().getValue().get(2);
				b.accept(new ObjectWalker() {
					@Override
					public void visit(LinearRing linearRing) {
						List<Double> values = linearRing.getControlPoints().getPosList().getValue();
						for (int i = 2; i < values.size(); i = i + 3) {
							values.set(i, values.get(i) - lowestZ);
						}
					}
				});
			}
		}
	}

	public static org.xmlobjects.gml.model.geometry.primitives.Polygon convertToCityGmlPoly(Polygon area) {
		if (area.getExteriorRing().getCoordinates().length == 0) {
			return null;
		}
		org.xmlobjects.gml.model.geometry.primitives.Polygon result = new org.xmlobjects.gml.model.geometry.primitives.Polygon();
		LinearRing lr = new LinearRing();
		result.setExterior(new AbstractRingProperty(lr));
		DirectPositionList posList = new DirectPositionList();
		lr.getControlPoints().setPosList(posList);
		List<Double> values = posList.getValue();
		for (Coordinate coord : area.getExteriorRing().getCoordinates()) {
			values.add(coord.x);
			values.add(coord.y);
			if (Double.isNaN(coord.z)) {
				coord.z = 0;
			}
			values.add(coord.z);
		}

		for (int i = 0; i < area.getNumInteriorRing(); i++) {
			LinearRing innerRing = new LinearRing();
			result.getInterior().add(new AbstractRingProperty(innerRing));
			DirectPositionList innerPosList = new DirectPositionList();
			innerRing.getControlPoints().setPosList(innerPosList);
			List<Double> innerValues = innerPosList.getValue();
			Coordinate[] coordinates = area.getInteriorRingN(i).getCoordinates();
			for (Coordinate coord : coordinates) {
				innerValues.add(coord.x);
				innerValues.add(coord.y);
				if (Double.isNaN(coord.z)) {
					coord.z = 0;
				}
				innerValues.add(coord.z);
			}
		}

		return result;
	}

	private static HttpResponse<String> getOsmData(String boundingBoxString) throws IOException, InterruptedException {
		String data = OSM_STRING.replace("{{bbox}}", boundingBoxString);
		String body = URLEncoder.encode("data", StandardCharsets.UTF_8) + "="
				+ URLEncoder.encode(data, StandardCharsets.UTF_8);

		HttpClient client = HttpClient.newHttpClient();
		HttpRequest request = HttpRequest.newBuilder().uri(OVERPASS_API_URI)
				.POST(HttpRequest.BodyPublishers.ofString(body)).build();
		System.out.println("Retrieving OSM data of bounding box: " + boundingBoxString);
		HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
		if (response.statusCode() != 200) {
			System.err.println("Failed to retrieve OSM data. Return code: " + response.statusCode());
			System.exit(1);
		}
		return response;
	}

	private static void writeCityGML(CityModel cityModel, Path outputPath)
			throws CityGMLWriteException, CityGMLContextException {
		CityGMLContext context = CityGMLContext.newInstance();
		CityGMLVersion version = CityGMLVersion.v2_0;
		CityGMLOutputFactory out = context.createCityGMLOutputFactory(version);

		try (CityGMLWriter writer = out.createCityGMLWriter(outputPath, StandardCharsets.UTF_8.name())) {
			writer.withIndent("  ").withDefaultSchemaLocations().withDefaultPrefixes()
					.withDefaultNamespace(CoreModule.of(version).getNamespaceURI()).write(cityModel);
		}
	}

	private static OsmData parseOsmResponse(String osmResponse, OsmData osmData)
			throws ParserConfigurationException, SAXException, IOException {
		Document document = createDomFromOsmResponse(osmResponse);

		// root element
		Node osmItem = document.getChildNodes().item(0);
		NodeList childNodes = osmItem.getChildNodes();

		Set<String> usedIds = new HashSet<>();

		for (int i = 0; i < childNodes.getLength(); i++) {
			Node node = childNodes.item(i);
			if (parseTreeIfPossible(osmData.getTreePoints(), node, usedIds)) {
				continue;
			}
			if (parseGreenAreasIfPossible(osmData, node, usedIds)) {
				continue;
			}
			parseGreenRelationsIfPossible(osmData, node, usedIds);
		}

		return osmData;
	}

	private static boolean parseGreenRelationsIfPossible(OsmData data, Node node, Set<String> usedIds) {
		if (!"relation".equals(node.getNodeName())) {
			return false;
		}
		String nodeValue = node.getAttributes().getNamedItem("id").getNodeValue();
		System.out.println("Relation: " + nodeValue);
		NodeList childNodes = node.getChildNodes();
		List<List<Coordinate>> innerRings = new ArrayList<>();
		List<Coordinate> outerRing = new ArrayList<>();
		boolean water = false;
		for (int i = 0; i < childNodes.getLength(); i++) {
			Node boundsOrMember = childNodes.item(i);
			if (!"member".equals(boundsOrMember.getNodeName())) {
				if ("tag".equals(boundsOrMember.getNodeName())) {
					if ("water".equals(boundsOrMember.getAttributes().getNamedItem("v").getNodeValue())) {
						water = true;
					}
				}
				continue;
			}
			Node member = boundsOrMember;
			String role = member.getAttributes().getNamedItem("role").getNodeValue();
			if (!"way".equals(member.getAttributes().getNamedItem("type").getNodeValue())) {
				continue;
			}
			String referenceId = member.getAttributes().getNamedItem("ref").getNodeValue();
			if (usedIds.contains(referenceId)) {
				System.out.println("Relation contains way that is already used " + referenceId);
				continue;
			}
			List<Coordinate> ringCoords;
			if ("inner".equals(role)) {
				ringCoords = new ArrayList<>();
				innerRings.add(ringCoords);
			} else if ("outer".equals(role)) {
				// outer ring
				outerRing.clear();
				ringCoords = outerRing;
			} else if ("main_stream".equals(role)) {
				ringCoords = outerRing;
			} else if ("side_stream".equals(role)) {
				ringCoords = outerRing;
			} else {
				System.out.println("Unknown role: " + role);
				// probably waterway
				continue;
			}
			NodeList memberNodes = member.getChildNodes();
			for (int memberNodesIndex = 0; memberNodesIndex < memberNodes.getLength(); memberNodesIndex++) {
				Node nd = memberNodes.item(memberNodesIndex);
				if ("nd".equals(nd.getNodeName())) {
					double lat = Double.parseDouble(nd.getAttributes().getNamedItem("lat").getNodeValue());
					double lon = Double.parseDouble(nd.getAttributes().getNamedItem("lon").getNodeValue());
					ProjCoordinate converted = convertCoordinatesFrom84(lon, lat);
					ringCoords.add(new Coordinate(converted.x, converted.y));
				}
			}
		}

		if (water) {
			org.locationtech.jts.geom.LinearRing outerLinearRing = geomFactory
					.createLinearRing(outerRing.toArray(new Coordinate[outerRing.size()]));
			// create the inner rings
			List<org.locationtech.jts.geom.LinearRing> innerLinearRings = new ArrayList<>();
			for (List<Coordinate> innerRing : innerRings) {
				org.locationtech.jts.geom.LinearRing innerLinearRing = geomFactory
						.createLinearRing(innerRing.toArray(new Coordinate[innerRing.size()]));
				innerLinearRings.add(innerLinearRing);
			}

			Polygon polygon = geomFactory.createPolygon(outerLinearRing,
					innerLinearRings.toArray(new org.locationtech.jts.geom.LinearRing[innerLinearRings.size()]));

			data.getWaterAreas().add(new WaterArea(polygon));
		}

//		validateRing(outerRing);
		// create the outer ring
//		org.locationtech.jts.geom.LinearRing outerLinearRing = geomFactory
//				.createLinearRing(outerRing.toArray(new Coordinate[outerRing.size()]));
//
//		// create the inner rings
//		List<org.locationtech.jts.geom.LinearRing> innerLinearRings = new ArrayList<>();
//		for (List<Coordinate> innerRing : innerRings) {
//			org.locationtech.jts.geom.LinearRing innerLinearRing = geomFactory
//					.createLinearRing(innerRing.toArray(new Coordinate[innerRing.size()]));
//			innerLinearRings.add(innerLinearRing);
//		}
//
//		if (outerRing.isEmpty()) {
//			return false;
//		}
//
//		// create the polygon
//		Polygon polygon = geomFactory.createPolygon(outerLinearRing,
//				innerLinearRings.toArray(new org.locationtech.jts.geom.LinearRing[innerLinearRings.size()]));
//
//		data.getGreenAreas().add(new GreenArea(polygon));
		return true;

	}

	private static void validateRing(List<Coordinate> outerRing) {
		if (outerRing.isEmpty()) {
			return;
		}
		if (!outerRing.get(0).equals(outerRing.get(outerRing.size() - 1))) {
			// close ring
			outerRing.add(outerRing.get(0));
		}
	}

	private static boolean parseGreenAreasIfPossible(OsmData data, Node node, Set<String> usedIds) {
		if (!"way".equals(node.getNodeName())) {
			return false;
		}

		boolean water = false;
		String id = node.getAttributes().getNamedItem("id").getNodeValue();
		if (usedIds.contains(id)) {
			System.out.println("Already used way id " + id);
			return true;
		}

		usedIds.add(id);
		NodeList childNodes = node.getChildNodes();
		List<Coordinate> coordinates = new ArrayList<>();
		boolean line = false;
		for (int i = 0; i < childNodes.getLength(); i++) {
			Node child = childNodes.item(i);
			if ("nd".equals(child.getNodeName())) {
				double lat = Double.parseDouble(getTagValue(child, "lat"));
				double lon = Double.parseDouble(getTagValue(child, "lon"));
				ProjCoordinate converted = convertCoordinatesFrom84(lon, lat);
				coordinates.add(new Coordinate(converted.x, converted.y));
			} else if ("tag".equals(child.getNodeName())) {
//				if (existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "tree_row")) {
//					line = true;
//				}
				if ((existTagWithValue(child, "k", "natural") && existTagWithValue(child, "v", "water"))
						|| existTagWithValue(child, "k", "waterway")) {
					water = true;
				}
			}
		}

		if (!coordinates.get(0).equals(coordinates.get(coordinates.size() - 1))) {
			// assume line if start and end coordinate do not match?
			line = true;
		}

		if (water) {
			if (line) {
				LineString lineString = geomFactory
						.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getWaterways().add(new Waterway(lineString));
			} else {
				Polygon polygon = geomFactory.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getWaterAreas().add(new WaterArea(polygon));
			}
		} else {
			if (line) {
				LineString lineString = geomFactory
						.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getTreeRows().add(new TreeRow(lineString));
			} else {
				Polygon polygon = geomFactory.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getGreenAreas().add(new GreenArea(polygon));
			}
		}

		return true;
	}

	private static boolean existTagWithValue(Node node, String key, String value) {
		return value.equals(getTagValue(node, key));
	}

	private static String getTagValue(Node node, String tagName) {
		Node namedItem = node.getAttributes().getNamedItem(tagName);
		if (namedItem == null) {
			return null;
		}
		return namedItem.getNodeValue();
	}

	private static boolean parseTreeIfPossible(List<TreePoint> treePoints, Node node, Set<String> usedIds) {
		if (!"node".equals(node.getNodeName())) {
			return false;
		}
		String id = node.getAttributes().getNamedItem("id").getNodeValue();
		if (usedIds.contains(id)) {
			System.out.println("Already used node id " + id);
			return true;
		}
		usedIds.add(id);
		double lat = Double.parseDouble(node.getAttributes().getNamedItem("lat").getNodeValue());
		double lon = Double.parseDouble(node.getAttributes().getNamedItem("lon").getNodeValue());
		ProjCoordinate converted = convertCoordinatesFrom84(lon, lat);
		Point point = geomFactory.createPoint(new Coordinate(converted.x, converted.y));
		treePoints.add(new TreePoint(point));
		return true;
	}

	private static Document createDomFromOsmResponse(String body)
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		// to be compliant, completely disable DOCTYPE declaration:
		factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
		// or completely disable external entities declarations:
		factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
		factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
		// or prohibit the use of all protocols by external entities:
		factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
		factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
		DocumentBuilder builder = factory.newDocumentBuilder();
		ByteArrayInputStream input = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
		return builder.parse(input);
	}

	private static CityModel readCityGml(Path inFile) throws CityGMLContextException, CityGMLReadException {
		CityGMLContext context = CityGMLContext.newInstance();
		CityGMLInputFactory in = context.createCityGMLInputFactory();
		try (CityGMLReader reader = in.createCityGMLReader(inFile)) {
			AbstractFeature feature = reader.next();
			if (feature instanceof CityModel cm) {
				return cm;
			}
		}
		throw new IllegalStateException("CityGML does not contain a CityModel feature type");
	}

	private static String extractEpsgCode(CityModel cityModel) {
		Envelope envelope = cityModel.getBoundedBy().getEnvelope();
		String srsName = envelope.getSrsName();
		return parseEpsg(srsName);
	}

	private static String parseEpsg(String srsName) {
		int utmIndex = srsName.indexOf("UTM");
		if (utmIndex > 0) {
			return "EPSG:258" + srsName.substring(utmIndex + 3, utmIndex + 5);
		}
		Pattern p = Pattern.compile("EPSG:+([0-9]+)");
		Matcher m = p.matcher(srsName);
		if (m.find()) {
			return "EPSG:" + m.group(1);
		}
		return srsName;
	}

	private static String extractAndConvertBoundingBox(CityModel cityModel, String epsgCode, OsmData osmData) {
		Envelope calculatedEnvelope = cityModel.computeEnvelope();

		DirectPosition lowerCorner = calculatedEnvelope.getLowerCorner();
		ProjCoordinate p1 = new ProjCoordinate(lowerCorner.getValue().get(0), lowerCorner.getValue().get(1));
		ProjCoordinate lowerCornerProjected = new ProjCoordinate();
		transform.transform(p1, lowerCornerProjected);

		DirectPosition upperCorner = calculatedEnvelope.getUpperCorner();
		p1.x = upperCorner.getValue().get(0);
		p1.y = upperCorner.getValue().get(1);
		ProjCoordinate upperCornerProjected = new ProjCoordinate();
		transform.transform(p1, upperCornerProjected);

		lowerCornerProjected.x -= BOUNDING_BOX_INCREASE;
		lowerCornerProjected.y -= BOUNDING_BOX_INCREASE;
		upperCornerProjected.x += BOUNDING_BOX_INCREASE;
		upperCornerProjected.y += BOUNDING_BOX_INCREASE;

		List<Coordinate> bboxCoordinates = new ArrayList<>();
		bboxCoordinates.add(new Coordinate(lowerCorner.getValue().get(0) - BOUNDING_BOX_INCREASE_IN_M,
				lowerCorner.getValue().get(1) - BOUNDING_BOX_INCREASE_IN_M));
		bboxCoordinates.add(new Coordinate(lowerCorner.getValue().get(0) - BOUNDING_BOX_INCREASE_IN_M,
				upperCorner.getValue().get(1) + BOUNDING_BOX_INCREASE_IN_M));
		bboxCoordinates.add(new Coordinate(upperCorner.getValue().get(0) + BOUNDING_BOX_INCREASE_IN_M,
				upperCorner.getValue().get(1) + BOUNDING_BOX_INCREASE_IN_M));
		bboxCoordinates.add(new Coordinate(upperCorner.getValue().get(0) + BOUNDING_BOX_INCREASE_IN_M,
				lowerCorner.getValue().get(1) - BOUNDING_BOX_INCREASE_IN_M));
		bboxCoordinates.add(new Coordinate(lowerCorner.getValue().get(0) - BOUNDING_BOX_INCREASE_IN_M,
				lowerCorner.getValue().get(1) - BOUNDING_BOX_INCREASE_IN_M));

		Polygon poly = geomFactory.createPolygon(bboxCoordinates.toArray(new Coordinate[bboxCoordinates.size()]));
		osmData.setBoundingBox(poly);

		return lowerCornerProjected.y + "," + lowerCornerProjected.x + "," + upperCornerProjected.y + ","
				+ upperCornerProjected.x;
	}

	private static ProjCoordinate convertCoordinatesFrom84(double x, double y) {
		ProjCoordinate p1 = new ProjCoordinate(x, y);
		ProjCoordinate result = new ProjCoordinate();
		backTransform.transform(p1, result);
		return result;
	}

	private static ProjCoordinate convertCoordinatesTo84(DirectPosition point) {
		double x = point.getValue().get(0);
		double y = point.getValue().get(1);
		return convertCoordinatesTo84(x, y);
	}

	private static ProjCoordinate convertCoordinatesTo84(double x, double y) {
		ProjCoordinate p1 = new ProjCoordinate(x, y);
		ProjCoordinate result = new ProjCoordinate();
		transform.transform(p1, result);
		return result;
	}

}
