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

import java.io.IOException;
import java.io.StringReader;
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.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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.CityModel;
import org.citygml4j.core.model.landuse.LandUse;
import org.citygml4j.core.model.transportation.Road;
import org.citygml4j.core.model.vegetation.PlantCover;
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.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.w3c.dom.Node;
import org.w3c.dom.NodeList;
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;

import de.hft.stuttgart.citygml.green.osm.jaxb.OSM;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmMember;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmNode;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmRelation;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmTag;
import de.hft.stuttgart.citygml.green.osm.jaxb.OsmWay;
import de.hft.stuttgart.citygml.green.osm.jaxb.WayNode;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;

public class GreenEnricher {

	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"];
			nwr["landuse"="vineyard"];
						);
						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 final GeometryFactory GEOM_FACTORY = new GeometryFactory();

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

		createTransformers(cityModel);

		OsmData osmData = new OsmData();
		String boundingBoxString = extractAndConvertBoundingBox(cityModel, 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);

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

		// trees
		TreeUtils.insertTrees(cityModel, osmData);
		
		clampToGround(cityModel);

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

	}


	public static void createTransformers(CityModel cityModel) {
		String epsgCode = extractEpsgCode(cityModel);
		sourceCRS = CRS_FACTORY.createFromName(epsgCode);
		transform = new BasicCoordinateTransform(sourceCRS, targetCRS);
		backTransform = new BasicCoordinateTransform(targetCRS, sourceCRS);
	}


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

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

	public static void fitToBoundingBox(OsmData osmData) {
		fitGreenAreas(osmData, osmData.getGreenAreas());
		fitRoadAreas(osmData, osmData.getRoadAreas());
		fitLandUseAreas(osmData, osmData.getLandUseAreas());

		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 fitLandUseAreas(OsmData osmData, List<LandUseArea> landUseAreas) {
		List<LandUseArea> newLandUseAreas = new ArrayList<>();
		for (LandUseArea landUseArea : landUseAreas) {
			Polygon area = landUseArea.getArea();
			Geometry intersection = area.intersection(osmData.getBoundingBox());
			if (intersection instanceof MultiPolygon multi) {
				Polygon poly1 = (Polygon) multi.getGeometryN(0);
				landUseArea.setArea(poly1);
				for (int k = 1; k < multi.getNumGeometries(); k++) {
					LandUseArea newLandUseArea = new LandUseArea();
					newLandUseArea.setArea((Polygon) multi.getGeometryN(k));
					newLandUseAreas.add(newLandUseArea);
				}
			} else {
				landUseArea.setArea((Polygon) intersection);
			}
		}
		landUseAreas.addAll(newLandUseAreas);		
	}


	private static void fitRoadAreas(OsmData osmData, List<RoadArea> roadAreas) {
		List<RoadArea> newRoadAreas = new ArrayList<>();
		for (RoadArea roadArea : roadAreas) {
			Polygon area = roadArea.getArea();
			Geometry intersection = area.intersection(osmData.getBoundingBox());
			if (intersection instanceof MultiPolygon multi) {
				Polygon poly1 = (Polygon) multi.getGeometryN(0);
				roadArea.setArea(poly1);
				for (int k = 1; k < multi.getNumGeometries(); k++) {
					RoadArea newRoadArea = new RoadArea();
					newRoadArea.setArea((Polygon) multi.getGeometryN(k));
					newRoadAreas.add(newRoadArea);
				}
			} else {
				roadArea.setArea((Polygon) intersection);
			}
		}
		roadAreas.addAll(newRoadAreas);
	}


	private static void fitGreenAreas(OsmData osmData, List<GreenArea> greenAreas) {
		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);
	}

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

				}
			}
		}
	}
	public 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;
	}

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

	public static void parseOsmResponse(String osmResponse, OsmData osmData) throws JAXBException {
		JAXBContext context = JAXBContext.newInstance(OSM.class);
		OSM osm = (OSM) context.createUnmarshaller().unmarshal(new StringReader(osmResponse));
		parseTrees(osmData.getTreePoints(), osm.getNodes());
		parseWays(osmData, osm);
		parseRelations(osmData, osm);
	}

	private static void parseRelations(OsmData data, OSM osm) {
		for (OsmRelation rel : osm.getRelations()) {
			boolean isWater = false;

			List<Polygon> polygons = new ArrayList<>();
			List<Coordinate> coordinates = new ArrayList<>();
			for (OsmMember member : rel.getMembers()) {
				for (WayNode nd : member.getNodes()) {
					ProjCoordinate converted = convertCoordinatesFrom84(nd.getLon(), nd.getLat());
					coordinates.add(new Coordinate(converted.x, converted.y));
				}

				if (member.getRole().isBlank()) {
					// assume outer ring
					// check if this ring is closed
					if (coordinates.get(0).equals2D(coordinates.get(coordinates.size() - 1))) {
						// ring is closed, create a polygon out of it
						Polygon polygon = GEOM_FACTORY.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
						coordinates.clear();
						polygons.add(polygon);
					}
				}
			}
			
			if (!coordinates.isEmpty() && coordinates.get(0).equals2D(coordinates.get(coordinates.size() - 1))) {
				// one huge polygon apparently
				Polygon polygon = GEOM_FACTORY.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
				coordinates.clear();
				polygons.add(polygon);
			} else {
				// line
			}
			
			for (OsmTag tag : rel.getTags()) {
				if ("water".equals(tag.getValue()) || "waterway".equals(tag.getKey())) {
					isWater = true;
				}
			}
			
			if (isWater) {
				for (Polygon p : polygons) {
					data.getWaterAreas().add(new WaterArea(p));
				}
			} else {
				for (Polygon p : polygons) {
					data.getGreenAreas().add(new GreenArea(p));
				}
			}

		}
	}

	private static void parseWays(OsmData data, OSM osm) {
		for (OsmWay way : osm.getWays()) {
			boolean water = false;
			// search if it is water of green stuff
			for (OsmTag tag : way.getTags()) {
				if ("water".equals(tag.getValue()) || "waterway".equals(tag.getKey())) {
					water = true;
				}
			}

			List<Coordinate> coordinates = new ArrayList<>();
			for (WayNode nd : way.getNodes()) {
				ProjCoordinate converted = convertCoordinatesFrom84(nd.getLon(), nd.getLat());
				coordinates.add(new Coordinate(converted.x, converted.y));
			}

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

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

	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 = GEOM_FACTORY
					.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 = GEOM_FACTORY
						.createLinearRing(innerRing.toArray(new Coordinate[innerRing.size()]));
				innerLinearRings.add(innerLinearRing);
			}

			Polygon polygon = GEOM_FACTORY.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 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 = GEOM_FACTORY
						.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getWaterways().add(new Waterway(lineString));
			} else {
				Polygon polygon = GEOM_FACTORY.createPolygon(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getWaterAreas().add(new WaterArea(polygon));
			}
		} else {
			if (line) {
				LineString lineString = GEOM_FACTORY
						.createLineString(coordinates.toArray(new Coordinate[coordinates.size()]));
				data.getTreeRows().add(new TreeRow(lineString));
			} else {
				Polygon polygon = GEOM_FACTORY.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 void parseTrees(List<TreePoint> treePoints, List<OsmNode> nodes) {
		for (OsmNode node : nodes) {
			ProjCoordinate converted = convertCoordinatesFrom84(node.getLon(), node.getLat());
			Point point = GEOM_FACTORY.createPoint(new Coordinate(converted.x, converted.y));
			treePoints.add(new TreePoint(point));
		}
	}

	public 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;
	}

	public static String extractAndConvertBoundingBox(CityModel cityModel, 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 = GEOM_FACTORY.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;
	}


	public static void convertRoadAreasToCityGML(CityModel cityModel, OsmData osmData) {
		for (RoadArea roadArea : osmData.getRoadAreas()) {
			Road road = new Road();
			org.xmlobjects.gml.model.geometry.primitives.Polygon poly = convertToCityGmlPoly(roadArea.getArea());
			if (poly == null) {
				System.out.println("Skipping RoadArea: " + roadArea.getArea());
				continue;
			}
			MultiSurface ms = new MultiSurface();
			road.setId(UUID.randomUUID().toString());
			ms.getSurfaceMember().add(new SurfaceProperty(poly));
			road.getDeprecatedProperties().setLod1MultiSurface(new MultiSurfaceProperty(ms));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(road));
		}
	}


	public static void converLandUseAreasToCityGML(CityModel cityModel, OsmData osmData) {
		for (LandUseArea landUseArea : osmData.getLandUseAreas()) {
			LandUse landUse = new LandUse();
			org.xmlobjects.gml.model.geometry.primitives.Polygon poly = convertToCityGmlPoly(landUseArea.getArea());
			if (poly == null) {
				System.out.println("Skipping RoadArea: " + landUseArea.getArea());
				continue;
			}
			MultiSurface ms = new MultiSurface();
			landUse.setId(UUID.randomUUID().toString());
			ms.getSurfaceMember().add(new SurfaceProperty(poly));
			landUse.setLod0MultiSurface(new MultiSurfaceProperty(ms));
			cityModel.getCityObjectMembers().add(new AbstractCityObjectProperty(landUse));
		}
	}

}
