import { AbstractMapWithMarkers } from '../abstract-map-with-markers';
import { loadLeaflet, loadLeafletClusterer, trackOutdoorActiveIfConfigured } from '../../utilities';
import { Lock } from '../../../common/lock.class';

/**
 * @implements {MapWithTour}
 * @implements {MapWithInfoWindow}
 */
export class Leaflet extends AbstractMapWithMarkers {

    initialize() {
        /** @private {Lock} */
        this.renderLock = new Lock();

        /** @private */
        this.polylines = [];
    }

    /**
     * @protected
     * @returns {Promise}
     */
    render(markers) {
        return this.renderLock.lock(async () => {
            if (!this.map && !this.clusterer) {
                this.map = await this.initializeMap();
                this.moveZoomControl();
                this.clusterer = await this.initializeMarkerClusterer(this.map);
            }

            const mapMarkers = this.getMapMarkers(markers);
            this.clusterer.clearLayers();
            this.clusterer.addLayer(new this.L.LayerGroup(mapMarkers));
        });
    }

    /** @public */
    centerMapOnMarkers(markers) {
        if (!this.map) {
            setTimeout(() => this.centerMapOnMarkers(markers), 250);
            return;
        }
        if (markers.length === 0) {
            return;
        }
        if (markers.length === 1) {
            this.map.setView(
                this.coordinatesToLatLng(markers[0].coordinates),
                this.options.zoom * 18 / 100
            );
            return;
        }

        const bounds = this.L.latLngBounds(
            this.coordinatesToLatLng(markers[0].coordinates),
            this.coordinatesToLatLng(markers[1].coordinates)
        );
        for (const marker of markers) {
            bounds.extend(this.coordinatesToLatLng(marker.coordinates));
        }
        this.map.fitBounds(bounds);
    }

    /**
     * @public
     * @param {Marker} marker
     * @returns {Promise|void}
     */
    drawTourOntoMap(marker, fitBoundsToTour) {
        if (!this.L) {
            setTimeout(() => this.drawTourOntoMap(marker), 250);
            return;
        }

        if (!marker.polyline) {
            marker.polyline = this.L.polyline([], { color: marker.tour.color || '#FF6347' });
        }

        marker.polyline
            .setLatLngs(marker.tour.polygon)
            .addTo(this.map);
        this.polylines.push(marker.polyline);

        if (fitBoundsToTour) {
            this.map.fitBounds(
                this.L.latLngBounds(marker.tour.polygon).pad(.1)
            );
        }
    }

    clearTours () {
        for (const polyline of this.polylines) {
            polyline.remove();
        }
        this.polylines = [];
    }

    openInfoWindow (marker, content) {
        if (!this.popup) {
            this.popup = this.initializePopup();
        }

        const zoom = Math.max(this.map.getZoom(), 16);
        this.map.setView(this.coordinatesToLatLng(marker.coordinates), zoom);
        this.popup
            .setLatLng(this.coordinatesToLatLng(marker.coordinates))
            .setContent(content)
            .openOn(this.map);

        this.popup.currentMarker = marker;
        this.clusterer.removeLayer(marker.mapMarker);
        this.map.addLayer(marker.mapMarker);

        trackOutdoorActiveIfConfigured(marker);
    }

    closeInfoWindow() {
        if (this.popup) {
            this.map.removeLayer(this.popup.currentMarker.mapMarker);
            this.clusterer.addLayer(this.popup.currentMarker.mapMarker);
            this.popup.remove();
            this.popup.currentMarker = null;
        }
    }

    /**
     * @protected
     * @param {object} options
     * @returns {object}
     */
    prepareOptions (options) {
        options = super.prepareOptions(options);
        options.tileLayer = options.tileLayer || {};
        options.tileLayer.url = options.tileLayer.url || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
        options.tileLayer.attribution = options.tileLayer.attribution || '';
        return options;
    }

    /**
     * @returns {Promise}
     * @protected
     */
    async initializeMap() {
        this.L = await loadLeaflet();

        const center = this.coordinatesToLatLng(this.options.center);
        center.lat = center.lat || 47.980870;
        center.lng = center.lng || 7.820810;

        // Initialize map
        const map = this.L.map(this.node).setView(
            center,
            this.options.zoom * 18 / 100,
        );

        this.L.tileLayer(this.options.tileLayer.url, {
            attribution: this.options.tileLayer.attribution,
            maxZoom: 18,
        }).addTo(map);

        return map;
    }

    /**
     * @protected
     * @param {Marker~Coordinates} coordinates
     * @returns {{lat: number, lng: number}}
     */
    coordinatesToLatLng(coordinates) {
        return { lat: coordinates.latitude, lng: coordinates.longitude };
    }

    /**
     * @private
     * @returns {Leaflet.Marker[]}
     */
    getMapMarkers(markers) {
        const mapMarkers = [];
        for (const marker of markers) {
            if (!marker.style.mapIcon && marker.style.iconUrl) {
                marker.style.mapIcon = new this.L.Icon({
                    iconUrl: marker.style.iconUrl,
                    iconSize: [ marker.style.width, marker.style.height ],
                    iconAnchor: [ marker.style.offsetX, marker.style.offsetY ],
                });
            }

            if (!marker.mapMarker) {
                marker.mapMarker = new this.L.Marker(this.coordinatesToLatLng(marker.coordinates));
                marker.mapMarker.on('click', () => this.callMarkerClickListeners(marker));
            }

            if (marker.style.mapIcon) {
                marker.mapMarker.setIcon(marker.style.mapIcon);
            }

            mapMarkers.push(marker.mapMarker);
        }
        return mapMarkers;
    }

    /**
     * @private
     * @param {Leaflet} map
     * @returns {Promise<void>}
     */
    async initializeMarkerClusterer(map) {
        const L = this.L;
        const clustering = this.options.clustering;
        const clusterer = await loadLeafletClusterer();

        const clustererInstance = new clusterer.MarkerClusterGroup({
            showCoverageOnHover: true,
            zoomToBoundsOnClick: true,
            spiderfyOnMaxZoom: true,
            spiderLegPolylineOptions: { weight: 2.5, color: '#222', opacity: .75 },
            iconCreateFunction(cluster) {
                return L.divIcon({
                    iconSize: [ 40, 40 ],
                    html: `
                        <div class="tb-map-marker-cluster" style="
                            background: ${clustering.backgroundColor};
                            color: ${clustering.color};
                            width: 100%;
                            height: 100%;
                        ">
                            <span class="tb-map-marker-cluster__label">
                                ${cluster.getChildCount()}
                            </span>
                        </div>
                    `,
                    className: 'tb-map-marker-cluster',
                });
            },
        });
        map.addLayer(clustererInstance);
        return clustererInstance;
    }

    /**
     * @private
     * @returns {Leaflet.Popup}
     */
    initializePopup() {
        return this.L.popup({
            minWidth: 266,
            maxWidth: 500,
            closeButton: false,
            closeOnClick: false,

            // TODO This should not be hard coded values.
            offset: [ 0, -25 ],
        });
    }

    /** @private */
    moveZoomControl() {
        if (this.map.zoomControl) {
            this.map.zoomControl.remove();
        }
        (new this.L.Control.Zoom({ position: 'topright' })).addTo(this.map);
    }
}
