/// <reference types="google.maps" preserve="true" />
import { Loader } from '@googlemaps/js-api-loader';
import { CoordsCannotBeParsedError, GeocoderFailedError, GeolocationUnsupportedError, GoogleMapsAPINotFoundError, IsNotAnObjectError, LocationNotFoundError, ProviderNotSupportedError, QUERY_FIELDS, QUERY_TYPES } from './Location.types';
const DegreeRegexChunks = [
    /^[-+]?/,
    // latitude degrees
    /([0-9]|[1-8][0-9]|90)(\.\d+)?°/,
    /\s?/,
    // latitude direction
    /(N|S)?/,
    // sepeartor between latitude and longitude
    /(,|,\s|\s)/,
    /[-+]?/,
    // longitude degrees
    /([0-9]|[1-9][0-9]|1[0-7][0-9]|180)(\.\d+)?°/,
    /\s?/,
    // longitude direction
    /(W|E)?$/
];
const DegreeRegularExpression = new RegExp(DegreeRegexChunks.map(c => c.source).join(''));
const DMRegexChunks = [
    /^[-+]?/,
    // latitude degrees
    /([0-9]|[1-8][0-9]|90)°/,
    /\s?/,
    // latitude minutes
    /([0-9]|[0-5][0-9])(\.\d+)?('|’)/,
    /\s?/,
    // latitude direction
    /(N|S)?/,
    // sepeartor between latitude and longitude
    /(,|,\s|\s)/,
    /[-+]?/,
    // longitude degrees
    /([0-9]|[1-9][0-9]|1[0-7][0-9]|180)°/,
    /\s?/,
    // longitude minutes
    /([0-9]|[0-5][0-9])(\.\d+)?('|’)/,
    /\s?/,
    // longitude direction
    /(W|E)?$/
];
const DMRegularExpression = new RegExp(DMRegexChunks.map(c => c.source).join(''));
const DMSRegexChunks = [
    /^[-+]?/,
    // latitude degrees
    /([0-9]|[1-8][0-9]|90)°/,
    /\s?/,
    // latitude minutes
    /([0-9]|[0-5][0-9])('|’)/,
    /\s?/,
    // latitude seconds
    /([0-9]|[0-5][0-9])(\.\d+)?("|″)/,
    /\s?/,
    // latitude direction
    /(N|S)?/,
    // sepeartor between latitude and longitude
    /(,|,\s|\s)/,
    /[-+]?/,
    // longitude degrees
    /([0-9]|[1-9][0-9]|1[0-7][0-9]|180)°/,
    /\s?/,
    // longitude minutes
    /([0-9]|[0-5][0-9])('|’)/,
    /\s?/,
    // longitude seconds
    /([0-9]|[0-5][0-9])(\.\d+)?("|″)/,
    /\s?/,
    // longitude direction
    /(W|E)?$/
];
const DMSRegularExpression = new RegExp(DMSRegexChunks.map(c => c.source).join(''));
const decimalRegexChunks = [
    /^[-+]?/,
    /([0-9]|[1-8][0-9]|90)/,
    /(\.\d+)?/,
    /(,|\s|,\s)/,
    /[-+]?/,
    /([0-9]|[1-9][0-9]|1[0-7][0-9]|180)/,
    /(\.\d+)?$/
];
const decimalFormatCoordinatesRegularExpression = new RegExp(decimalRegexChunks.map(c => c.source).join(''));
const providers = {
    google: {
        name: 'Google',
        loadedPromise: null,
        loadedApiKey: '',
        loadAPI(opts) {
            const loader = new Loader({
                version: 'quarterly',
                apiKey: opts.apiKey ?? '',
                region: opts.region,
                language: opts.language,
                libraries: ['places', 'marker']
            });
            return loader.load().then(() => { }, () => {
                throw new Error(GoogleMapsAPINotFoundError);
            });
        }
    },
    undefined: {
        name: 'undefined',
        loadedPromise: null,
        loadedApiKey: '',
        loadAPI: data => Promise.reject(new Error(`${ProviderNotSupportedError}: ${data.name}`))
    }
};
export async function loadMapsAPI(name = '', opts) {
    if (typeof opts !== 'object' || !opts) {
        throw new Error(IsNotAnObjectError);
    }
    const provider = providers[name] ?? providers.undefined;
    if (!provider.loadedPromise) {
        provider.loadedPromise = provider
            .loadAPI(opts)
            .then(() => {
            provider.loadedApiKey = opts?.apiKey ?? '';
        })
            .catch(() => {
            provider.loadedPromise = null;
        });
    }
    return provider.loadedPromise;
}
/**
 * Identifies coordinates in degree format
 * eg. 45.78°N 9.11°E
 */
export function isValueDegreeFormatCoordinate(value) {
    return DegreeRegularExpression.test(value);
}
/**
 * Identifies coordinates in degree, minute format
 * eg. 45°45.4’N 9°23’E
 */
export function isValueDMCoordinate(value) {
    return DMRegularExpression.test(value);
}
/**
 * Identifies coordinates in degree, minute, second format
 * eg. 45°45’32.4″N 9°23’39.9″E
 */
export function isValueDMSCoordinate(value) {
    return DMSRegularExpression.test(value);
}
/**
 * Identifies coordinates in decimal format
 * eg. 14.678, -38.765
 */
export function isValueACoordinate(value) {
    return (decimalFormatCoordinatesRegularExpression.test(value) ||
        isValueDMSCoordinate(value) ||
        isValueDMCoordinate(value) ||
        isValueDegreeFormatCoordinate(value));
}
/**
 * Converts DMS (degree, minute, second) format latitude/longitude into decimal format.
 */
function convertDMSToDD({ degrees, minutes, seconds }, direction) {
    let dd = degrees;
    if (minutes) {
        if (degrees > 0)
            dd += minutes / 60;
        if (degrees < 0)
            dd -= minutes / 60;
    }
    if (seconds) {
        if (degrees > 0)
            dd += seconds / (60 * 60);
        if (degrees < 0)
            dd -= seconds / (60 * 60);
    }
    if (degrees > 0 && (direction === 'S' || direction === 'W')) {
        dd *= -1;
    }
    return dd;
}
/**
 * Parses DMS (degree, minute, second) format string into decimal format coordinates.
 */
export function parseDMS(input) {
    if (isValueDMSCoordinate(input)) {
        const parts = input.split(/'|’|"\s?|″\s?|°|,?\s|,\s?/);
        const lat = convertDMSToDD({
            degrees: Number(parts[0]),
            minutes: Number(parts[1]),
            seconds: Number(parts[2])
        }, parts[3]);
        const lng = convertDMSToDD({
            degrees: Number(parts[4]),
            minutes: Number(parts[5]),
            seconds: Number(parts[6])
        }, parts[7]);
        return [lat, lng];
    }
    if (isValueDMCoordinate(input)) {
        const parts = input.split(/'\s?|’\s?|°|,?\s|,\s?/);
        const lat = convertDMSToDD({ degrees: Number(parts[0]), minutes: Number(parts[1]) }, parts[2]);
        const lng = convertDMSToDD({ degrees: Number(parts[3]), minutes: Number(parts[4]) }, parts[5]);
        return [lat, lng];
    }
    const parts = input.split(/°\s?|,?\s|,\s?/);
    const lat = convertDMSToDD({ degrees: Number(parts[0]) }, parts[1]);
    const lng = convertDMSToDD({ degrees: Number(parts[2]) }, parts[3]);
    return [lat, lng];
}
export async function getNavigatorPosition() {
    if (!navigator.geolocation)
        throw new Error(GeolocationUnsupportedError);
    return new Promise((resolve, reject) => {
        navigator.geolocation.getCurrentPosition(currentLocation => {
            resolve({
                latitude: currentLocation.coords.latitude,
                longitude: currentLocation.coords.longitude
            });
        }, error => reject(new Error(error.message)));
    });
}
export function toGoogleLatLng(coords) {
    return new google.maps.LatLng(coords.latitude, coords.longitude);
}
/** Returns an array of place prediction objects ('place' can be an establishment, geographic location, or prominent point of interest)
 * and  session token (valid for multiple queries, followed by one place selection).
 */
export async function getPlacePredictions(location, bias = {}) {
    const autocompleteService = new google.maps.places.AutocompleteService();
    const optionalParams = {};
    if (bias?.location) {
        if (bias?.location.center === 'current') {
            await getNavigatorPosition()
                .then(coords => {
                optionalParams.location = toGoogleLatLng(coords);
                optionalParams.radius = bias.location.radius;
            })
                // no-op user didn't allow location
                .catch(() => { });
        }
        else {
            optionalParams.location = toGoogleLatLng(bias.location.center);
            optionalParams.radius = bias.location.radius;
        }
    }
    if (bias?.bounds) {
        const [sw, ne] = bias.bounds;
        optionalParams.bounds = new google.maps.LatLngBounds(toGoogleLatLng(sw), toGoogleLatLng(ne));
    }
    return new Promise((resolve, reject) => {
        const token = new google.maps.places.AutocompleteSessionToken();
        autocompleteService.getPlacePredictions({
            input: location,
            types: QUERY_TYPES,
            sessionToken: token,
            ...optionalParams
        }, (placePredictions, status) => {
            if (status === google.maps.places.PlacesServiceStatus.OK) {
                resolve({ placePredictions: placePredictions ?? [], token });
            }
            else {
                reject(new Error(LocationNotFoundError));
            }
        });
    });
}
export async function getPlace(location, mapElement = document.createElement('div')) {
    const placesService = new google.maps.places.PlacesService(mapElement);
    const { placePredictions, token } = await getPlacePredictions(location);
    return new Promise((resolve, reject) => {
        placesService.getDetails({
            placeId: placePredictions[0]?.place_id,
            fields: QUERY_FIELDS,
            sessionToken: token
        }, (place, pStatus) => {
            if (pStatus === google.maps.places.PlacesServiceStatus.OK) {
                resolve({
                    latitude: place?.geometry?.location?.lat() ?? NaN,
                    longitude: place?.geometry?.location?.lng() ?? NaN
                });
            }
            else {
                reject(new Error(LocationNotFoundError));
            }
        });
    });
}
export async function getPlaceById(placeId, sessionToken = new google.maps.places.AutocompleteSessionToken(), mapElement = document.createElement('div')) {
    const placesService = new google.maps.places.PlacesService(mapElement);
    return new Promise((resolve, reject) => {
        placesService.getDetails({ placeId, fields: QUERY_FIELDS, sessionToken }, (place, pStatus) => {
            if (pStatus === google.maps.places.PlacesServiceStatus.OK) {
                resolve({
                    name: place?.name,
                    address: place?.formatted_address,
                    latitude: place?.geometry?.location?.lat(),
                    longitude: place?.geometry?.location?.lng()
                });
            }
            else {
                reject(new Error(LocationNotFoundError));
            }
        });
    });
}
/** Returns coordinations for given input: either coords, address or current position. */
export async function getCoords(coords, mapElement) {
    if (coords === '' || coords === undefined)
        throw new Error();
    if (coords === 'current')
        return getNavigatorPosition();
    let location;
    if (typeof coords === 'string') {
        let latLng = [];
        if (isValueDMSCoordinate(coords) ||
            isValueDMCoordinate(coords) ||
            isValueDegreeFormatCoordinate(coords)) {
            latLng = parseDMS(coords);
        }
        else {
            let splitter;
            if (coords.includes(', ')) {
                splitter = ', ';
            }
            else if (coords.includes(',')) {
                splitter = ',';
            }
            else if (coords.includes(' ')) {
                splitter = ' ';
            }
            else {
                splitter = ', ';
            }
            latLng = coords.split(splitter).map(Number);
        }
        const [lat, lng] = latLng;
        if (Number.isNaN(lat) || Number.isNaN(lng) || lat === undefined || lng === undefined)
            return getPlace(coords, mapElement);
        location = { latitude: lat, longitude: lng };
    }
    else {
        location = coords;
    }
    if (Number.isFinite(location.latitude) && Number.isFinite(location.longitude))
        return location;
    throw new Error(CoordsCannotBeParsedError);
}
export async function getAddress(coords) {
    const location = {
        lat: coords.latitude,
        lng: coords.longitude
    };
    const geocoder = new google.maps.Geocoder();
    return new Promise((resolve, reject) => {
        geocoder.geocode({ location }, (results, status) => {
            if (status === 'OK') {
                const [result] = results ?? [];
                if (result) {
                    resolve({
                        name: result.formatted_address,
                        address: result.formatted_address,
                        ...(result.geometry && {
                            latitude: result.geometry.location.lat(),
                            longitude: result.geometry.location.lng()
                        })
                    });
                }
                else {
                    resolve({
                        name: `${coords.latitude}, ${coords.longitude}`,
                        latitude: coords.latitude,
                        longitude: coords.longitude
                    });
                }
            }
            else {
                reject(new Error(`${GeocoderFailedError}: ${status}`));
            }
        });
    });
}
export const isLatLngObject = (obj) => {
    return (!!obj &&
        typeof obj === 'object' &&
        'lat' in obj &&
        typeof obj.lat === 'function' &&
        'lng' in obj &&
        typeof obj.lng === 'function');
};
export const getGoogleMapsDirHref = (destination, waypoints, travelMode = 'driving') => {
    const url = new URL('https://www.google.com/maps/dir/?api=1');
    url.searchParams.append('destination', destination);
    url.searchParams.append('waypoints', waypoints.join('|'));
    url.searchParams.append('travelmode', travelMode);
    return url.href;
};
//# sourceMappingURL=utils.js.map