/** @odoo-module **/

/*global L*/

import { renderToString } from "@web/core/utils/render";

const { Component, onWillUnmount, onWillUpdateProps, useEffect, useRef, useState } = owl;

const apiTilesRouteWithToken =
    "https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}";
const apiTilesRouteWithoutToken = "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png";

const colors = [
    "#F06050",
    "#6CC1ED",
    "#F7CD1F",
    "#814968",
    "#30C381",
    "#D6145F",
    "#475577",
    "#F4A460",
    "#EB7E7F",
    "#2C8397",
];

const mapTileAttribution = `
    © <a href="https://www.mapbox.com/about/maps/">Mapbox</a>
    © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>
    <strong>
        <a href="https://www.mapbox.com/map-feedback/" target="_blank">
            Improve this map
        </a>
    </strong>`;

export class MapRenderer extends Component {
    setup() {
        this.leafletMap = null;
        this.markers = [];
        this.polylines = [];
        this.mapContainerRef = useRef("mapContainer");
        this.state = useState({
            closedGroupIds: [],
        });
        this.nextId = 1;

        useEffect(
            () => {
                this.leafletMap = L.map(this.mapContainerRef.el, {
                    maxBounds: [L.latLng(180, -180), L.latLng(-180, 180)],
                });
                L.tileLayer(this.apiTilesRoute, {
                    attribution: mapTileAttribution,
                    tileSize: 512,
                    zoomOffset: -1,
                    minZoom: 2,
                    maxZoom: 19,
                    id: "mapbox/streets-v11",
                    accessToken: this.props.model.metaData.mapBoxToken,
                }).addTo(this.leafletMap);
            },
            () => []
        );
        useEffect(() => {
            this.updateMap();
        });

        onWillUpdateProps(this.onWillUpdateProps);
        onWillUnmount(this.onWillUnmount);
    }
    /**
     * Update group opened/closed state.
     */
    async onWillUpdateProps(nextProps) {
        if (this.props.model.data.groupByKey !== nextProps.model.data.groupByKey) {
            this.state.closedGroupIds = [];
        }
    }
    /**
     * Remove map and the listeners on its markers and routes.
     */
    onWillUnmount() {
        this.removeMarkers();
        this.removeRoutes();
        if (this.leafletMap) {
            this.leafletMap.remove();
        }
    }

    /**
     * Return the route to the tiles api with or without access token.
     *
     * @returns {string}
     */
    get apiTilesRoute() {
        return this.props.model.data.useMapBoxAPI
            ? apiTilesRouteWithToken
            : apiTilesRouteWithoutToken;
    }

