package eu.simstadt.regionchooser;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.locationtech.proj4j.BasicCoordinateTransform;
import org.locationtech.proj4j.CRSFactory;
import org.locationtech.proj4j.CoordinateReferenceSystem;
import org.locationtech.proj4j.ProjCoordinate;


public class RegionChooserUtils
{
	private static final CRSFactory CRS_FACTORY = new CRSFactory();
	public static final CoordinateReferenceSystem WGS84 = CRS_FACTORY.createFromName("EPSG:4326");
	private static final Pattern srsNamePattern = Pattern.compile("(?i)(?<=srsName=[\"'])[^\"']+(?=[\"'])");
	private static final int CITYGML_HEADER_LENGTH = 50;
	private static final String EPSG = "EPSG:";

	private RegionChooserUtils() {
		// only static use
	}

	/**
	 * The srsName (The name by which this reference system is identified) inside the CityGML file can have multiple
	 * formats. This method tries to parse the string and detect the corresponding reference system. If it is found, it
	 * returns a proj4j.CoordinateReferenceSystem. It throws an IllegalArgumentException otherwise.
	 *
	 * This method should be able to parse any EPSG id : e.g. "EPSG:1234". German Citygmls might also have "DE_DHDN_3GK3"
	 * or "ETRS89_UTM32" as srsName, so those are also included. It isn't guaranteed that those formats are correctly
	 * parsed, though.
	 *
	 * The EPSG ids and parameters are defined in resources ('nad/epsg') inside proj4j-0.1.0.jar. Some EPSG ids are
	 * missing though, e.g. 7415
	 *
	 * @param srsName
	 * @return CoordinateReferenceSystem
	 */
	public static CoordinateReferenceSystem crsFromSrsName(String srsName) {
		// EPSG:31467
		Pattern pEPSG = Pattern.compile("^(EPSG:\\d+)$");
		Matcher mEPSG = pEPSG.matcher(srsName);
		if (mEPSG.find()) {
			return CRS_FACTORY.createFromName(srsName);
		}

		// urn:ogc:def:crs,crs:EPSG:6.12:31467,crs:EPSG:6.12:5783
		//   or
		// urn:ogc:def:crs,crs:EPSG::28992
		Pattern pOGC = Pattern.compile("urn:ogc:def:crs(?:,crs)?:EPSG:[\\d\\.]*:([\\d]+)\\D*");
		Matcher mOGC = pOGC.matcher(srsName);
		if (mOGC.find()) {
			return CRS_FACTORY.createFromName(EPSG + mOGC.group(1));
		}
		// urn:adv:crs:DE_DHDN_3GK3*DE_DHHN92_NH
		// urn:adv:crs:ETRS89_UTM32*DE_DHHN92_NH
		Pattern pURN = Pattern.compile("urn:adv:crs:([^\\*]+)");
		Matcher mURN = pURN.matcher(srsName);
		if (mURN.find()) {
			String shortSrsName = mURN.group(1);

			// Gauss Krueger:
			if (shortSrsName.startsWith("DE_DHDN_3GK")) {
				int gaussKruegerBaseEPSG = 31464;
				int gaussKruegerId = Integer.parseInt(shortSrsName.substring(11));
				return CRS_FACTORY.createFromName(EPSG + (gaussKruegerBaseEPSG + gaussKruegerId));
			}

			// UTM North:
			if (shortSrsName.startsWith("ETRS89_UTM")) {
				int utmBaseEPSG = 25800;
				int utmId = Integer.parseInt(shortSrsName.substring(10));
				return CRS_FACTORY.createFromName(EPSG + (utmBaseEPSG + utmId));
			}
		}
		throw new IllegalArgumentException("Unknown srsName format: " + srsName);
	}

	/**
	 * Converts a jts.geom.Polygon from one CoordinateReferenceSystem to another.
	 *
	 * NOTE: It would be easier with org.geotools.referencing.CRS instead of Proj4J
	 *
	 * @param polygonInOriginalCRS
	 * @param originalCRS
	 * @param newCRS
	 * @return
	 */
	public static Polygon changePolygonCRS(Polygon polygonInOriginalCRS, CoordinateReferenceSystem originalCRS,
			CoordinateReferenceSystem newCRS) {
		GeometryFactory geometryFactory = new GeometryFactory();
		Coordinate[] convexHullcoordinates = polygonInOriginalCRS.getCoordinates();
		BasicCoordinateTransform transformToWgs84 = new BasicCoordinateTransform(originalCRS, newCRS);
		for (int i = 0; i < convexHullcoordinates.length; i++) {
			ProjCoordinate wgs84Coordinate = transformToWgs84.transform(
					new ProjCoordinate(convexHullcoordinates[i].x, convexHullcoordinates[i].y), new ProjCoordinate());
			convexHullcoordinates[i] = new Coordinate(wgs84Coordinate.x, wgs84Coordinate.y);
		}

		return geometryFactory.createPolygon(convexHullcoordinates);
	}

	public static String wktPolygonToLocalCRS(String wktPolygonInWGS84, CoordinateReferenceSystem localCRS)
			throws ParseException {
		final WKTReader wktReader = new WKTReader();
		final WKTWriter wktWriter = new WKTWriter();
		// WKT coordinates are in WGS84, so should be first converted to srsName
		Polygon wgs84Polygon = (Polygon) wktReader.read(wktPolygonInWGS84);
		return wktWriter.write(changePolygonCRS(wgs84Polygon, WGS84, localCRS));
	}

	/**
	 *
	 * Fast scan of the 50 first lines of a Citygml file to look for srsName. It might not be as reliable as parsing the
	 * whole CityGML, but it should be much faster and use much less memory. For a more reliable solution, use
	 * GeoCoordinatesAccessor. This solution can be convenient for Web services, RegionChooser or HullExtractor.
	 *
	 * @param citygmlPath
	 * @return
	 * @throws IOException
	 */
	public static CoordinateReferenceSystem crsFromCityGMLHeader(Path citygmlPath) throws IOException {
		try (Stream<String> lines = Files.lines(citygmlPath).limit(CITYGML_HEADER_LENGTH)) {
			Optional<String> line = lines.filter(srsNamePattern.asPredicate()).findFirst();
			if (line.isPresent()) {
				Matcher matcher = srsNamePattern.matcher(line.get());
				matcher.find();
				return crsFromSrsName(matcher.group());
			} else {
				throw new IllegalArgumentException("No srsName found in the header of " + citygmlPath);
			}
		}
	}

	/**
	 * Finds every CityGML in every .proj folder in a repository.
	 *
	 * @param repository
	 * @return a stream of CityGML Paths
	 * @throws IOException
	 */
	public static Stream<Path> everyCityGML(Path repository) throws IOException {
		return Files.walk(repository, 2)
				.sorted()
				.filter(
						p -> Files.isRegularFile(p) &&
								p.getParent().toString().endsWith(".proj") &&
								p.toString().toLowerCase().endsWith(".gml"));
	}

	/**
	 * Returns application version, if it has been written in the JAR file during deployment.
	 *
	 * e.g. "0.9.1-SNAPSHOT (rev. 73cbe48e, 2018-07-20)"
	 */
	public static String getApplicationVersion() {
		Package regionChooserJar = RegionChooserFX.class.getPackage();
		return Objects.toString(regionChooserJar.getImplementationVersion(),
				String.format("development version (%s)", LocalDate.now()));
	}
}
