import L from "leaflet";
import "leaflet/dist/leaflet.css";

// manually load the markers so that they can be embedded in the prod bundle
import markerIconRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
import markerIconUrl from "leaflet/dist/images/marker-icon.png";
import markerShadowUrl from "leaflet/dist/images/marker-shadow.png";

const defaultZoomLevel = 8;

window.app = window.app || {};
window.app.components = window.app.components || {};

/**
 * Leaflet component for showing and adjust a single geo point value.
 *
 * @param  {Object} [propsArg]
 * @return {Element}
 */
window.app.components.leaflet = function(propsArg = {}) {
    const props = store({
        rid: undefined,
        id: undefined,
        hidden: undefined,
        inert: undefined,
        className: "",
        point: { lat: 0, lon: 0 },
        onchange: function(point) {},
    });

    const watchers = app.utils.extendStore(props, propsArg);

    let map;
    let marker;
    let panTimeoutId;

    watchers.push(
        watch(
            () => {
                if (props.point.lat > 90) {
                    props.point.lat = 90;
                }
                if (props.point.lat < -90) {
                    props.point.lat = -90;
                }

                if (props.point.lon > 180) {
                    props.point.lon = 180;
                }
                if (props.point.lon < -180) {
                    props.point.lon = -180;
                }
            },
            () => {
                panInside();
            },
        ),
    );

    function panInside(debounce = 200) {
        if (!map) {
            return;
        }

        clearTimeout(panTimeoutId);
        panTimeoutId = setTimeout(() => {
            marker?.setLatLng([props.point.lat, props.point.lon]);
            map?.panInside([props.point.lat, props.point.lon], { padding: [20, 40] });
        }, debounce);
    }

    function initMap(mapEl) {
        const latlon = [toFixedCoord(props.point.lat), toFixedCoord(props.point.lon)];

        map = L.map(mapEl, { zoomControl: false }).setView(latlon, defaultZoomLevel);

        L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
            attribution: "&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a>",
        }).addTo(map);

        // reassign the default marker images with the loaded ones
        // (https://leafletjs.com/reference.html#icon-default-option)
        L.Icon.Default.prototype.options.iconUrl = markerIconUrl;
        L.Icon.Default.prototype.options.iconRetinaUrl = markerIconRetinaUrl;
        L.Icon.Default.prototype.options.shadowUrl = markerShadowUrl;
        L.Icon.Default.imagePath = "";

        marker = L.marker(latlon, {
            draggable: true,
            autoPan: true,
        }).addTo(map);

        marker.bindTooltip("drag or right click anywhere on the map to move");

        marker.on("moveend", (e) => {
            if (e.sourceTarget?._latlng) {
                select(e.sourceTarget._latlng.lat, e.sourceTarget._latlng.lng, false);
            }
        });

        map.on("contextmenu", (e) => {
            select(e.latlng.lat, e.latlng.lng, false);
        });
    }

    function destroyMap() {
        clearTimeout(panTimeoutId);
        marker?.remove();
        map?.remove();
    }

    function select(lat, lon, centerMap = true) {
        const point = {
            lat: toFixedCoord(lat),
            lon: toFixedCoord(lon),
        };

        if (props.onchange && props.onchange(point) === false) {
            return;
        }

        props.point = point;

        // center the map
        if (centerMap) {
            marker?.setLatLng([props.point.lat, props.point.lon]); // optimistic marker update
            map?.panTo([props.point.lat, props.point.lon], { animate: false });
        }

        map.getContainer()?.dispatchEvent(new CustomEvent("change", { detail: point }));

        resetSearch?.();
    }

    const [searchEl, resetSearch] = initSearch(select);

    return t.div(
        {
            rid: props.rid,
            id: () => props.id,
            hidden: () => props.hidden,
            inert: () => props.inert,
            className: "map-container",
            onunmount: () => {
                watchers.forEach((w) => w?.unwatch());
            },
        },
        searchEl,
        t.div({
            className: "map-box",
            onmount: (el) => {
                initMap(el);
            },
            onunmount: () => {
                destroyMap();
            },
        }),
    );
};

function toFixedCoord(coord) {
    return +(+coord).toFixed(6) || 0;
}

function initSearch(selectFunc = null) {
    const data = store({
        searchTerm: "",
        isSearching: false,
        searchResults: [],
    });

    let searchTimeoutId;
    let searchAbortController;

    function reset() {
        searchAbortController?.abort("reset");
        clearTimeout(searchTimeoutId);

        data.isSearching = false;
        data.searchResults = [];
        data.searchTerm = "";
    }

    // note: using debounce > 1s to minimize hitting the API rate limits
    // (see also https://operations.osmfoundation.org/policies/nominatim/)
    function search(debounce = 1100) {
        clearTimeout(searchTimeoutId);
        searchAbortController?.abort("search debounce");

        data.isSearching = true;
        data.searchResults = [];

        if (!data.searchTerm) {
            data.isSearching = false;
            return;
        }

        searchTimeoutId = setTimeout(async () => {
            try {
                searchAbortController = new AbortController();

                const response = await fetch(
                    "https://nominatim.openstreetmap.org/search.php?format=jsonv2&q="
                        + encodeURIComponent(data.searchTerm),
                    { signal: searchAbortController.signal },
                );
                if (response.status != 200) {
                    throw new Error("OpenStreetMap API error " + response.status);
                }

                const results = [];

                const addresses = await response.json();
                for (const item of addresses) {
                    results.push({
                        lat: item.lat,
                        lon: item.lon,
                        name: item.display_name,
                    });
                }

                data.searchResults = results;
            } catch (err) {
                console.warn("[address search failed]", err);
            }

            data.isSearching = false;
        }, debounce);
    }

    const searchInput = t.div(
        { className: "fields" },
        t.div(
            { className: "field" },
            t.input({
                type: "text",
                placeholder: "Search address...",
                value: () => data.searchTerm,
                oninput: (e) => (data.searchTerm = e.target.value),
            }),
        ),
        t.div({ className: "field addon p-l-10 p-r-10" }, () => {
            if (data.isSearching) {
                return t.span({ className: "loader sm" });
            }

            if (data.searchTerm.length) {
                return t.button(
                    {
                        type: "button",
                        className: "link-hint",
                        title: "Clear search",
                        onclick: () => reset(),
                    },
                    t.i({ className: "ri-close-line", ariaHidden: true }),
                );
            }
        }),
    );

    const searchDropdown = t.div({ className: "dropdown", popover: "manual" }, () => {
        return data.searchResults.map((item) => {
            return t.button(
                {
                    type: "button",
                    className: "dropdown-item",
                    title: "Select address coordinates",
                    onclick: () => selectFunc?.(item.lat, item.lon),
                },
                item.name,
            );
        });
    });

    const watchers = [];

    return [
        t.div(
            {
                className: "map-search",
                onmount: () => {
                    watchers.push(
                        watch(
                            () => data.searchTerm,
                            (searchTerm) => {
                                search(searchTerm);
                            },
                        ),
                    );

                    watchers.push(
                        watch(
                            () => data.searchResults,
                            (results) => {
                                if (results.length) {
                                    searchDropdown.showPopover({ source: searchInput });
                                } else {
                                    searchDropdown.hidePopover();
                                }
                            },
                        ),
                    );
                },
                onunmount: () => {
                    watchers.forEach((w) => w?.unwatch());
                    reset();
                },
            },
            searchInput,
            searchDropdown,
        ),
        reset,
    ];
}
