const LEAFLET_VERSION = "1.9.4";
const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VERSION}/dist/leaflet.js`;
const LEAFLET_CSS = `https://unpkg.com/leaflet@${LEAFLET_VERSION}/dist/leaflet.css`;

let leafletJsPromise = null;
let leafletCssTextPromise = null;

function loadLeafletJs() {
  if (window.L) return Promise.resolve(window.L);
  if (leafletJsPromise) return leafletJsPromise;

  leafletJsPromise = new Promise((resolve, reject) => {
    const existing = document.querySelector(`script[src="${LEAFLET_JS}"]`);
    if (existing) {
      if (window.L) {
        resolve(window.L);
        return;
      }
      existing.addEventListener("load", () => resolve(window.L), { once: true });
      existing.addEventListener("error", reject, { once: true });
      return;
    }

    const script = document.createElement("script");
    script.src = LEAFLET_JS;
    script.onload = () => resolve(window.L);
    script.onerror = reject;
    document.head.appendChild(script);
  });

  return leafletJsPromise;
}

function loadLeafletCssText() {
  if (leafletCssTextPromise) return leafletCssTextPromise;
  leafletCssTextPromise = fetch(LEAFLET_CSS).then((r) => {
    if (!r.ok) {
      throw new Error(`Failed to fetch Leaflet CSS: ${r.status}`);
    }
    return r.text();
  });
  return leafletCssTextPromise;
}

async function ensureLeafletCssInScope(el) {
  const rootNode = el.getRootNode();
  const target =
    rootNode instanceof ShadowRoot ? rootNode : document.head;

  if (target.querySelector?.('style[data-leaflet-css="true"]')) {
    return;
  }

  const cssText = await loadLeafletCssText();
  const style = document.createElement("style");
  style.setAttribute("data-leaflet-css", "true");
  style.textContent =
    cssText +
    `
    .anywidget-leaflet-root {
      width: 100%;
      height: 100%;
      min-height: 200px;
    }
    `;

  target.prepend(style);
}

export default {
  async render({ model, el }) {
    await ensureLeafletCssInScope(el);
    const L = await loadLeafletJs();

    el.style.display = "block";
    el.style.width = "100%";
    el.style.height = model.get("height");
    el.style.position = "relative";

    const mapEl = document.createElement("div");
    mapEl.className = "anywidget-leaflet-root";
    el.appendChild(mapEl);

    const mapOptions = {
      zoomControl: model.get("zoom_control"),
      attributionControl: model.get("attribution_control"),
    };

    const map = L.map(mapEl, mapOptions).setView(
      model.get("center"),
      model.get("zoom")
    );

    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: "© OpenStreetMap contributors",
    }).addTo(map);

    let suppressModelToView = false;
    let suppressViewToModel = false;

    function updateModelFromMap() {
      if (suppressViewToModel) return;

      suppressModelToView = true;
      const c = map.getCenter();
      const b = map.getBounds();

      model.set("center", [c.lat, c.lng]);
      model.set("zoom", map.getZoom());
      model.set("bounds", [
        [b.getSouth(), b.getWest()],
        [b.getNorth(), b.getEast()],
      ]);
      model.save_changes();
      suppressModelToView = false;
    }

    map.on("moveend zoomend", updateModelFromMap);

    let clickSeq = 0;
    map.on("click", (e) => {
      clickSeq += 1;
      model.set("clicked", {
        seq: clickSeq,
        lat: e.latlng.lat,
        lng: e.latlng.lng,
      });
      model.save_changes();
    });

    function updateMapFromModel() {
      if (suppressModelToView) return;

      suppressViewToModel = true;

      const nextCenter = model.get("center");
      const nextZoom = model.get("zoom");
      const nextHeight = model.get("height");

      if (nextHeight) {
        el.style.height = nextHeight;
      }

      const currentCenter = map.getCenter();
      const currentZoom = map.getZoom();

      const centerChanged =
        Math.abs(currentCenter.lat - nextCenter[0]) > 1e-9 ||
        Math.abs(currentCenter.lng - nextCenter[1]) > 1e-9;

      const zoomChanged = currentZoom !== nextZoom;

      if (centerChanged || zoomChanged) {
        map.setView(nextCenter, nextZoom);
      }

      requestAnimationFrame(() => map.invalidateSize());

      suppressViewToModel = false;
    }

    model.on("change:center", updateMapFromModel);
    model.on("change:zoom", updateMapFromModel);
    model.on("change:height", updateMapFromModel);

    requestAnimationFrame(() => map.invalidateSize());
    updateModelFromMap();

    return () => {
      map.remove();
    };
  },
};
