<template>
  <div class="relative h-full w-full" @click="contextMenu.tile.show = false; contextMenu.marker.show = false">
    <div
      v-if="mapsLoaded && maps.length === 0"
      class="absolute inset-0 z-[500] flex flex-col items-center justify-center gap-4 bg-base-200/90 p-6"
    >
      <p class="text-center text-lg">Map list is empty.</p>
      <p class="text-center text-sm opacity-80">
        Make sure you are logged in and at least one map exists in Admin (uncheck «Hidden» if needed).
      </p>
      <div class="flex gap-2">
        <NuxtLink to="/login" class="btn btn-sm">Login</NuxtLink>
        <NuxtLink to="/admin" class="btn btn-sm btn-primary">Admin</NuxtLink>
      </div>
    </div>
    <div ref="mapRef" class="map h-full w-full" />
    <!-- Grid coords & zoom (bottom-right) -->
    <div
      v-if="displayCoords"
      class="absolute bottom-2 right-2 z-[501] rounded-lg px-3 py-2 font-mono text-sm bg-base-100/95 backdrop-blur-sm border border-base-300/50 shadow"
      aria-label="Current grid position and zoom"
      title="mapId · x, y · zoom"
    >
      {{ mapid }} · {{ displayCoords.x }}, {{ displayCoords.y }} · z{{ displayCoords.z }}
    </div>
    <!-- Control panel -->
    <div
      class="absolute left-3 top-[10%] z-[502] flex transition-all duration-300 ease-out"
      :class="panelCollapsed ? 'w-12' : 'w-64'"
    >
      <div
        class="rounded-xl bg-base-100/80 backdrop-blur-xl border border-base-300/50 shadow-xl overflow-hidden transition-all duration-300 flex flex-col"
        :class="panelCollapsed ? 'w-12 items-center py-2' : 'w-56'"
      >
        <div v-show="!panelCollapsed" class="flex flex-col p-4 gap-4 flex-1 min-w-0">
            <!-- Zoom -->
            <section class="flex flex-col gap-2">
              <h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Zoom</h3>
              <div class="flex items-center gap-2">
                <button
                  type="button"
                  class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
                  title="Zoom in"
                  aria-label="Zoom in"
                  @click="zoomIn"
                >
                  <icons-icon-zoom-in />
                </button>
                <button
                  type="button"
                  class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
                  title="Zoom out"
                  aria-label="Zoom out"
                  @click="zoomOutControl"
                >
                  <icons-icon-zoom-out />
                </button>
                <button
                  type="button"
                  class="btn btn-outline btn-sm btn-square transition-all duration-200 hover:scale-105"
                  title="Reset view — center map and minimum zoom"
                  aria-label="Reset view"
                  @click="zoomOut"
                >
                  <icons-icon-home />
                </button>
              </div>
            </section>
            <!-- Display -->
            <section class="flex flex-col gap-2">
              <h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Display</h3>
              <label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2">
                <input v-model="showGridCoordinates" type="checkbox" class="checkbox checkbox-sm" />
                <span class="label-text">Show grid coordinates</span>
              </label>
              <label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2">
                <input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
                <span class="label-text">Hide markers</span>
              </label>
            </section>
            <!-- Navigation -->
            <section class="flex flex-col gap-3">
              <h3 class="text-xs font-semibold uppercase tracking-wider text-base-content/70">Navigation</h3>
              <div class="form-control">
                <label class="label py-0"><span class="label-text">Jump to Map</span></label>
                <select v-model="selectedMapId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
                  <option :value="null">Select map</option>
                  <option v-for="m in maps" :key="m.ID" :value="m.ID">{{ m.Name }}</option>
                </select>
              </div>
              <div class="form-control">
                <label class="label py-0"><span class="label-text">Overlay Map</span></label>
                <select v-model="overlayMapId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
                  <option :value="-1">None</option>
                  <option v-for="m in maps" :key="'overlay-' + m.ID" :value="m.ID">{{ m.Name }}</option>
                </select>
              </div>
              <div class="form-control">
                <label class="label py-0"><span class="label-text">Jump to Quest Giver</span></label>
                <select v-model="selectedMarkerId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
                  <option :value="null">Select quest giver</option>
                  <option v-for="q in questGivers" :key="q.id" :value="q.id">{{ q.name }}</option>
                </select>
              </div>
              <div class="form-control">
                <label class="label py-0"><span class="label-text">Jump to Player</span></label>
                <select v-model="selectedPlayerId" class="select select-bordered select-sm w-full focus:ring-2 focus:ring-primary">
                  <option :value="null">Select player</option>
                  <option v-for="p in players" :key="p.id" :value="p.id">{{ p.name }}</option>
                </select>
              </div>
            </section>
        </div>
        <button
          type="button"
          class="btn btn-ghost btn-sm btn-square shrink-0 m-1 transition-all duration-200 hover:scale-105"
          :title="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
          :aria-label="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
          @click="panelCollapsed = !panelCollapsed"
        >
          <icons-icon-chevron-right v-if="panelCollapsed" />
          <icons-icon-panel-left v-else />
        </button>
      </div>
    </div>
    <!-- Context menu (tile) -->
    <div
      v-show="contextMenu.tile.show"
      class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
      :style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
    >
      <button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.tile.data && (wipeTile(contextMenu.tile.data), (contextMenu.tile.show = false))">
        Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
      </button>
      <button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.tile.data && (openCoordSet(contextMenu.tile.data), (contextMenu.tile.show = false))">
        Rewrite tile coords
      </button>
    </div>
    <!-- Context menu (marker) -->
    <div
      v-show="contextMenu.marker.show"
      class="fixed z-[1000] bg-base-100/95 backdrop-blur-xl shadow-xl rounded-lg border border-base-300 py-1 min-w-[180px] transition-opacity duration-150"
      :style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
    >
      <button type="button" class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors" @click="contextMenu.marker.data?.id != null && (hideMarkerById(contextMenu.marker.data.id), (contextMenu.marker.show = false))">
        Hide marker {{ contextMenu.marker.data?.name }}
      </button>
    </div>
    <!-- Coord-set modal: close via Cancel, backdrop click, or Escape -->
    <dialog ref="coordSetModal" class="modal" @cancel="closeCoordSetModal">
      <div class="modal-box transition-all duration-200" @click.stop>
        <h3 class="font-bold text-lg">Rewrite tile coords</h3>
        <p class="py-2">From ({{ coordSetFrom.x }}, {{ coordSetFrom.y }}) to:</p>
        <div class="flex gap-2">
          <input v-model.number="coordSet.x" type="number" class="input input-bordered flex-1" placeholder="X" />
          <input v-model.number="coordSet.y" type="number" class="input input-bordered flex-1" placeholder="Y" />
        </div>
        <div class="modal-action">
          <form method="dialog" @submit="submitCoordSet">
            <button type="submit" class="btn btn-primary">Submit</button>
            <button type="button" class="btn" @click="closeCoordSetModal">Cancel</button>
          </form>
        </div>
      </div>
      <div class="modal-backdrop cursor-pointer" aria-label="Close" @click="closeCoordSetModal" />
    </dialog>
  </div>
