package eu.simstadt.regionchooser.fast_xml_parser;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.logging.Logger;
import org.locationtech.jts.algorithm.ConvexHull;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import com.ximpleware.XPathParseException;
import eu.simstadt.regionchooser.RegionChooserUtils;


public class ConvexHullCalculator
{
	private static final Logger LOGGER = Logger.getLogger(ConvexHullCalculator.class.getName());

	private ConvexHullCalculator() {
		throw new IllegalStateException("Utility class");
	}

	/**
	 * Relatively fast method to extract a convex hull in WGS84 for any Citygml file for which CRS is known and whose
	 * size is less than 2GB (VTD XML limitation). It iterates over every building, gets the bounding box as 4
	 * coordinates and calculates a convex hull of all those coordinates at the end of the iteration. It finally converts
	 * this convex hull to WGS84 coordinates.
	 *
	 * The convex hull is needed for RegionChooser. No SimstadtModel or CityDoctor model is needed, thanks to
	 * eu.simstadt.regionchooser.fast_xml_parser.CityGmlIterator.
	 *
	 * E.g. : POLYGON ((9.219282617376651 48.876828283254675, 9.2175568365387 48.87695546490524, 9.213228008654541
	 * 48.87741235218009, 9.21293830332426 48.8774437308139, 9.212628150503749 48.87995232037036, 9.21263222062228
	 * 48.880912500705406, 9.213601288895058 48.88138432918416, 9.213701498914048 48.8813841435154, 9.217160380858063
	 * 48.88092123714512, 9.217396359933812 48.88085245173012, 9.217558989421159 48.880784074008496, 9.219328059150042
	 * 48.879894270949485, 9.219367632516049 48.876847095121995, 9.219367549551574 48.87682812171015, 9.219282617376651
	 * 48.876828283254675))
	 *
	 * for "20140218_Gruenbuehl_LOD2.gml"
	 *
	 * @param citygmlPath
	 * @return convex Hull as jts.geom.Geometry, with the originalCRS written in userData.
	 */
	public static Geometry calculateFromCityGML(Path citygmlPath) throws XPathParseException, IOException {

		GeometryFactory geometryFactory = new GeometryFactory();
		ArrayList<Coordinate> allPoints = new ArrayList<>();
		CityGmlIterator citygml = new CityGmlIterator(citygmlPath);
		for (BuildingXmlNode buildingXmlNode : citygml) {
			if (buildingXmlNode.hasCoordinates()) {
				allPoints.add(new Coordinate(buildingXmlNode.xMin, buildingXmlNode.yMin));
				allPoints.add(new Coordinate(buildingXmlNode.xMin, buildingXmlNode.yMax));
				allPoints.add(new Coordinate(buildingXmlNode.xMax, buildingXmlNode.yMin));
				allPoints.add(new Coordinate(buildingXmlNode.xMax, buildingXmlNode.yMax));
			}
		}

		Coordinate[] pointsArray = allPoints.toArray(new Coordinate[allPoints.size()]);
		ConvexHull ch = new org.locationtech.jts.algorithm.ConvexHull(pointsArray, geometryFactory);

		// Convert convex hull in original coordinates to WGS84 coordinates.
		// NOTE: It's faster to convert to WGS84 once the convex hull is calculated, because there are fewer points
		Polygon originalConvexHull = (Polygon) ch.getConvexHull();

		CoordinateReferenceSystem originalCRS = RegionChooserUtils.crsFromCityGMLHeader(citygmlPath);
		Polygon convexHull = RegionChooserUtils.changePolygonCRS(originalConvexHull, originalCRS,
				RegionChooserUtils.WGS84);
		convexHull.setUserData(originalCRS.toString());
		return convexHull;
	}

	/**
	 * Finds every CityGML in a folder recursively, extract their convex hull and send them to callback. Used by
	 * RegionChooser Javascript to display the CityGMLs.
	 *
	 * If hull has been extracted, get it from cache, calculate it otherwise.
	 *
	 * @param repository
	 * @param callback
	 * @throws IOException
	 */
	public static void extractHullsForEveryCityGML(Path repository, Consumer<String> callback) throws IOException {
		LOGGER.info("Parsing " + repository);
		RegionChooserUtils.everyCityGML(repository)
				.map(gmlPath -> {
					LOGGER.fine("Importing " + repository.relativize(gmlPath));
					try {
						Path kmlPath = getHullPath(repository, gmlPath);
						if (Files.exists(kmlPath)) {
							LOGGER.fine("Using cache from " + repository.relativize(kmlPath));
							return new String(Files.readAllBytes(kmlPath), StandardCharsets.UTF_8);
						} else {
							LOGGER.info("No hull found in cache for " + repository.relativize(gmlPath) + ". Creating it!");
							return calculateConvexHullPlacemark(gmlPath, kmlPath, false);
						}
					} catch (IOException ex) {
						ex.printStackTrace();
						return null;
					}
				})
				.filter(Objects::nonNull)
				.forEach(hullKML -> {
					if (hullKML.contains("<LinearRing>")) {
						callback.accept(hullKML);
					} else {
						LOGGER.warning("Cache is empty!");
					}
				});
	}

