import { LightningElement, api, track } from 'lwc';
import {
    classListMutation,
    guid,
    normalizeString,
    deepCopy,
} from 'lightning/utilsPrivate';
import labelOpenInGoogleMaps from '@salesforce/label/LightningMap.openInGoogleMaps';
import labelCoordinatesTitle from '@salesforce/label/LightningMap.coordinatesTitle';
import labelIframeTitle from '@salesforce/label/LightningMap.iframeTitle';
import { formatAddress, titleCase } from './mapUtils';
import { getCoreInfo } from 'lightning/configProvider';

const i18n = {
    openInGoogleMapsString: labelOpenInGoogleMaps,
    coordinatesTitleString: labelCoordinatesTitle,
    primitiveMapIframeTitle: labelIframeTitle,
};

const EXTERNAL_GOOGLE_MAPS_URL = 'https://www.google.com/maps/place/';

/**
 * Displays a map with markers for one or more locations.
 */
export default class LightningMap extends LightningElement {
    @track _mapHref = EXTERNAL_GOOGLE_MAPS_URL;
    @track _coordinates = [];
    @track _activeCoordinate = null;
    @track _markersTitle = i18n.coordinatesTitleString;

    privateZoomLevel = null;
    privateCenter = null;
    privateMarkers = null;

    /**
     * @name privateCoordinateItems
     * @type {Array}
     * @private
     * Array that holds all the child primitiveCoordinateItem(s).
     */
    privateCoordinateItems = [];

    mapDomain = `https://maps${getCoreInfo().untrustedContentDomain}:${
        getCoreInfo().securePort
    }`;

    mapSrc = `${
        this.mapDomain
    }/lightningmaps/mapsloader?resource=primitiveMap&version=${
        getCoreInfo().internalAppVersion
    }`;

    /**
     * If present, the footer element is displayed below the map.
     * The footer shows an 'Open in Google Maps' link that opens an external window
     * to display the selected marker location in Google Maps. Default value is false.
     *
     * @type {Boolean}
     * @default false
     */
    @api showFooter = false;

    /**
     * Displays or hides the list of locations. Valid values are visible, hidden, or auto.
     * This value defaults to auto, which shows the list only when multiple markers are present.
     * Passing in an invalid value hides the list view.
     */
    @api listView = 'auto';

    /**
     * @param {Integer} value - Corresponds to zoom levels defined in Google Maps API
     * If not specified, automatically chooses an appropriate zoom level
     * to show all markers with comfortable margins.
     */
    set zoomLevel(value) {
        this.privateZoomLevel = value;
        this.postToIframe({ zoomLevel: this.privateZoomLevel });
    }

    /**
     * The zoom levels as defined by Google Maps API.
     * If a zoom level is not specified, a default zoom level is applied to accommodate all markers on the map.
     * @type {number}
     */
    @api
    get zoomLevel() {
        return this.privateZoomLevel;
    }

    /**
     * @param {Object} value - A single address value to center the map around
     */
    set center(value) {
        this.privateCenter = value;
        const computedCenter = this.primitivifyMarker(deepCopy(this.center));
        this.postToIframe({ center: computedCenter });
    }

    /**
     * A location to use as the map's center.
     * If center is not specified, the map centers automatically.
     *
     * @type {object}
     */
    @api
    get center() {
        return this.privateCenter;
    }

    /**
     * Setter for the markersTitle property.
     * @param {String} title - A title string for the list of locations
     */
    set markersTitle(title) {
        this._markersTitle = titleCase(title);
    }

    /**
     * Provides the heading title for the markers. Required if specifying multiple markers.
     * The title is displayed below the map as a header for the list of clickable addresses.
     *
     * @type {string}
     */
    @api
    get markersTitle() {
        return this._markersTitle;
    }

    /**
     * Setter for the selectedMarkerValue property.
     * @param {String} value - The value of the selected marker
     */
    set selectedMarkerValue(value) {
        const selectedMarker = this._coordinatesMapByValue[value];
        const selectedMarkerKey = selectedMarker && selectedMarker.key;
        this.selectMarker(selectedMarkerKey);
    }