    /**
     * If there's located records, adds the corresponding marker on the map.
     * Binds events to the created markers.
     */
    addMarkers() {
        this.removeMarkers();

        const markersInfo = {};
        let records = this.props.model.data.records;
        if (this.props.model.data.isGrouped) {
            records = Object.entries(this.props.model.data.recordGroups)
                .filter(([key]) => !this.state.closedGroupIds.includes(key))
                .flatMap(([groupId, value]) => value.records.map((elem) => ({ ...elem, groupId })));
        }

        const pinInSamePlace = {};
        for (const record of records) {
            const partner = record.partner;
            if (partner && partner.partner_latitude && partner.partner_longitude) {
                const lat_long = `${partner.partner_latitude}-${partner.partner_longitude}`;
                const group = this.props.model.data.recordGroups ? `-${record.groupId}` : "";
                const key = `${lat_long}${group}`;
                if (key in markersInfo) {
                    markersInfo[key].record = record;
                    markersInfo[key].ids.push(record.id);
                } else {
                    pinInSamePlace[lat_long] = ++pinInSamePlace[lat_long] || 0;
                    markersInfo[key] = {
                        record: record,
                        ids: [record.id],
                        pinInSamePlace: pinInSamePlace[lat_long],
                    };
                }
            }
        }

        for (const markerInfo of Object.values(markersInfo)) {
            const params = {
                count: markerInfo.ids.length,
                isMulti: markerInfo.ids.length > 1,
                number: this.props.model.data.records.indexOf(markerInfo.record) + 1,
                numbering: this.props.model.metaData.numbering,
            };

            if (this.props.model.data.isGrouped) {
                const groupId = markerInfo.record.groupId;
                params.color = this.getGroupColor(groupId);
                params.number = this.props.model.data.recordGroups[groupId].records.findIndex(
                    (record) => {
                        return record.id === markerInfo.record.id;
                    }
                );
            }

            // Icon creation
            const iconInfo = {
                className: "o-map-renderer--marker",
                html: renderToString("web_map.marker", params),
            };

            const offset = markerInfo.pinInSamePlace * 0.000025;
            // Attach marker with icon and popup
            const marker = L.marker(
                [
                    markerInfo.record.partner.partner_latitude + offset,
                    markerInfo.record.partner.partner_longitude - offset,
                ],
                { icon: L.divIcon(iconInfo) }
            );
            marker.addTo(this.leafletMap);
            marker.on("click", () => {
                this.createMarkerPopup(markerInfo, offset);
            });
            this.markers.push(marker);
        }
    }
    /**
     * If there are computed routes, create polylines and add them to the map.
     * each element of this.props.routeInfo[0].legs array represent the route between
     * two waypoints thus each of these must be a polyline.
     */
    addRoutes() {
        this.removeRoutes();
        if (!this.props.model.data.useMapBoxAPI || !this.props.model.data.routes.length) {
            return;
        }

        for (const leg of this.props.model.data.routes[0].legs) {
            const latLngs = [];
            for (const step of leg.steps) {
                for (const coordinate of step.geometry.coordinates) {
                    latLngs.push(L.latLng(coordinate[1], coordinate[0]));
                }
            }

            const polyline = L.polyline(latLngs, {
                color: "blue",
                weight: 5,
                opacity: 0.3,
            }).addTo(this.leafletMap);

            const polylines = this.polylines;
            polyline.on("click", function () {
                for (const polyline of polylines) {
                    polyline.setStyle({ color: "blue", opacity: 0.3 });
                }
                this.setStyle({ color: "darkblue", opacity: 1.0 });
            });
            this.polylines.push(polyline);
        }
    }
    /**
     * Create a popup for the specified marker.
     *
     * @param {Object} markerInfo
     * @param {Number} latLongOffset
     */
    createMarkerPopup(markerInfo, latLongOffset = 0) {
        const popupFields = this.getMarkerPopupFields(markerInfo);
        const partner = markerInfo.record.partner;
        const popupHtml = renderToString("web_map.markerPopup", {
            fields: popupFields,
            hasFormView: this.props.model.metaData.hasFormView,
            url: `https://www.google.com/maps/dir/?api=1&destination=${partner.partner_latitude},${partner.partner_longitude}`,
        });

        const popup = L.popup({ offset: [0, -30] })
            .setLatLng([
                partner.partner_latitude + latLongOffset,
                partner.partner_longitude - latLongOffset,
            ])
            .setContent(popupHtml)
            .openOn(this.leafletMap);

        const openBtn = popup
            .getElement()
            .querySelector("button.o-map-renderer--popup-buttons-open");
        if (openBtn) {
            openBtn.onclick = () => {
                this.props.onMarkerClick(markerInfo.ids);
            };
        }
        return popup;
    }
    /**
     * @param {Number} groupId
     */
    getGroupColor(groupId) {
        const index = Object.keys(this.props.model.data.recordGroups).indexOf(groupId);
        return colors[index % colors.length];
    }
    /**
     * Creates an array of latLng objects if there is located records.
     *
     * @returns {latLngBounds|boolean} objects containing the coordinates that
     *          allows all the records to be shown on the map or returns false
     *          if the records does not contain any located record.
     */
    getLatLng() {
        const tabLatLng = [];
        for (const record of this.props.model.data.records) {
            const partner = record.partner;
            if (partner && partner.partner_latitude && partner.partner_longitude) {
                tabLatLng.push(L.latLng(partner.partner_latitude, partner.partner_longitude));
            }
        }
        if (!tabLatLng.length) {
            return false;
        }
        return L.latLngBounds(tabLatLng);
    }
    /**
     * Get the fields' name and value to display in the popup.
     *
     * @param {Object} markerInfo
     * @returns {Object} value contains the value of the field and string
     *                   contains the value of the xml's string attribute
     */
    getMarkerPopupFields(markerInfo) {
        const record = markerInfo.record;
        const fieldsView = [];
        // Only display address in multi coordinates marker popup
        if (markerInfo.ids.length > 1) {
            if (!this.props.model.metaData.hideAddress) {
                fieldsView.push({
                    id: this.nextId++,
                    value: record.partner.contact_address_complete,
                    string: this.env._t("Address"),
                });
            }
            return fieldsView;
        }
        if (!this.props.model.metaData.hideName) {
            fieldsView.push({
                id: this.nextId++,
                value: record.display_name,
                string: this.env._t("Name"),
            });
        }
        if (!this.props.model.metaData.hideAddress) {
            fieldsView.push({
                id: this.nextId++,
                value: record.partner.contact_address_complete,
                string: this.env._t("Address"),
            });
        }
        for (const field of this.props.model.metaData.fieldNamesMarkerPopup) {
            if (record[field.fieldName]) {
                const fieldName =
                    record[field.fieldName] instanceof Array
                        ? record[field.fieldName][1]
                        : record[field.fieldName];
                fieldsView.push({
                    id: this.nextId++,
                    value: fieldName,
                    string: field.string,
                });
            }
        }
        return fieldsView;
    }
    /**
     * Remove the markers from the map and empty the markers array.
     */
    removeMarkers() {
        for (const marker of this.markers) {
            marker.off("click");
            this.leafletMap.removeLayer(marker);
        }
        this.markers = [];
    }
    /**
     * Remove the routes from the map and empty the the polyline array.
     */
    removeRoutes() {
        for (const polyline of this.polylines) {
            polyline.off("click");
            this.leafletMap.removeLayer(polyline);
        }
        this.polylines = [];
    }
    /**
     * Update position in the map, markers and routes.
     */
    updateMap() {
        if (this.props.model.data.shouldUpdatePosition) {
            const initialCoord = this.getLatLng();
            if (initialCoord) {
                this.leafletMap.flyToBounds(initialCoord, { animate: false });
            } else {
                this.leafletMap.fitWorld();
            }
            this.leafletMap.closePopup();
        }
        this.addMarkers();
        this.addRoutes();
    }

    /**
     * Center the map on a certain pin and open the popup linked to it.
     *
     * @param {Object} record
     */
    centerAndOpenPin(record) {
        const popup = this.createMarkerPopup({
            record: record,
            ids: [record.id],
        });
        const px = this.leafletMap.project([
            record.partner.partner_latitude,
            record.partner.partner_longitude,
        ]);
        const popupHeight = popup.getElement().offsetHeight;
        px.y -= popupHeight / 2;
        const latlng = this.leafletMap.unproject(px);
        this.leafletMap.panTo(latlng, { animate: true });
    }
    /**
     * @param {Number} id
     */
    toggleGroup(id) {
        if (this.state.closedGroupIds.includes(id)) {
            const index = this.state.closedGroupIds.indexOf(id);
            this.state.closedGroupIds.splice(index, 1);
        } else {
            this.state.closedGroupIds.push(id);
        }
    }
}

MapRenderer.template = "web_map.MapRenderer";
MapRenderer.props = {
    model: Object,
    onMarkerClick: Function,
};
