package eu.simstadt.regionchooser;

import java.io.IOException;
import java.nio.file.Path;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import com.ximpleware.NavException;
import com.ximpleware.XPathEvalException;
import com.ximpleware.XPathParseException;
import eu.simstadt.geo.fast_xml_parser.BuildingXmlNode;
import eu.simstadt.geo.fast_xml_parser.CityGmlIterator;


public class RegionExtractor
{
	private static final WKTReader wktReader = new WKTReader();

	private static final Logger LOGGER = Logger.getLogger(RegionExtractor.class.getName());
	private static final GeometryFactory gf = new GeometryFactory();

	/**
	 * Main method behind RegionChooser. Given a CityGML (as Path) and a geometry (as Well-known text POLYGON, in the
	 * same coordinate system as the CityGML), it iterates over each Building and checks if the building is inside the
	 * geometry. It only works with CityGML files smaller than 2GB. It uses VTD-XML parser instead of a whole
	 * Simstadt/Citydoctor/Citygml model.
	 * 
	 * 
	 * @param citygmlPath
	 * @param wktPolygon
	 * @param string
	 * @return a StringBuffer, full with the extracted Citygml, including header, buildings and footer.
	 * @throws ParseException
	 * @throws IOException
	 * @throws XPathEvalException
	 * @throws NavException
	 * @throws XPathParseException
	 * @throws NumberFormatException
	 */
	static StringBuilder selectRegionDirectlyFromCityGML(Path citygmlPath, String wktPolygon, String srsName)
			throws ParseException, XPathParseException, NavException, XPathEvalException,
			IOException {

		int buildingsCount = 0;
		int foundBuildingsCount = 0;
		StringBuilder sb = new StringBuilder();
		Geometry poly = wktReader.read(wktPolygon);

		CityGmlIterator citygml = new CityGmlIterator(citygmlPath);
		for (BuildingXmlNode buildingXmlNode : citygml) {
			if (buildingsCount == 0) {
				sb.append(replaceEnvelopeInHeader(citygml.getHeader(), poly.getEnvelopeInternal(), srsName));
			}
			buildingsCount += 1;
			if (buildingXmlNode.hasCoordinates()) {
				Coordinate coord = new Coordinate(buildingXmlNode.x, buildingXmlNode.y);
				Point point = gf.createPoint(coord);
				if (point.within(poly)) {
					foundBuildingsCount++;
					sb.append(buildingXmlNode.toString());
				}
			}
			if (buildingsCount % 1000 == 0) {
				LOGGER.info("1000 buildings parsed");
			}
		}

		LOGGER.info("Buildings found in selected region " + foundBuildingsCount);
		sb.append(citygml.getFooter());
		return sb;
	}

	/**
	 * Some Citygml files include an envelope (bounding box), defined at the very beginning of the file. If the extracted
	 * region comes from a huge file (e.g. from NYC), it might inherit this header with a huge envelope. Some methods
	 * might get confused by this wrong envelope, so this method replaces the original envelope with the bounding box
	 * from the extracting polygon. The real envelope might be even smaller, but it could only be known at the end of the
	 * parsing, after having analyzed every building. The envelope should be written in the header. If present, min and
	 * max values for Z are kept.
	 * 
	 * @param header
	 * @param envelope
	 * @param srsName
	 * @return CityGML Header with an updated envelope
	 */
	private static String replaceEnvelopeInHeader(String header, Envelope envelope, String srsName) {
		//NOTE: Sorry for using a regex to parse XML. The header in itself isn't a valid XML, so this looked like the easiest solution.
		double zMin = 0;
		double zMax = 0;
		Pattern boundedByPattern = Pattern.compile(
				"(?is)<gml:boundedBy>.*?<gml:lowerCorner>(.*?)</gml:lowerCorner>\\s*<gml:upperCorner>(.*?)</gml:upperCorner>.*?</gml:boundedBy>");
		Matcher matcher = boundedByPattern.matcher(header);
		String headerWithoutEnvelope = header;
		if (matcher.find()) {
			headerWithoutEnvelope = matcher.replaceFirst("");
			String[] mins = matcher.group(1).split("\\s+");
			String[] maxs = matcher.group(2).split("\\s+");
			if (mins.length > 2) {
				zMin = Double.valueOf(mins[2]);
			}
			if (maxs.length > 2) {
				zMax = Double.valueOf(maxs[2]);
			}
		}
		String newEnvelope = "<gml:boundedBy>\r\n" +
				"  <gml:Envelope srsName=\"" + srsName + "\" srsDimension=\"3\">\r\n" + //NOTE: Would srsDimension="2" be better? Should the original Z get extracted?
				"    <gml:lowerCorner>" + envelope.getMinX() + " " + envelope.getMinY() + " " + zMin
				+ "</gml:lowerCorner>\r\n" +
				"    <gml:upperCorner>" + envelope.getMaxX() + " " + envelope.getMaxY() + " " + zMax
				+ "</gml:upperCorner>\r\n" +
				"  </gml:Envelope>\r\n" +
				"</gml:boundedBy>\r\n";
		return headerWithoutEnvelope + newEnvelope;
	}
}