	/**
	 * Writes a KML cache file for the convex hull of a CityGML file and returns the KML content as string. Depending on
	 * throwIfError, the method will either ignore any problem (and write the message inside the KML file (preventing the
	 * process from happening again next time) or stop the process, without returning or caching anything.
	 *
	 * @param gmlPath
	 * @param kmlPath
	 * @param throwIfError
	 * @return the KML content as a String
	 */
	public static String calculateConvexHullPlacemark(Path gmlPath, Path kmlPath, boolean throwIfError) {
		String result;
		try {
			Geometry convexHull = ConvexHullCalculator.calculateFromCityGML(gmlPath);
			//NOTE: Header and footer with <kml><Document> don't seem to be needed. They're easier to concatenate without.
			StringBuilder kmlPlacemark = new StringBuilder();
			kmlPlacemark.append("    <Placemark>\n");
			kmlPlacemark.append("      <name>");
			kmlPlacemark.append(gmlPath.getFileName());
			kmlPlacemark.append("</name>\n");
			kmlPlacemark.append("      <ExtendedData>\n");
			kmlPlacemark.append("        <Data name=\"project\">\n");
			kmlPlacemark.append("          <value>");
			kmlPlacemark.append(gmlPath.getParent().getFileName().toString().replace(".proj", ""));
			kmlPlacemark.append("</value>\n");
			kmlPlacemark.append("        </Data>\n");
			kmlPlacemark.append("        <Data name=\"srsName\">\n");
			kmlPlacemark.append("          <value>");
			kmlPlacemark.append(convexHull.getUserData());
			kmlPlacemark.append("</value>\n");
			kmlPlacemark.append("        </Data>\n");
			kmlPlacemark.append("      </ExtendedData>\n");
			kmlPlacemark.append("      <Polygon>\n");
			kmlPlacemark.append("        <outerBoundaryIs>\n");
			kmlPlacemark.append("          <LinearRing>\n");
			kmlPlacemark.append("            <tessellate>1</tessellate>\n");
			kmlPlacemark.append("            <coordinates>");
			for (Coordinate coordinate : convexHull.getCoordinates()) {
				kmlPlacemark.append(coordinate.x);
				kmlPlacemark.append(",");
				kmlPlacemark.append(coordinate.y);
				kmlPlacemark.append(",0 ");
			}
			kmlPlacemark.append("</coordinates>\n");
			kmlPlacemark.append("          </LinearRing>\n");
			kmlPlacemark.append("        </outerBoundaryIs>\n");
			kmlPlacemark.append("      </Polygon>\n");
			kmlPlacemark.append("    </Placemark>\n");
			result = kmlPlacemark.toString();
		} catch (Exception ex) {
			// Something went wrong.
			LOGGER.severe("Couldn't calculate hull : " + ex.getMessage());
			// Either throw again:
			if (throwIfError) {
				throw new RuntimeException(ex);
			}
			// Or write the error message inside a comment in the KML cache file. It could be dangerous (the file doesn't contain any geometry)
			// but will prevent the same process from happening next time again. This could be desirable if the Error happens at the end of a 2GB file.
			StringWriter errors = new StringWriter();
			ex.printStackTrace(new PrintWriter(errors));
			result = "<kml>\n<!--\n" + errors.toString() + "\n-->\n</kml>";
		}
		try (BufferedWriter writer = Files.newBufferedWriter(kmlPath)) {
			writer.write(result);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		LOGGER.info("Convex hull written to cache.");
		return result;
	}

	/**
	 * For a given CityGML Path, returns the Path at which KML hull should be cached. If needed it creates the '.cache'
	 * folder and hides it. The '.cache' folder isn't specific to the project: every kml cache file is written inside the
	 * same repository '.cache' folder.
	 *
	 * The original CityGML filesize is written in KML basename, in order to update the hull if the CityGML file is
	 * modified, e.g. LoD2_564_5512_2_BY_11946489.kml
	 *
	 * @param repository
	 * @param citygmlPath
	 *
	 * @return KML Path
	 * @throws IOException
	 */
	private static Path getHullPath(Path repository, Path citygmlPath) throws IOException {
		String kmlFilename = citygmlPath.getFileName().toString().replaceAll("(?i)\\.gml$",
				"_" + Files.size(citygmlPath) + ".kml");
		Path cacheFolder = repository.resolve(".cache/");
		Path hullsFolder = cacheFolder.resolve("hulls/");
		Files.createDirectories(hullsFolder);
		try {
			Files.setAttribute(cacheFolder, "dos:hidden", true);
		} catch (UnsupportedOperationException ex) {
			// do nothing for non-windows os
		}
		return hullsFolder.resolve(kmlFilename);
	}
}