    /**
     * Provides the value of the currently selected marker.
     * If the selected marker does not have the value property specified, then returns undefined.
     * @type {String}
     */
    @api
    get selectedMarkerValue() {
        const selectedMarker = this._coordinatesMapByKey[this._activeMarkerId];
        return selectedMarker && selectedMarker.value;
    }

    /**
     * Setter function, for mapMarkers.
     * Depending on the number of markers passed, we display a single view map or
     * a map with multiple markers and a list of coordinates
     * @param {Object[]} mapMarkers - the markers array with the following format:
     * map-markers = [
     *  {
     *      location: {
     *           City: 'San Francisco',
     *           Country: 'USA',
     *           PostalCode: '94105',
     *           state: 'CA',
     *           street: '50 Fremont St',
     *       },
     *      value: 'unique identifier 001',
     *      // Extra info for tile in sidebar
     *      icon: 'standard:account',
     *      title: 'Julies Kitchen', // e.g. Account.Name
     *  },
     *  {
     *      location: {
     *          City: 'San Francisco',
     *          Country: 'USA',
     *          PostalCode: '94105',
     *          State: 'CA',
     *          Street: '30 Fremont St.',
     *      },
     *      value: 'unique identifier 002',
     *      icon: 'standard:account',
     *      title: 'Tender Greens', // e.g. Account.Name
     *  }
     */
    set mapMarkers(mapMarkers) {
        this.privateMarkers = mapMarkers;
        this.initMarkers(mapMarkers);
        this._activeCoordinate = mapMarkers[0];
    }

    /**
     * One or more objects with the address or latitude and longitude to be displayed on the map.
     * If latitude and longitude are provided, the address is ignored.
     * @type {array}
     * @required
     */
    @api
    get mapMarkers() {
        return this.privateMarkers;
    }

    /**
     * getter for the i18 constant containing the localized strings
     */
    get i18n() {
        return i18n;
    }

    /**
     * returns the href link to open the map on an external window.
     * e.g. "https://www.google.com/maps/place/1+Market+St,+San+Francisco,+CA+94105"
     */
    get mapHref() {
        const activeCoordinate = this._activeCoordinate.location;
        let mapHrefURL = '';

        if (activeCoordinate.Latitude && activeCoordinate.Longitude) {
            mapHrefURL = encodeURI(
                `${EXTERNAL_GOOGLE_MAPS_URL}${activeCoordinate.Latitude},${
                    activeCoordinate.Longitude
                }`
            );
        } else {
            mapHrefURL = encodeURI(
                `${EXTERNAL_GOOGLE_MAPS_URL}${normalizeString(
                    activeCoordinate.Street
                )}+${normalizeString(activeCoordinate.City)}+${normalizeString(
                    activeCoordinate.State
                )}+${normalizeString(activeCoordinate.PostalCode)}`
            );
        }
        return mapHrefURL;
    }

    /**
     * Controls the visibility of the coordinates list-view/sidebar.
     * See listView attribute.
     */
    get showCoordinatesSidebar() {
        const outputs = {
            visible: true,
            hidden: false,
            auto: this._coordinates && this._coordinates.length > 1,
        };
        return outputs[this.listView];
    }

    connectedCallback() {
        classListMutation(this.classList, {
            'slds-grid': true,
            'slds-has-coordinates': this.showCoordinatesSidebar,
        });

        window.addEventListener('message', this.handleMessage.bind(this));
    }

