/*!
 * Copyright (c) what3words http://what3words.com
 */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Loader } from "@googlemaps/js-api-loader";
import { DEFAULTS, MAP_SCRIPT_ID, MARKER_SRC, MIN_GRID_ZOOM_LEVEL, RETRIES, } from "../../../lib/constants";
import { MapProvider } from "./abstract";
const DEFAULT_LIBS = ["places"];
export class GoogleMaps extends MapProvider {
    constructor() {
        super(...arguments);
        this.loader = DEFAULTS.null;
        this.grid = [];
        this.map = DEFAULTS.null;
        this.eventListeners = {};
    }
    init(map, opts, onInit) {
        const { libraries = DEFAULT_LIBS } = opts;
        this.loader = new Loader({
            id: MAP_SCRIPT_ID,
            apiKey: opts.api_key,
            version: opts.version,
            libraries: libraries.includes("places")
                ? libraries
                : [...libraries, "places"],
            language: opts.language,
            region: opts.region,
            retries: RETRIES,
        });
        this.loader
            .load()
            .then((google) => {
            this.map = new google.maps.Map(map, {
                center: opts.center,
                tilt: opts.tilt,
                disableDefaultUI: opts.disable_default_ui,
                zoom: opts.zoom,
                zoomControl: opts.zoom_control,
                zoomControlOptions: {
                    position: this.isMapControlPosition(opts.zoom_control_position)
                        ? opts.zoom_control_position
                        : undefined,
                },
                mapTypeId: opts.map_type_id,
                mapTypeControl: opts.map_type_control,
                mapTypeControlOptions: {
                    position: this.isMapControlPosition(opts.map_type_control_position)
                        ? opts.map_type_control_position
                        : undefined,
                },
                scaleControl: opts.scale_control,
                streetViewControl: opts.street_view_control,
                streetViewControlOptions: {
                    position: this.isMapControlPosition(opts.street_view_control_position)
                        ? opts.street_view_control_position
                        : undefined,
                },
                rotateControl: opts.rotate_control,
                rotateControlOptions: {
                    position: this.isMapControlPosition(opts.rotate_control_position)
                        ? opts.rotate_control_position
                        : undefined,
                },
                fullscreenControl: opts.fullscreen_control,
                fullscreenControlOptions: {
                    position: this.isMapControlPosition(opts.fullscreen_control_position)
                        ? opts.fullscreen_control_position
                        : undefined,
                },
            });
            this.autocomplete = new google.maps.places.AutocompleteService();
            this.places = new google.maps.places.PlacesService(this.map);
            if (onInit)
                onInit();
        })
            .catch((_error) => ({}));
    }
    addEventListener(event, listener, opts) {
        var _a, _b;
        if (!this.map)
            return;
        (_a = this.eventListeners[event]) === null || _a === void 0 ? void 0 : _a.remove();
        const isAttachedOnce = (_b = opts === null || opts === void 0 ? void 0 : opts.once) !== null && _b !== void 0 ? _b : false;
        const callback = event === "click"
            ? (e) => { var _a; return listener((_a = e === null || e === void 0 ? void 0 : e.latLng) === null || _a === void 0 ? void 0 : _a.toJSON()); }
            : listener;
        let eventListener;
        if (isAttachedOnce)
            eventListener = google.maps.event.addListenerOnce(this.map, this.toEvent(event), callback);
        else
            eventListener = google.maps.event.addListener(this.map, event, callback);
        this.eventListeners[event] = eventListener;
    }
    removeEventListener(event) {
        var _a;
        (_a = this.eventListeners[event]) === null || _a === void 0 ? void 0 : _a.remove();
    }
    removeAllEventListeners() {
        Object.entries(this.eventListeners).forEach(([, listener]) => listener.remove());
    }
    destroy() {
        var _a;
        (_a = this.loader) === null || _a === void 0 ? void 0 : _a.deleteScript();
        this.removeAllEventListeners();
    }
    plotGrid(grid, onClick) {
        this.grid = grid.lines.map((line, index) => {
            var _a;
            const map_type_id = (_a = this.map) === null || _a === void 0 ? void 0 : _a.getMapTypeId();
            const strokeColor = this.getGridStrokeColor(map_type_id);
            const strokeOpacity = this.getGridStrokeOpacity(map_type_id);
            const gridline = new google.maps.Polyline({
                path: [line.start, line.end],
                geodesic: false,
                strokeWeight: 1,
                strokeOpacity,
                strokeColor,
            });
            this.eventListeners[`grid-${index}`] = gridline.addListener("click", async (e) => {
                var _a;
                const coordinates = (_a = e === null || e === void 0 ? void 0 : e.latLng) === null || _a === void 0 ? void 0 : _a.toJSON();
                const zoom = this.zoom;
                this.panTo(coordinates);
                if (zoom && zoom < MIN_GRID_ZOOM_LEVEL) {
                    this.zoom = this.selected_zoom;
                }
                if (onClick && coordinates)
                    onClick(coordinates);
            });
            gridline.setMap(this.map);
            return gridline;
        });
    }
    clearGrid() {
        this.grid.forEach((polyline) => {
            if (!polyline)
                return;
            polyline.setVisible(false);
            polyline.setMap(null);
        });
        this.grid = [];
    }
    plotSquare(square) {
        if (this.square)
            this.square.setMap(null);
        this.square = this.createSquare(square);
        this.square.setMap(this.map);
    }
    plotMarker(address, callback, markerIcon = MARKER_SRC) {
        var _a;
        if (this.marker) {
            this.marker.setMap(null);
            (_a = this.eventListeners.drag) === null || _a === void 0 ? void 0 : _a.remove();
        }
        this.marker = new google.maps.Marker({
            icon: markerIcon,
            map: this.map,
            optimized: true,
            position: address.coordinates,
            title: address.words,
            clickable: true,
            draggable: true,
        });
        this.eventListeners.drag = this.marker.addListener("dragend", this.onChangePosition(callback).bind(this));
    }
    get center() {
        var _a;
        if (!this.map)
            return;
        return (_a = this.map.getCenter()) === null || _a === void 0 ? void 0 : _a.toJSON();
    }
    set center(coordinate) {
        if (!this.map)
            return;
        this.map.setCenter(coordinate);
    }
    get zoom() {
        if (!this.map)
            return;
        return this.map.getZoom();
    }
    set zoom(zoom) {
        if (!this.map)
            return;
        this.map.setZoom(zoom);
    }
    /**
     * Retrieves bounds from map instance if present and returns w3w boundingBox object; otherwise returns the set default.
     * @returns Bounds a w3w bounding box object
     */
    get bounds() {
        return this.convertToW3wBounds(this.mapBounds);
    }
    /**
     * Retrieves bounds from map instance if present; otherwise returns the set default.
     * See https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngBoundsLiteral
     * @returns mapBounds a LatLngBoundsLiteral object
     */
    get mapBounds() {
        var _a;
        if (!this.map)
            return DEFAULTS.bounds;
        const mapBounds = JSON.parse(JSON.stringify((_a = this.map.getBounds()) !== null && _a !== void 0 ? _a : DEFAULTS.bounds));
        return mapBounds;
    }
    set viewType(type) {
        if (!this.map)
            return;
        this.map.setMapTypeId(type);
    }
    set options(options) {
        if (!this.map)
            return;
        this.map.setOptions(options);
    }
    set mapTypeControlPosition(position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.setOptions({
            mapTypeControlOptions: {
                position,
            },
        });
    }
    set zoomControlPosition(position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.setOptions({
            zoomControlOptions: {
                position,
            },
        });
    }
    set fullscreenControlPosition(position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.setOptions({
            fullscreenControlOptions: {
                position,
            },
        });
    }
    set streetViewControlPosition(position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.setOptions({
            streetViewControlOptions: {
                position,
            },
        });
    }
    set rotateControlPosition(position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.setOptions({
            rotateControlOptions: {
                position,
            },
        });
    }
    set mapTypeId(id) {
        if (!this.map)
            return;
        this.map.setMapTypeId(id);
    }
    panTo(coordinate) {
        if (!this.map || !coordinate)
            return;
        this.map.panTo(coordinate);
    }
    setMapControlPosition(control, position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.controls[position] = new google.maps.MVCArray([
            ...this.map.controls[position].getArray(),
            control,
        ]); // BUG: type coersion shouldn't be necessary
    }
    getPlacesSuggestions(input, origin, language) {
        return new Promise((res, rej) => {
            var _a;
            const query = {
                input,
                language,
                origin: origin !== null && origin !== void 0 ? origin : undefined,
            };
            (_a = this.autocomplete) === null || _a === void 0 ? void 0 : _a.getPlacePredictions(query, (places, status) => {
                if (status !== google.maps.places.PlacesServiceStatus.OK) {
                    rej(status);
                }
                if (!places)
                    return;
                res(places.map((place) => ({
                    id: place.place_id,
                    description: place.description,
                    value: place.description,
                    distance: { value: place.distance_meters, units: "m" },
                })));
            });
        });
    }
    getCoordinatesFromPlace(id) {
        return new Promise((res, rej) => {
            var _a;
            (_a = this.places) === null || _a === void 0 ? void 0 : _a.getDetails({ placeId: id }, (address, status) => {
                var _a, _b;
                if (status !== google.maps.places.PlacesServiceStatus.OK) {
                    rej(status);
                }
                if (!address)
                    res(null);
                res((_b = (_a = address.geometry.location) === null || _a === void 0 ? void 0 : _a.toJSON()) !== null && _b !== void 0 ? _b : null);
            });
        });
    }
    addControl(control, position) {
        if (!this.map || !this.isMapControlPosition(position))
            return;
        this.map.controls[position] = new google.maps.MVCArray([
            ...this.map.controls[position].getArray(),
            control,
        ]); // BUG: type coersion shouldn't be necessary
    }
    createSquare(square) {
        return new google.maps.Rectangle({
            bounds: new google.maps.LatLngBounds(square.southwest, square.northeast),
            strokeWeight: 1,
            fillColor: "#0A3049",
            strokeColor: "#0A3049",
            strokePosition: google.maps.StrokePosition.INSIDE,
        });
    }
    getGridStrokeColor(map_type_id) {
        switch (map_type_id) {
            case google.maps.MapTypeId.SATELLITE:
            case google.maps.MapTypeId.HYBRID:
                return "#FFFFFF";
            case google.maps.MapTypeId.ROADMAP:
            case google.maps.MapTypeId.TERRAIN:
            default:
                return "#0A3049";
        }
    }
    getGridStrokeOpacity(map_type_id) {
        switch (map_type_id) {
            case google.maps.MapTypeId.SATELLITE:
            case google.maps.MapTypeId.HYBRID:
                return 0.3;
            case google.maps.MapTypeId.ROADMAP:
            case google.maps.MapTypeId.TERRAIN:
            default:
                return 0.1;
        }
    }
    isMapControlPosition(position) {
        if (!position)
            return false;
        return Object.values(google.maps.ControlPosition).includes(position);
    }
    onChangePosition(callback) {
        return async (e) => {
            var _a;
            const coordinates = (_a = e.latLng) === null || _a === void 0 ? void 0 : _a.toJSON();
            const zoom = this.zoom;
            if (zoom && zoom < MIN_GRID_ZOOM_LEVEL) {
                this.panTo(coordinates);
                this.zoom = this.selected_zoom;
            }
            if (callback && coordinates)
                callback(coordinates);
        };
    }
    toEvent(event) {
        switch (event) {
            case "loaded":
                return "tilesloaded";
            case "maptype_changed":
                return "maptypeid_changed";
            default:
                return event;
        }
    }
    /**
     * @param gmapsBounds a gmaps literal bounds object
     */
    convertToW3wBounds({ south, west, north, east, }) {
        return {
            southwest: { lat: south, lng: west },
            northeast: { lat: north, lng: east },
        };
    }
    /**
     * @param w3wBounds a w3w bounding box object
     */
    convertToBounds({ southwest, northeast, }) {
        const { lat: south, lng: west } = southwest;
        const { lat: north, lng: east } = northeast;
        return { south, west, north, east };
    }
}
