import maplibregl from 'maplibre-gl'
import layers from './layers.ts'

/**
 * Attach/detach for regional PMTiles sources, shared by the app map and the
 * mini-map so the two can't drift on layer naming or insertion order. The
 * caller resolves the source URL (IndexedDB-backed or streamed) and tracks
 * which sources it wants; these own the source-plus-layers bookkeeping.
 */

/**
 * Where to insert regional layers: below the world place labels so country
 * and city names stay readable over regional water/roads, else below the
 * bookmark pins.
 */
function regionalBeforeId(map: maplibregl.Map): string | undefined {
  if (map.getLayer('place-continent')) return 'place-continent'
  if (map.getLayer('bookmarks-clusters')) return 'bookmarks-clusters'
  return undefined
}

// Keyed by map instance (not just source id): m-map and m-mini-map are
// separate maplibregl.Map instances that can each attach a source with the
// same region id, so per-id-only state would have one instance's popup or
// listener cleanup stomp on the other's.
const stationPopups = new WeakMap<maplibregl.Map, maplibregl.Popup>()

/** Shows a name-only popup for the clicked station, replacing any other
 * one already open on this map. */
function onStationClick(
  map: maplibregl.Map,
  e: maplibregl.MapLayerMouseEvent,
): void {
  const feature = e.features?.[0]
  const name = feature?.properties?.['name:latin']
  if (!feature || !name) return
  const coords = (feature.geometry as unknown as {
    coordinates: [number, number]
  }).coordinates
  stationPopups.get(map)?.remove()
  stationPopups.set(
    map,
    new maplibregl.Popup({ offset: 12, closeButton: false })
      .setLngLat(coords)
      .setText(name)
      .addTo(map),
  )
}

function onStationMouseEnter(map: maplibregl.Map): void {
  map.getCanvas().style.cursor = 'pointer'
}

function onStationMouseLeave(map: maplibregl.Map): void {
  map.getCanvas().style.cursor = ''
}

type StationListeners = {
  click: (e: maplibregl.MapLayerMouseEvent) => void
  enter: () => void
  leave: () => void
}

/** Per-map, per-source station click/hover listeners, kept so
 * `detachRegionalSource` can remove the exact same closures it registered —
 * `map.off` matches by reference, so without this a source that's
 * attached/detached repeatedly (panning a region in and out of view) would
 * stack duplicate listeners. */
const stationListeners = new WeakMap<
  maplibregl.Map,
  Map<string, StationListeners>
>()

function listenersForMap(map: maplibregl.Map): Map<string, StationListeners> {
  let forMap = stationListeners.get(map)
  if (!forMap) {
    forMap = new Map()
    stationListeners.set(map, forMap)
  }
  return forMap
}

/** Adds the source and its style layers. No-op if already attached. */
export function attachRegionalSource(
  map: maplibregl.Map,
  id: string,
  url: string,
  dark: boolean,
): void {
  if (map.getSource(id)) return
  map.addSource(id, { type: 'vector', url })
  const beforeId = regionalBeforeId(map)
  for (const layer of layers(id, dark)) {
    map.addLayer(layer as maplibregl.AddLayerObject, beforeId)
  }
  const stationLayerId = `${id}-transit-station`
  const listeners = {
    click: (e: maplibregl.MapLayerMouseEvent) => onStationClick(map, e),
    enter: () => onStationMouseEnter(map),
    leave: () => onStationMouseLeave(map),
  }
  map.on('click', stationLayerId, listeners.click)
  map.on('mouseenter', stationLayerId, listeners.enter)
  map.on('mouseleave', stationLayerId, listeners.leave)
  listenersForMap(map).set(id, listeners)
}

/** Removes the source's layers, then the source. Safe if absent. */
export function detachRegionalSource(map: maplibregl.Map, id: string): void {
  const stationLayerId = `${id}-transit-station`
  const forMap = listenersForMap(map)
  const listeners = forMap.get(id)
  if (listeners) {
    map.off('click', stationLayerId, listeners.click)
    map.off('mouseenter', stationLayerId, listeners.enter)
    map.off('mouseleave', stationLayerId, listeners.leave)
    forMap.delete(id)
  }
  // Layer ids don't depend on the palette — either theme's definitions name
  // the same layers, so `dark`'s value here is arbitrary.
  for (const layer of layers(id, false)) {
    if (map.getLayer(layer.id)) map.removeLayer(layer.id)
  }
  if (map.getSource(id)) map.removeSource(id)
}

/**
 * Re-applies every paint property from a freshly built (light or dark) layer
 * set to the equivalent already-attached style layers — the cheap way to
 * "re-theme" a live map without tearing down sources. No-ops per layer id
 * that isn't currently attached (a source not yet loaded, or already gone).
 */
export function applyLayerPalette(
  map: maplibregl.Map,
  layerDefs: { id: string; paint?: Record<string, unknown> }[],
): void {
  for (const { id, paint } of layerDefs) {
    if (!paint || !map.getLayer(id)) continue
    for (const [key, value] of Object.entries(paint)) {
      map.setPaintProperty(id, key, value)
    }
  }
}
