import { EventEmitter, Platform, CodedError } from '@unimodules/core';
import invariant from 'invariant';
import ExpoLocation from './ExpoLocation';
const LocationEventEmitter = new EventEmitter(ExpoLocation);
var LocationAccuracy;
(function (LocationAccuracy) {
    LocationAccuracy[LocationAccuracy["Lowest"] = 1] = "Lowest";
    LocationAccuracy[LocationAccuracy["Low"] = 2] = "Low";
    LocationAccuracy[LocationAccuracy["Balanced"] = 3] = "Balanced";
    LocationAccuracy[LocationAccuracy["High"] = 4] = "High";
    LocationAccuracy[LocationAccuracy["Highest"] = 5] = "Highest";
    LocationAccuracy[LocationAccuracy["BestForNavigation"] = 6] = "BestForNavigation";
})(LocationAccuracy || (LocationAccuracy = {}));
var LocationActivityType;
(function (LocationActivityType) {
    LocationActivityType[LocationActivityType["Other"] = 1] = "Other";
    LocationActivityType[LocationActivityType["AutomotiveNavigation"] = 2] = "AutomotiveNavigation";
    LocationActivityType[LocationActivityType["Fitness"] = 3] = "Fitness";
    LocationActivityType[LocationActivityType["OtherNavigation"] = 4] = "OtherNavigation";
    LocationActivityType[LocationActivityType["Airborne"] = 5] = "Airborne";
})(LocationActivityType || (LocationActivityType = {}));
export { LocationAccuracy as Accuracy, LocationActivityType as ActivityType };
export var GeofencingEventType;
(function (GeofencingEventType) {
    GeofencingEventType[GeofencingEventType["Enter"] = 1] = "Enter";
    GeofencingEventType[GeofencingEventType["Exit"] = 2] = "Exit";
})(GeofencingEventType || (GeofencingEventType = {}));
export var GeofencingRegionState;
(function (GeofencingRegionState) {
    GeofencingRegionState[GeofencingRegionState["Unknown"] = 0] = "Unknown";
    GeofencingRegionState[GeofencingRegionState["Inside"] = 1] = "Inside";
    GeofencingRegionState[GeofencingRegionState["Outside"] = 2] = "Outside";
})(GeofencingRegionState || (GeofencingRegionState = {}));
let nextWatchId = 0;
let headingId;
function _getNextWatchId() {
    nextWatchId++;
    return nextWatchId;
}
function _getCurrentWatchId() {
    return nextWatchId;
}
let watchCallbacks = {};
let deviceEventSubscription;
let headingEventSub;
let googleApiKey;
const googleApiUrl = 'https://maps.googleapis.com/maps/api/geocode/json';
export async function getProviderStatusAsync() {
    return ExpoLocation.getProviderStatusAsync();
}
export async function enableNetworkProviderAsync() {
    // If network provider is disabled (user's location mode is set to "Device only"),
    // Android's location provider may not give you any results. Use this method in order to ask the user
    // to change the location mode to "High accuracy" which uses Google Play services and enables network provider.
    // `getCurrentPositionAsync` and `watchPositionAsync` are doing it automatically anyway.
    if (Platform.OS === 'android') {
        return ExpoLocation.enableNetworkProviderAsync();
    }
}
export async function getCurrentPositionAsync(options = {}) {
    return ExpoLocation.getCurrentPositionAsync(options);
}
// Start Compass Module
// To simplify, we will call watchHeadingAsync and wait for one update To ensure accuracy, we wait
// for a couple of watch updates if the data has low accuracy
export async function getHeadingAsync() {
    return new Promise(async (resolve, reject) => {
        try {
            // If there is already a compass active (would be a watch)
            if (headingEventSub) {
                let tries = 0;
                const headingSub = LocationEventEmitter.addListener('Expo.headingChanged', ({ heading }) => {
                    if (heading.accuracy > 1 || tries > 5) {
                        resolve(heading);
                        LocationEventEmitter.removeSubscription(headingSub);
                    }
                    else {
                        tries += 1;
                    }
                });
            }
            else {
                let done = false;
                let subscription;
                let tries = 0;
                subscription = await watchHeadingAsync((heading) => {
                    if (!done) {
                        if (heading.accuracy > 1 || tries > 5) {
                            subscription.remove();
                            resolve(heading);
                            done = true;
                        }
                        else {
                            tries += 1;
                        }
                    }
                    else {
                        subscription.remove();
                    }
                });
                if (done) {
                    subscription.remove();
                }
            }
        }
        catch (e) {
            reject(e);
        }
    });
}
export async function watchHeadingAsync(callback) {
    // Check if there is already a compass event watch.
    if (headingEventSub) {
        _removeHeadingWatcher(headingId);
    }
    headingEventSub = LocationEventEmitter.addListener('Expo.headingChanged', ({ watchId, heading }) => {
        const callback = watchCallbacks[watchId];
        if (callback) {
            callback(heading);
        }
        else {
            ExpoLocation.removeWatchAsync(watchId);
        }
    });
    headingId = _getNextWatchId();
    watchCallbacks[headingId] = callback;
    await ExpoLocation.watchDeviceHeading(headingId);
    return {
        remove() {
            _removeHeadingWatcher(headingId);
        },
    };
}
// Removes the compass listener and sub from JS and Native
function _removeHeadingWatcher(watchId) {
    if (!watchCallbacks[watchId]) {
        return;
    }
    delete watchCallbacks[watchId];
    ExpoLocation.removeWatchAsync(watchId);
    if (headingEventSub) {
        LocationEventEmitter.removeSubscription(headingEventSub);
        headingEventSub = null;
    }
}
// End Compass Module
function _maybeInitializeEmitterSubscription() {
    if (!deviceEventSubscription) {
        deviceEventSubscription = LocationEventEmitter.addListener('Expo.locationChanged', ({ watchId, location }) => {
            const callback = watchCallbacks[watchId];
            if (callback) {
                callback(location);
            }
            else {
                ExpoLocation.removeWatchAsync(watchId);
            }
        });
    }
}
export async function geocodeAsync(address) {
    return ExpoLocation.geocodeAsync(address).catch(error => {
        const platformUsesGoogleMaps = Platform.OS === 'android' || Platform.OS === 'web';
        if (platformUsesGoogleMaps && error.code === 'E_NO_GEOCODER') {
            if (!googleApiKey) {
                throw new CodedError(error.code, `${error.message} Please set a Google API Key to use geocoding.`);
            }
            return _googleGeocodeAsync(address);
        }
        throw error;
    });
}
export async function reverseGeocodeAsync(location) {
    if (typeof location.latitude !== 'number' || typeof location.longitude !== 'number') {
        throw new TypeError('Location should be an object with number properties `latitude` and `longitude`.');
    }
    return ExpoLocation.reverseGeocodeAsync(location).catch(error => {
        const platformUsesGoogleMaps = Platform.OS === 'android' || Platform.OS === 'web';
        if (platformUsesGoogleMaps && error.code === 'E_NO_GEOCODER') {
            if (!googleApiKey) {
                throw new CodedError(error.code, `${error.message} Please set a Google API Key to use geocoding.`);
            }
            return _googleReverseGeocodeAsync(location);
        }
        throw error;
    });
}
export function setApiKey(apiKey) {
    googleApiKey = apiKey;
}
async function _googleGeocodeAsync(address) {
    const result = await fetch(`${googleApiUrl}?key=${googleApiKey}&address=${encodeURI(address)}`);
    const resultObject = await result.json();
    if (resultObject.status === 'ZERO_RESULTS') {
        return [];
    }
    assertGeocodeResults(resultObject);
    return resultObject.results.map(result => {
        let location = result.geometry.location;
        // TODO: This is missing a lot of props
        return {
            latitude: location.lat,
            longitude: location.lng,
        };
    });
}
async function _googleReverseGeocodeAsync(options) {
    const result = await fetch(`${googleApiUrl}?key=${googleApiKey}&latlng=${options.latitude},${options.longitude}`);
    const resultObject = await result.json();
    if (resultObject.status === 'ZERO_RESULTS') {
        return [];
    }
    assertGeocodeResults(resultObject);
    return resultObject.results.map(result => {
        const address = {};
        result.address_components.forEach(component => {
            if (component.types.includes('locality')) {
                address.city = component.long_name;
            }
            else if (component.types.includes('street_address')) {
                address.street = component.long_name;
            }
            else if (component.types.includes('administrative_area_level_1')) {
                address.region = component.long_name;
            }
            else if (component.types.includes('country')) {
                address.country = component.long_name;
            }
            else if (component.types.includes('postal_code')) {
                address.postalCode = component.long_name;
            }
            else if (component.types.includes('point_of_interest')) {
                address.name = component.long_name;
            }
        });
        return address;
    });
}
// https://developers.google.com/maps/documentation/geocoding/intro
function assertGeocodeResults(resultObject) {
    const { status, error_message } = resultObject;
    if (status !== 'ZERO_RESULTS' && status !== 'OK') {
        if (error_message) {
            throw new CodedError(status, error_message);
        }
        else if (status === 'UNKNOWN_ERROR') {
            throw new CodedError(status, 'the request could not be processed due to a server error. The request may succeed if you try again.');
        }
        throw new CodedError(status, `An error occurred during geocoding.`);
    }
}
// Polyfill: navigator.geolocation.watchPosition
function watchPosition(success, error, options) {
    _maybeInitializeEmitterSubscription();
    const watchId = _getNextWatchId();
    watchCallbacks[watchId] = success;
    ExpoLocation.watchPositionImplAsync(watchId, options).catch(err => {
        _removeWatcher(watchId);
        error({ watchId, message: err.message, code: err.code });
    });
    return watchId;
}
export async function watchPositionAsync(options, callback) {
    _maybeInitializeEmitterSubscription();
    const watchId = _getNextWatchId();
    watchCallbacks[watchId] = callback;
    await ExpoLocation.watchPositionImplAsync(watchId, options);
    return {
        remove() {
            _removeWatcher(watchId);
        },
    };
}
// Polyfill: navigator.geolocation.clearWatch
function clearWatch(watchId) {
    _removeWatcher(watchId);
}
function _removeWatcher(watchId) {
    // Do nothing if we have already removed the subscription
    if (!watchCallbacks[watchId]) {
        return;
    }
    ExpoLocation.removeWatchAsync(watchId);
    delete watchCallbacks[watchId];
    if (Object.keys(watchCallbacks).length === 0 && deviceEventSubscription) {
        LocationEventEmitter.removeSubscription(deviceEventSubscription);
        deviceEventSubscription = null;
    }
}
function getCurrentPosition(success, error = () => { }, options = {}) {
    invariant(typeof success === 'function', 'Must provide a valid success callback.');
    invariant(typeof options === 'object', 'options must be an object.');
    _getCurrentPositionAsyncWrapper(success, error, options);
}
// This function exists to let us continue to return undefined from getCurrentPosition, while still
// using async/await for the internal implementation of it
async function _getCurrentPositionAsyncWrapper(success, error, options) {
    try {
        await ExpoLocation.requestPermissionsAsync();
        const result = await getCurrentPositionAsync(options);
        success(result);
    }
    catch (e) {
        error(e);
    }
}
export async function requestPermissionsAsync() {
    await ExpoLocation.requestPermissionsAsync();
}
// --- Location service
export async function hasServicesEnabledAsync() {
    return await ExpoLocation.hasServicesEnabledAsync();
}
// --- Background location updates
function _validateTaskName(taskName) {
    invariant(taskName && typeof taskName === 'string', '`taskName` must be a non-empty string.');
}
export async function isBackgroundLocationAvailableAsync() {
    const providerStatus = await getProviderStatusAsync();
    return providerStatus.backgroundModeEnabled;
}
export async function startLocationUpdatesAsync(taskName, options = { accuracy: LocationAccuracy.Balanced }) {
    _validateTaskName(taskName);
    await ExpoLocation.startLocationUpdatesAsync(taskName, options);
}
export async function stopLocationUpdatesAsync(taskName) {
    _validateTaskName(taskName);
    await ExpoLocation.stopLocationUpdatesAsync(taskName);
}
export async function hasStartedLocationUpdatesAsync(taskName) {
    _validateTaskName(taskName);
    return ExpoLocation.hasStartedLocationUpdatesAsync(taskName);
}
// --- Geofencing
function _validateRegions(regions) {
    if (!regions || regions.length === 0) {
        throw new Error('Regions array cannot be empty. Use `stopGeofencingAsync` if you want to stop geofencing all regions');
    }
    for (const region of regions) {
        if (typeof region.latitude !== 'number') {
            throw new TypeError(`Region's latitude must be a number. Got '${region.latitude}' instead.`);
        }
        if (typeof region.longitude !== 'number') {
            throw new TypeError(`Region's longitude must be a number. Got '${region.longitude}' instead.`);
        }
        if (typeof region.radius !== 'number') {
            throw new TypeError(`Region's radius must be a number. Got '${region.radius}' instead.`);
        }
    }
}
export async function startGeofencingAsync(taskName, regions = []) {
    _validateTaskName(taskName);
    _validateRegions(regions);
    await ExpoLocation.startGeofencingAsync(taskName, { regions });
}
export async function stopGeofencingAsync(taskName) {
    _validateTaskName(taskName);
    await ExpoLocation.stopGeofencingAsync(taskName);
}
export async function hasStartedGeofencingAsync(taskName) {
    _validateTaskName(taskName);
    return ExpoLocation.hasStartedGeofencingAsync(taskName);
}
export function installWebGeolocationPolyfill() {
    if (Platform.OS !== 'web') {
        // Polyfill navigator.geolocation for interop with the core react-native and web API approach to
        // geolocation
        // @ts-ignore
        window.navigator.geolocation = {
            getCurrentPosition,
            watchPosition,
            clearWatch,
            // We don't polyfill stopObserving, this is an internal method that probably should not even exist
            // in react-native docs
            stopObserving: () => { },
        };
    }
}
export { 
// For internal purposes
LocationEventEmitter as EventEmitter, _getCurrentWatchId, };
//# sourceMappingURL=Location.js.map