</template>

<script setup lang="ts">
import { GridCoordLayer, HnHCRS, HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
import { SmartTileLayer } from '~/lib/SmartTileLayer'
import { Marker } from '~/lib/Marker'
import { Character } from '~/lib/Character'
import { UniqueList } from '~/lib/UniqueList'
import type L from 'leaflet'

const props = withDefaults(
  defineProps<{
    characterId?: number
    mapId?: number
    gridX?: number
    gridY?: number
    zoom?: number
  }>(),
  { characterId: -1, mapId: undefined, gridX: 0, gridY: 0, zoom: 1 }
)

const mapRef = ref<HTMLElement | null>(null)
const route = useRoute()
const api = useMapApi()

const showGridCoordinates = ref(false)
const hideMarkers = ref(false)
const panelCollapsed = ref(false)
const trackingCharacterId = ref(-1)
const maps = ref<{ ID: number; Name: string; size?: number }[]>([])
const mapsLoaded = ref(false)
const selectedMapId = ref<number | null>(null)
const overlayMapId = ref<number>(-1)
const questGivers = ref<{ id: number; name: string; marker?: any }[]>([])
const players = ref<{ id: number; name: string }[]>([])
const selectedMarkerId = ref<number | null>(null)
const selectedPlayerId = ref<number | null>(null)
const auths = ref<string[]>([])
const coordSetFrom = ref({ x: 0, y: 0 })
const coordSet = ref({ x: 0, y: 0 })
const coordSetModal = ref<HTMLDialogElement | null>(null)
const displayCoords = ref<{ x: number; y: number; z: number } | null>(null)

const contextMenu = reactive({
  tile: { show: false, x: 0, y: 0, data: null as { coords: { x: number; y: number } } | null },
  marker: { show: false, x: 0, y: 0, data: null as { id: number; name: string } | null },
})

let map: L.Map | null = null
let layer: InstanceType<typeof SmartTileLayer> | null = null
let overlayLayer: InstanceType<typeof SmartTileLayer> | null = null
let coordLayer: InstanceType<typeof GridCoordLayer> | null = null
let markerLayer: L.LayerGroup | null = null
let source: EventSource | null = null
let intervalId: ReturnType<typeof setInterval> | null = null
let mapid = 0
let markers: UniqueList<InstanceType<typeof Marker>> | null = null
let characters: UniqueList<InstanceType<typeof Character>> | null = null
let markersHidden = false
let autoMode = false

function toLatLng(x: number, y: number) {
  return map!.unproject([x, y], HnHMaxZoom)
}

function changeMap(id: number) {
  if (id === mapid) return
  mapid = id
  if (layer) {
    layer.map = mapid
    layer.redraw()
  }
  if (overlayLayer) {
    overlayLayer.map = -1
    overlayLayer.redraw()
  }
  if (markers && !markersHidden) {
    markers.getElements().forEach((it: any) => it.remove({ map: map!, markerLayer: markerLayer!, mapid }))
    markers.getElements().filter((it: any) => it.map === mapid).forEach((it: any) => it.add({ map: map!, markerLayer: markerLayer!, mapid }))
  }
  if (characters) {
    characters.getElements().forEach((it: any) => {
      it.remove({ map: map! })
      it.add({ map: map!, mapid })
    })
  }
}

function zoomIn() {
  map?.zoomIn()
}

function zoomOutControl() {
  map?.zoomOut()
}

function zoomOut() {
  trackingCharacterId.value = -1
  map?.setView([0, 0], HnHMinZoom, { animate: false })
}

function wipeTile(data: { coords: { x: number; y: number } }) {
  if (!data?.coords) return
  api.adminWipeTile({ map: mapid, x: data.coords.x, y: data.coords.y })
}

function closeCoordSetModal() {
  coordSetModal.value?.close()
}

function openCoordSet(data: { coords: { x: number; y: number } }) {
  if (!data?.coords) return
  coordSetFrom.value = { ...data.coords }
  coordSet.value = { x: data.coords.x, y: data.coords.y }
  coordSetModal.value?.showModal()
}

function submitCoordSet() {
  api.adminSetCoords({
    map: mapid,
    fx: coordSetFrom.value.x,
    fy: coordSetFrom.value.y,
    tx: coordSet.value.x,
    ty: coordSet.value.y,
  })
  coordSetModal.value?.close()
}

function hideMarkerById(id: number) {
  if (id == null) return
  api.adminHideMarker({ id })
  const m = markers?.byId(id)
  if (m) m.remove({ map: map!, markerLayer: markerLayer!, mapid })
}

function closeContextMenus() {
  contextMenu.tile.show = false
  contextMenu.marker.show = false
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape') closeContextMenus()
}

onMounted(async () => {
  if (import.meta.client) {
    window.addEventListener('keydown', onKeydown)
  }
  if (!import.meta.client || !mapRef.value) return
  const L = (await import('leaflet')).default

  const [charactersData, mapsData] = await Promise.all([
    api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
    api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
  ])

  const mapsList: { ID: number; Name: string; size?: number }[] = []
  const raw = mapsData as Record<string, { ID?: number; Name?: string; id?: number; name?: string; size?: number }>
  for (const id in raw) {
    const m = raw[id]
    if (!m || typeof m !== 'object') continue
    const idVal = m.ID ?? m.id
    const nameVal = m.Name ?? m.name
    if (idVal == null || nameVal == null) continue
    mapsList.push({ ID: Number(idVal), Name: String(nameVal), size: m.size })
  }
  mapsList.sort((a, b) => (b.size ?? 0) - (a.size ?? 0))
  maps.value = mapsList
  mapsLoaded.value = true

  const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
  if (config?.title) document.title = config.title
  if (config?.auths) auths.value = config.auths

  map = L.map(mapRef.value, {
    minZoom: HnHMinZoom,
    maxZoom: HnHMaxZoom,
    crs: HnHCRS,
    attributionControl: false,
    zoomControl: false,
    inertia: true,
    zoomAnimation: true,
    fadeAnimation: true,
    markerZoomAnimation: true,
  })

  const initialMapId =
    props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0
  mapid = initialMapId

  // Tiles are served at /map/grids/ (backend path, not SPA baseURL)
  const runtimeConfig = useRuntimeConfig()
  const apiBase = (runtimeConfig.public.apiBase as string) ?? '/map/api'
  const backendBase = apiBase.replace(/\/api\/?$/, '') || '/map'
  const tileUrl = `${backendBase}/grids/{map}/{z}/{x}_{y}.png?{cache}`
  layer = new SmartTileLayer(tileUrl, {
    minZoom: 1,
    maxZoom: 6,
    zoomOffset: 0,
    zoomReverse: true,
    tileSize: TileSize,
    updateWhenIdle: true,
    keepBuffer: 2,
  }) as any
  layer!.map = initialMapId
  layer!.invalidTile =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
  layer!.addTo(map)

  overlayLayer = new SmartTileLayer(tileUrl, {
    minZoom: 1,
    maxZoom: 6,
    zoomOffset: 0,
    zoomReverse: true,
    tileSize: TileSize,
    opacity: 0.5,
    updateWhenIdle: true,
    keepBuffer: 2,
  }) as any
  overlayLayer!.map = -1
  overlayLayer!.invalidTile =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
  overlayLayer!.addTo(map)

  coordLayer = new GridCoordLayer({
    tileSize: TileSize,
    minZoom: HnHMinZoom,
    maxZoom: HnHMaxZoom,
    opacity: 0,
    visible: false,
    pane: 'tilePane',
  } as any)
  coordLayer.addTo(map)
  coordLayer.setZIndex(500)

  markerLayer = L.layerGroup()
  markerLayer.addTo(map)
  markerLayer.setZIndex(600)

  const baseURL = useRuntimeConfig().app.baseURL ?? '/'
  const markerIconPath = baseURL.endsWith('/') ? baseURL : baseURL + '/'
  L.Icon.Default.imagePath = markerIconPath

  map.on('contextmenu', (mev: L.LeafletMouseEvent) => {
    if (auths.value.includes('admin')) {
      const point = map!.project(mev.latlng, 6)
      const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
      contextMenu.tile.show = true
      contextMenu.tile.x = mev.originalEvent.clientX
      contextMenu.tile.y = mev.originalEvent.clientY
      contextMenu.tile.data = { coords }
    }
  })

  // SSE is at /map/updates (backend path, not SPA baseURL). Same origin so it connects to correct host/port.
  const updatesPath = `${backendBase}/updates`
  const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
  source = new EventSource(updatesUrl)
  source.onmessage = (event: MessageEvent) => {
    try {
      const raw = event?.data
      if (raw == null || typeof raw !== 'string' || raw.trim() === '') return
      const updates = JSON.parse(raw)
      if (!Array.isArray(updates)) return
      for (const u of updates) {
        const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
        layer!.cache[key] = u.T
        if (overlayLayer) overlayLayer.cache[key] = u.T
        if (layer!.map === u.M) layer!.refresh(u.X, u.Y, u.Z)
        if (overlayLayer && overlayLayer.map === u.M) overlayLayer.refresh(u.X, u.Y, u.Z)
      }
    } catch {
      // Ignore parse errors (e.g. empty SSE message or non-JSON)
    }
  }
  source.onerror = () => {
    // Connection lost or 401; avoid uncaught errors
  }
  source.addEventListener('merge', (e: MessageEvent) => {
    try {
      const merge = JSON.parse(e?.data ?? '{}')
      if (mapid === merge.From) {
      const mapTo = merge.To
      const point = map!.project(map!.getCenter(), 6)
      const coordinate = {
        x: Math.floor(point.x / TileSize) + merge.Shift.x,
        y: Math.floor(point.y / TileSize) + merge.Shift.y,
        z: map!.getZoom(),
      }
      const latLng = toLatLng(coordinate.x * 100, coordinate.y * 100)
      changeMap(mapTo)
      api.getMarkers().then((body) => updateMarkers(Array.isArray(body) ? body : []))
      map!.setView(latLng, map!.getZoom())
    }
    } catch {
      // Ignore merge parse errors
    }
  })

  markers = new UniqueList<InstanceType<typeof Marker>>()
  characters = new UniqueList<InstanceType<typeof Character>>()

  updateCharacters(charactersData as any[])

  if (props.characterId !== undefined && props.characterId >= 0) {
    trackingCharacterId.value = props.characterId
  } else if (props.mapId != null && props.gridX != null && props.gridY != null && props.zoom != null) {
    const latLng = toLatLng(props.gridX * 100, props.gridY * 100)
    if (mapid !== props.mapId) changeMap(props.mapId)
    selectedMapId.value = props.mapId
    map.setView(latLng, props.zoom, { animate: false })
  } else if (mapsList.length > 0) {
    const first = mapsList[0]
    if (first) {
      changeMap(first.ID)
      selectedMapId.value = first.ID
      map.setView([0, 0], HnHDefaultZoom, { animate: false })
    }
  }

  // Recompute map size after layout (fixes grid/container height chain in Nuxt)
  nextTick(() => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (map) map.invalidateSize()
      })
    })
  })

  intervalId = setInterval(() => {
    api.getCharacters().then((body) => updateCharacters(Array.isArray(body) ? body : [])).catch(() => clearInterval(intervalId!))
  }, 2000)

  api.getMarkers().then((body) => updateMarkers(Array.isArray(body) ? body : []))

  function updateMarkers(markersData: any[]) {
    if (!markers || !map || !markerLayer) return
    const list = Array.isArray(markersData) ? markersData : []
    const ctx = { map, markerLayer, mapid, overlayLayer, auths: auths.value }
    markers.update(
      list.map((it) => new Marker(it)),
      (marker: InstanceType<typeof Marker>) => {
        if (marker.map === mapid || marker.map === overlayLayer?.map) marker.add(ctx)
        marker.setClickCallback(() => map!.setView(marker.marker!.getLatLng(), HnHMaxZoom))
        marker.setContextMenu((mev: L.LeafletMouseEvent) => {
          if (auths.value.includes('admin')) {
            contextMenu.marker.show = true
            contextMenu.marker.x = mev.originalEvent.clientX
            contextMenu.marker.y = mev.originalEvent.clientY
            contextMenu.marker.data = { id: marker.id, name: marker.name }
          }
        })
      },
      (marker: InstanceType<typeof Marker>) => marker.remove(ctx),
      (marker: InstanceType<typeof Marker>, updated: any) => marker.update(ctx, updated)
    )
    questGivers.value = markers.getElements().filter((it: any) => it.type === 'quest')
  }

  function updateCharacters(charactersData: any[]) {
    if (!characters || !map) return
    const list = Array.isArray(charactersData) ? charactersData : []
    const ctx = { map, mapid }
    characters.update(
      list.map((it) => new Character(it)),
      (character: InstanceType<typeof Character>) => {
        character.add(ctx)
        character.setClickCallback(() => (trackingCharacterId.value = character.id))
      },
      (character: InstanceType<typeof Character>) => character.remove(ctx),
      (character: InstanceType<typeof Character>, updated: any) => {
        if (trackingCharacterId.value === updated.id) {
          if (mapid !== updated.map) changeMap(updated.map)
          const latlng = map!.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
          map!.setView(latlng, HnHMaxZoom)
        }
        character.update(ctx, updated)
      }
    )
    players.value = characters.getElements()
  }

  watch(showGridCoordinates, (v) => {
    if (coordLayer) {
      ;(coordLayer.options as { visible?: boolean }).visible = v
      coordLayer.setOpacity(v ? 1 : 0)
      if (v && map) {
        coordLayer.bringToFront?.()
        coordLayer.redraw?.()
        map.invalidateSize()
      } else if (coordLayer) {
        coordLayer.redraw?.()
      }
    }
  })

  watch(hideMarkers, (v) => {
    markersHidden = v
    if (!markers) return
    const ctx = { map: map!, markerLayer: markerLayer!, mapid, overlayLayer }
    if (v) {
      markers.getElements().forEach((it: any) => it.remove(ctx))
    } else {
      markers.getElements().forEach((it: any) => it.remove(ctx))
      markers.getElements().filter((it: any) => it.map === mapid || it.map === overlayLayer?.map).forEach((it: any) => it.add(ctx))
    }
  })

  watch(trackingCharacterId, (value) => {
    if (value === -1) return
    const character = characters?.byId(value)
    if (character) {
      changeMap(character.map)
      const latlng = map!.unproject([character.position.x, character.position.y], HnHMaxZoom)
      map!.setView(latlng, HnHMaxZoom)
      autoMode = true
    } else {
      map!.setView([0, 0], HnHMinZoom)
      trackingCharacterId.value = -1
    }
  })

  watch(selectedMapId, (value) => {
    if (value == null) return
    changeMap(value)
    const zoom = map!.getZoom()
    map!.setView([0, 0], zoom)
  })

  watch(overlayMapId, (value) => {
    if (overlayLayer) overlayLayer.map = value ?? -1
    overlayLayer?.redraw()
    if (!markers) return
    const ctx = { map: map!, markerLayer: markerLayer!, mapid, overlayLayer }
    markers.getElements().forEach((it: any) => it.remove(ctx))
    markers.getElements().filter((it: any) => it.map === mapid || it.map === (value ?? -1)).forEach((it: any) => it.add(ctx))
  })

  watch(selectedMarkerId, (value) => {
    if (value == null) return
    const marker = markers?.byId(value)
    if (marker?.marker) map!.setView(marker.marker.getLatLng(), map!.getZoom())
  })

  watch(selectedPlayerId, (value) => {
    if (value != null) trackingCharacterId.value = value
  })

  function updateDisplayCoords() {
    if (!map) return
    const point = map.project(map.getCenter(), 6)
    displayCoords.value = {
      x: Math.floor(point.x / TileSize),
      y: Math.floor(point.y / TileSize),
      z: map.getZoom(),
    }
  }

  map.on('moveend', updateDisplayCoords)
  updateDisplayCoords()
  map.on('zoomend', () => {
    if (map) map.invalidateSize()
  })
  map.on('drag', () => {
    trackingCharacterId.value = -1
  })
  map.on('zoom', () => {
    if (autoMode) {
      autoMode = false
    } else {
      trackingCharacterId.value = -1
    }
  })
})

onBeforeUnmount(() => {
  if (import.meta.client) {
    window.removeEventListener('keydown', onKeydown)
  }
  if (intervalId) clearInterval(intervalId)
  if (source) source.close()
  if (map) map.remove()
})
</script>

<style scoped>
.map {
  height: 100%;
  min-height: 400px;
}
:deep(.leaflet-container .leaflet-tile-pane img.leaflet-tile) {
  mix-blend-mode: normal;
  visibility: visible !important;
}
:deep(.map-tile) {
  width: 100px;
  height: 100px;
  min-width: 100px;
  min-height: 100px;
  position: relative;
  border-bottom: 1px solid rgba(255, 255, 255, 0.3);
  border-right: 1px solid rgba(255, 255, 255, 0.3);
  color: #fff;
  font-size: 11px;
  line-height: 1.2;
  pointer-events: none;
  text-shadow: 0 0 2px #000, 0 1px 2px #000;
}
:deep(.map-tile-text) {
  position: absolute;
  left: 2px;
  top: 2px;
}
</style>
