package eu.simstadt.geo;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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.osgeo.proj4j.BasicCoordinateTransform;
import org.osgeo.proj4j.CRSFactory;
import org.osgeo.proj4j.CoordinateReferenceSystem;
import org.osgeo.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 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);
		//NOTE: Could use a HashMap if the switch/case becomes too long.
		if (mURN.find()) {
			switch (mURN.group(1)) {
			case "DE_DHDN_3GK2":
				return CRS_FACTORY.createFromName("EPSG:31466");
			case "DE_DHDN_3GK3":
				return CRS_FACTORY.createFromName("EPSG:31467");
			case "DE_DHDN_3GK4":
				return CRS_FACTORY.createFromName("EPSG:31468");
			case "DE_DHDN_3GK5":
				return CRS_FACTORY.createFromName("EPSG:31469");
			case "ETRS89_UTM32":
				return CRS_FACTORY.createFromName("EPSG:25832");
			default:
				// nothing found
			}
		}
		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);
	}

	/**
	 * 
	 * 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 {
		Optional<String> line = Files.lines(citygmlPath).limit(50).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"));
	}

}