    /**
     * Function to normalize and store the coordinates being passed.
     * We store an array with all the coordindates as well as a map for easy access.
     * @param {Object} mapMarkers - Array of Coordindates
     */
    initMarkers(mapMarkers) {
        const mapMarkersLength = mapMarkers.length;
        const coordinates = [];
        const coordinatesMapByKey = {};
        const coordinatesMapByValue = {};
        let i = 0,
            coordinate = {},
            key;

        for (i; i < mapMarkersLength; i++) {
            key = guid();
            coordinate = deepCopy(mapMarkers[i]);
            coordinate.key = key;
            coordinate.formattedAddress = formatAddress(coordinate.location);
            if (!coordinate.icon) {
                coordinate.icon = 'standard:location';
            }
            coordinates.push(coordinate);
            coordinatesMapByKey[key] = coordinate;
            if (coordinate.value) {
                coordinatesMapByValue[coordinate.value] = coordinate;
            }
        }

        this._coordinates = coordinates;
        this._coordinatesMapByKey = coordinatesMapByKey;
        this._coordinatesMapByValue = coordinatesMapByValue;

        const markers = this._coordinates.map(marker =>
            this.primitivifyMarker(marker)
        );

        this.postToIframe({ markers });
    }

    handleCoordinateRegister(event) {
        event.stopPropagation(); // suppressing event since its not part of public API
        this.privateCoordinateItems.push(event.srcElement);
    }

    /**
     * Click handler for the coordinate click.
     * On click we post the coordinate key to the primitive map so it can get selected
     * @param {Object} event - The event object containing the key of the coordinate clicked
     */
    handleCoordinateClick(event) {
        const key = event.detail.key;
        this.selectMarker(key);
        this.postToIframe({
            activeMarkerId: this._activeMarkerId,
        });
    }

    /**
     * Click handler for the coordinate hover.
     * @param {Object} event - The event object containing the key of the coordinate hovered
     */
    handleCoordinateHover(event) {
        this._hoverMarkerId = event.detail.key;
        this.postToIframe({
            hoverMarkerId: this._hoverMarkerId,
        });
    }

    /**
     * Handle messages from the child iframe
     * @param {Object} event - The event object
     */
    handleMessage(event) {
        const messageType = event.data && event.data.event;
        if (messageType === 'markerselect') {
            const key = event.data.key;
            this.selectMarker(key);
        }
    }

    selectMarker(key) {
        const activeCoordinate = this._coordinatesMapByKey[key];
        this._activeCoordinate = activeCoordinate;

        this._activeMarkerId = key;

        // unselect other child coordinateitems from the coordinates list
        this.privateCoordinateItems.forEach(coordinate => {
            if (coordinate.guid === key) {
                coordinate.selected = true;
            } else {
                coordinate.selected = false;
            }
        });

        // fire select event
        // eslint-disable-next-line lightning-global/no-custom-event-bubbling
        const markerSelectEvent = new CustomEvent('markerselect', {
            bubbles: true,
        });
        this.dispatchEvent(markerSelectEvent);
    }

    /**
     * Create marker for sending to primitive map.
     * Extract only information that is relevant to primitive map
     * @param {Object} marker  - a marker containing location and related information.
     * @returns {Object} marker - a marker with only keys relevant to primitive map.
     */
    primitivifyMarker(marker) {
        let primitifiedMarker = null;

        if (marker && marker.location) {
            primitifiedMarker = {
                key: marker.key,
                title: marker.title,
                description: marker.description,
                ...marker.location,
            };
        }

        return primitifiedMarker;
    }

    /**
     * Method helper to posts messages to the map iframe
     * @param {Object} data - The payload to post to the iframe
     */
    postToIframe(data) {
        if (this.iframeLoaded) {
            this.mapIframe.callbacks.postToWindow(data);
        }
    }

    /**
     * handler function for when the iframe is loaded, at which point we
     * store a reference for the callback postToWindow method for iframe communication.
     * We also post the first payload of coordindates to the primitive map
     * @param {Object} event - The event object containing the postToWindow callback
     */
    handleIframeLoad(event) {
        const center = this.center
            ? this.primitivifyMarker(deepCopy(this.center))
            : null;
        const zoomLevel = this.zoomLevel;
        const markers = deepCopy(this._coordinates).map(marker =>
            this.primitivifyMarker(marker)
        );

        this.iframeLoaded = true;
        this.mapIframe = event.detail;
        this.postToIframe({ center, markers, zoomLevel });
    }
}
