/*
 * Copyright (C) 2016 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
 * Karlsruhe, Germany.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.fraunhofer.iosb.ilt.frostserver.util;

import java.io.IOException;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.geojson.GeoJsonObject;
import org.geojson.LineString;
import org.geojson.LngLatAlt;
import org.geojson.Point;
import org.geojson.Polygon;

/**
 *
 * @author jab
 */
public class GeoHelper {

    private static final String NUMBER_REGEX = "[+-]?[0-9]*(\\.[0-9]+)?";
    private static final String POINT_2D_REGEX = NUMBER_REGEX + "\\s+" + NUMBER_REGEX;
    private static final String POINT_3D_REGEX = POINT_2D_REGEX + "\\s+" + NUMBER_REGEX;
    private static final String LIST_POINT_2D_REGEX = POINT_2D_REGEX + "(?:\\s*,\\s*" + POINT_2D_REGEX + ")*";
    private static final String LIST_POINT_3D_REGEX = POINT_3D_REGEX + "(?:\\s*,\\s*" + POINT_3D_REGEX + ")*";
    private static final String LIST_LIST_POINT_2D_REGEX = "[(]" + LIST_POINT_2D_REGEX + "[)](?:\\s*,\\s*[(]" + LIST_POINT_2D_REGEX + "[)])*";
    private static final String LIST_LIST_POINT_3D_REGEX = "[(]" + LIST_POINT_3D_REGEX + "[)](?:\\s*,\\s*[(]" + LIST_POINT_3D_REGEX + "[)])*";

    private static final String WKT_POINT_REGEX = "POINT\\s*[(](" + POINT_2D_REGEX + "|" + POINT_3D_REGEX + ")[)]";
    private static final String WKT_LINE_REGEX = "LINESTRING\\s*[(](" + LIST_POINT_2D_REGEX + "|" + LIST_POINT_3D_REGEX + ")[)]";
    private static final String WKT_POLYGON_REGEX = "POLYGON( Z)?\\s*[(](" + LIST_LIST_POINT_2D_REGEX + "|" + LIST_LIST_POINT_3D_REGEX + ")[)]";

    public static final Pattern WKT_POINT_PATTERN = Pattern.compile(WKT_POINT_REGEX, Pattern.CASE_INSENSITIVE);
    public static final Pattern WKT_LINE_PATTERN = Pattern.compile(WKT_LINE_REGEX, Pattern.CASE_INSENSITIVE);
    public static final Pattern WKT_POLYGON_PATTERN = Pattern.compile(WKT_POLYGON_REGEX, Pattern.CASE_INSENSITIVE);

    private static final String DOES_NOT_MATCH_PATTERN = "' does not match pattern '";

    private GeoHelper() {

    }

    public static Point parsePoint(String value) {
        Matcher matcher = GeoHelper.WKT_POINT_PATTERN.matcher(value.trim());
        if (matcher.matches()) {
            String[] coordinates = matcher.group(1).split(" ");
            if (coordinates.length < 2 || coordinates.length > 3) {
                throw new IllegalArgumentException("only 2d or 3d points are supported");
            }
            if (coordinates.length == 2) {
                return new Point(Double.parseDouble(coordinates[0]), Double.parseDouble(coordinates[1]));
            } else {
                return new Point(Double.parseDouble(coordinates[0]), Double.parseDouble(coordinates[1]), Double.parseDouble(coordinates[2]));
            }
        } else {
            throw new IllegalArgumentException("'" + value + DOES_NOT_MATCH_PATTERN + GeoHelper.WKT_POINT_PATTERN.pattern() + "'");
        }
    }

    public static LineString parseLine(String value) {
        Matcher matcher = GeoHelper.WKT_LINE_PATTERN.matcher(value.trim());
        if (matcher.matches()) {
            String[] points = matcher.group(1).split("\\s*,\\s*");
            return new LineString(
                    Arrays.asList(points).stream()
                            .map(x -> Arrays.asList(x.split(" "))) //split each point in coorinates array
                            .map(x -> x.stream().map(Double::parseDouble)) // parse each coordinate to double
                            .map(x -> getPoint(x.toArray(size -> new Double[size])).getCoordinates()) //collect double coordinate into double[] and convert to Point
                            .toArray(size -> new LngLatAlt[size]));
        } else {
            throw new IllegalArgumentException("'" + value + DOES_NOT_MATCH_PATTERN + GeoHelper.WKT_LINE_PATTERN.pattern() + "'");
        }
    }

    private static LngLatAlt[] stringListToPoints(String value) {
        return Arrays.asList(value.split("\\s*,\\s*")).stream()
                .map(x -> Arrays.asList(x.split(" "))) //split each point in coorinates array
                .map(x -> x.stream().map(Double::parseDouble)) // parse each coordinate to double
                .map(x -> getPoint(x.toArray(size -> new Double[size])).getCoordinates()) //collect double coordinate into double[] and convert to Point
                .toArray(size -> new LngLatAlt[size]);
    }

    public static Polygon parsePolygon(String value) {
        Matcher matcher = GeoHelper.WKT_POLYGON_PATTERN.matcher(value.trim());
        if (matcher.matches()) {
            // definition of GeoJson Polygon:
            // First parameter is exterior ring, all others are interior rings
            String group = matcher.group(2);
            String[] rings = group.trim().substring(1, group.length() - 1).split("[)]\\s*,\\s*[(]");
            Polygon result = new Polygon(stringListToPoints(rings[0]));
            for (int i = 1; i < rings.length; i++) {
                // add interior rings
                result.addInteriorRing(stringListToPoints(rings[i]));
            }
            return result;
        } else {
            throw new IllegalArgumentException("'" + value + DOES_NOT_MATCH_PATTERN + GeoHelper.WKT_POLYGON_PATTERN.pattern() + "'");
        }
    }

    public static GeoJsonObject parseWkt(String value) {
        try {
            return parsePoint(value);
        } catch (IllegalArgumentException e1) {
            // Not a Point
        }
        try {
            return parseLine(value);
        } catch (IllegalArgumentException e2) {
            // Not a LineString
        }
        try {
            return parsePolygon(value);
        } catch (IllegalArgumentException e3) {
            // Not a Polygon
        }
        throw new IllegalArgumentException("unknown WKT string format '" + value + "'");
    }

    public static <T extends Number> Point getPoint(T... values) {
        if (values == null || values.length < 2 || values.length > 3) {
            throw new IllegalArgumentException("values must have a length of 2 or 3.");
        }
        if (values.length == 2) {
            return new Point(values[0].doubleValue(), values[1].doubleValue());
        }
        return new Point(values[0].doubleValue(), values[1].doubleValue(), values[2].doubleValue());
    }

    public static GeoJsonObject parseGeoJson(String geoJsonString) throws IOException {
        return SimpleJsonMapper.getSimpleObjectMapper().readValue(geoJsonString, GeoJsonObject.class);
    }
}