"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCoordinates = parseCoordinates;
exports.parseDecimalCoordinate = parseDecimalCoordinate;
exports.parseCoordinate = parseCoordinate;
exports.roundGpsDecimal = roundGpsDecimal;
exports.parsePosition = parsePosition;
exports.processCoordinate = processCoordinate;
const Number_1 = require("./Number");
const String_1 = require("./String");
// Constants
const MAX_LATITUDE_DEGREES = 90;
const MAX_LONGITUDE_DEGREES = 180;
class CoordinateParseError extends Error {
    constructor(message) {
        super(message);
        this.name = "CoordinateParseError";
    }
}
// Regex to match simple decimal coordinates, like "37.5, -122.5"
const DecimalCoordsRE = /^(-?\d+(?:\.\d+)?)[,\s]+(-?\d+(?:\.\d+)?)$/;
/**
 * Parses a string containing both latitude and longitude coordinates.
 * @param input - String containing both coordinates
 * @returns Object containing latitude and longitude in decimal degrees, or undefined if parsing fails
 * @throws CoordinateParseError if the input format is invalid
 */
function parseCoordinates(input) {
    input = (0, String_1.toS)(input).trim();
    if (input.length === 0) {
        throw new CoordinateParseError("Input string cannot be empty");
    }
    if (DecimalCoordsRE.test(input)) {
        const split = input.split(/[\s,]+/);
        const [latitude, longitude] = split
            .map(Number_1.toFloat)
            .map((ea) => (ea == null ? null : roundGpsDecimal(ea)));
        if (latitude == null || longitude == null) {
            throw new CoordinateParseError("Failed to parse decimal coordinates");
        }
        return { latitude, longitude };
    }
    let latitude;
    let longitude;
    for (const coord of parseStringCoordinates(input)) {
        if (!coord.direction) {
            throw new CoordinateParseError("Direction is required for position parsing");
        }
        if (coord.direction === "S" || coord.direction === "N") {
            if (latitude !== undefined) {
                throw new CoordinateParseError("Multiple latitude values found");
            }
            latitude = toDecimalDegrees(coord);
        }
        else {
            if (longitude != null) {
                throw new CoordinateParseError("Multiple longitude values found");
            }
            longitude = toDecimalDegrees(coord);
        }
    }
    const missing = [];
    if (latitude == null)
        missing.push("latitude");
    if (longitude == null)
        missing.push("longitude");
    if (latitude == null || longitude == null) {
        throw new CoordinateParseError(`Missing ${missing.join(" and ")}`);
    }
    else {
        return { latitude, longitude };
    }
}
/**
 * Parses a string containing two coordinates (latitude and longitude).
 * @param input - String containing coordinates
 * @returns Tuple of [latitude, longitude] coordinates
 */
function parseStringCoordinates(input) {
    if (!input?.trim()) {
        throw new CoordinateParseError("Input string cannot be empty");
    }
    const lat = parseCoordinate(input, true);
    const remainders = lat.remainder;
    if ((0, String_1.blank)(remainders)) {
        throw new CoordinateParseError("Expected multiple coordinates");
    }
    return [lat, parseCoordinate(remainders)];
}
/**
 * Parses a coordinate string in decimal degrees format.
 * @param input - String containing a single coordinate
 * @returns Object containing decimal degrees and direction, or undefined if parsing fails
 * @throws CoordinateParseError if the format is not decimal degrees or direction is missing
 */
function parseDecimalCoordinate(input) {
    if (!input?.trim()) {
        throw new CoordinateParseError("Input string cannot be empty");
    }
    const coord = parseCoordinate(input);
    if (coord.format !== "D") {
        throw new CoordinateParseError("Expected decimal degrees format");
    }
    if (!coord.direction) {
        throw new CoordinateParseError("Missing direction");
    }
    return { decimal: toDecimalDegrees(coord), direction: coord.direction };
}
const DecimalCoordRE = /^(-?\d+(?:\.\d+)?)$/;
/**
 * Parses a single coordinate string into its components.
 * @param input - String containing a single coordinate
 * @param expectRemainders - If true, allow additional text after the coordinate
 * @returns Parsed coordinate object
 * @throws CoordinateParseError if the format is invalid
 */
function parseCoordinate(input, expectRemainders = false) {
    input = (0, String_1.toS)(input).trim();
    if (input.length === 0) {
        throw new CoordinateParseError("Input string cannot be empty");
    }
    if (DecimalCoordRE.test(input)) {
        const f = (0, Number_1.toFloat)(input);
        if (f == null) {
            throw new CoordinateParseError("Failed to parse decimal coordinate");
        }
        const r = roundGpsDecimal(f);
        return {
            degrees: r,
            decimal: r,
            format: "D",
            direction: undefined,
            minutes: undefined,
            seconds: undefined,
            remainder: "",
        };
    }
    const dmsPattern = /^(?<degrees>-?\d+)\s*(?:°|DEG)\s*(?<minutes>\d+)\s*['′]\s*(?<seconds>\d+(?:\.\d+)?)\s*["″]\s?(?<direction>[NSEW])?[\s,]{0,3}(?<remainder>.*)$/i;
    const dmPattern = /^(?<degrees>-?\d+)\s*(?:°|DEG)\s*(?<minutes>\d+(?:\.\d+)?)\s?['′]\s?(?<direction>[NSEW])?(?<remainder>.*)$/i;
    const dPattern = /^(?<degrees>-?\d+(?:\.\d+)?)\s*(?:°|DEG)\s?(?<direction>[NSEW])?(?<remainder>.*)$/i;
    const trimmedInput = input.trimStart();
    let match;
    let format = null;
    if ((match = trimmedInput.match(dmsPattern))) {
        format = "DMS";
    }
    else if ((match = trimmedInput.match(dmPattern))) {
        format = "DM";
    }
    else if ((match = trimmedInput.match(dPattern))) {
        format = "D";
    }
    if (match == null ||
        format == null ||
        (!expectRemainders && !(0, String_1.blank)(match?.groups?.remainder))) {
        throw new CoordinateParseError("Invalid coordinate format. Expected one of:\n" +
            "  DDD° MM' SS.S\" k (deg/min/sec)\n" +
            "  DDD° MM.MMM' k (deg/decimal minutes)\n" +
            "  DDD.DDDDD° (decimal degrees)\n" +
            "  (where k indicates direction: N, S, E, or W)");
    }
    if (!match.groups) {
        throw new CoordinateParseError("Failed to parse coordinate components");
    }
    const { degrees: degreesStr, minutes: minutesStr, seconds: secondsStr, direction: directionStr, remainder, } = match.groups;
    const direction = directionStr?.toUpperCase();
    if (degreesStr == null) {
        throw new CoordinateParseError("Missing degrees in coordinate");
    }
    const degrees = parseFloat(degreesStr);
    let minutes;
    let seconds;
    if (format === "DMS") {
        if (minutesStr == null || secondsStr == null) {
            throw new CoordinateParseError("Missing minutes or seconds in DMS coordinate");
        }
        minutes = parseInt(minutesStr, 10);
        seconds = parseFloat(secondsStr);
        if (minutes >= 60) {
            throw new CoordinateParseError("Minutes must be between 0 and 59");
        }
        if (seconds >= 60) {
            throw new CoordinateParseError("Seconds must be between 0 and 59.999...");
        }
    }
    else if (format === "DM") {
        if (minutesStr == null) {
            throw new CoordinateParseError("Missing minutes in DM coordinate");
        }
        minutes = parseFloat(minutesStr);
        if (minutes >= 60) {
            throw new CoordinateParseError("Minutes must be between 0 and 59.999...");
        }
    }
    const maxDegrees = direction === "N" || direction === "S"
        ? MAX_LATITUDE_DEGREES
        : MAX_LONGITUDE_DEGREES;
    if (Math.abs(degrees) > maxDegrees) {
        throw new CoordinateParseError(`Degrees must be between -${maxDegrees} and ${maxDegrees} for ${direction} direction`);
    }
    const coords = {
        degrees,
        minutes,
        seconds,
        direction,
        format,
        remainder: remainder?.trim(),
    };
    const decimal = toDecimalDegrees(coords);
    return {
        ...coords,
        decimal,
    };
}
function toDecimalDegrees(coord) {
    const degrees = (0, Number_1.toFloat)(coord.degrees) ?? 0;
    const sign = Math.sign(degrees);
    let decimal = Math.abs(degrees);
    decimal += Math.abs((0, Number_1.toFloat)(coord.minutes) ?? 0) / 60.0;
    decimal += Math.abs((0, Number_1.toFloat)(coord.seconds) ?? 0) / 3600.0;
    if (coord.direction === "S" || coord.direction === "W" || sign < 0) {
        decimal = -decimal;
    }
    const maxDegrees = coord.direction === "N" || coord.direction === "S"
        ? MAX_LATITUDE_DEGREES
        : MAX_LONGITUDE_DEGREES;
    const axis = coord.direction === "N" || coord.direction === "S"
        ? "latitude"
        : "longitude";
    if (Math.abs(decimal) > maxDegrees) {
        throw new CoordinateParseError(`Degrees must be between -${maxDegrees} and ${maxDegrees} for ${axis}`);
    }
    // Round to 6 decimal places
    // Most consumer devices can only resolve 4-5 decimal places (1m resolution)
    return roundGpsDecimal(decimal);
}
const MAX_LAT_LON_DIFF = 1;
function roundGpsDecimal(decimal) {
    return (0, Number_1.roundToDecimalPlaces)(decimal, 6);
}
function parsePosition(position) {
    if ((0, String_1.blank)(position))
        return;
    const [lat, lon] = (0, String_1.toS)(position).split(/[, ]+/).map(Number_1.toFloat);
    return lat != null && lon != null ? [lat, lon] : undefined;
}
function processCoordinate(config, warnings) {
    let { value, ref } = config;
    const { geoValue, coordinateType } = config;
    const { expectedRefPositive, expectedRefNegative, max } = config;
    let isInvalid = false;
    // Validate ref is reasonable -- it should either start with
    // expectedRefPositive or expectedRefNegative:
    ref = (0, String_1.toS)(ref).trim().toUpperCase().slice(0, 1);
    if (!(0, String_1.blank)(config.ref) &&
        ref !== expectedRefPositive &&
        ref !== expectedRefNegative) {
        warnings.push(`Invalid GPS${coordinateType}Ref: ${JSON.stringify(config.ref)}.`);
        ref = value < 0 ? expectedRefNegative : expectedRefPositive;
    }
    // Check range
    if (Math.abs(value) > max) {
        isInvalid = true;
        warnings.push(`Invalid GPS${coordinateType}: ${value} is out of range`);
        return { value, ref, isInvalid };
    }
    // Apply hemisphere reference
    if (ref === expectedRefNegative) {
        value = -Math.abs(value);
    }
    // Check for mismatched signs with GeolocationPosition
    if (geoValue != null &&
        Math.abs(Math.abs(geoValue) - Math.abs(value)) < MAX_LAT_LON_DIFF) {
        if (Math.sign(geoValue) !== Math.sign(value)) {
            value = -value;
            warnings.push(`Corrected GPS${coordinateType} sign based on GeolocationPosition`);
        }
        // Force ref to correct value
        const expectedRef = geoValue < 0 ? expectedRefNegative : expectedRefPositive;
        if (ref !== expectedRef) {
            ref = expectedRef;
            if (!(0, String_1.blank)(config.ref)) {
                warnings.push(`Corrected GPS${coordinateType}Ref to ${expectedRef} based on GeolocationPosition`);
            }
        }
    }
    // Ensure ref matches coordinate sign
    const expectedRef = value < 0 ? expectedRefNegative : expectedRefPositive;
    if (ref != null && ref !== expectedRef && !(0, String_1.blank)(config.ref)) {
        warnings.push(`Corrected GPS${coordinateType}Ref to ${ref} to match coordinate sign`);
    }
    ref = expectedRef;
    return { value: roundGpsDecimal(value), ref, isInvalid };
}
//# sourceMappingURL=CoordinateParser.js.map