<template>
  <div class="relative h-full w-full" @click="(e: MouseEvent) => e.button === 0 && mapLogic.closeContextMenus()">
    <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" />
    <MapMapCoordsDisplay
      :mapid="mapLogic.state.mapid"
      :display-coords="mapLogic.state.displayCoords"
    />
    <MapControls
      :show-grid-coordinates="mapLogic.state.showGridCoordinates.value"
      @update:show-grid-coordinates="(v) => (mapLogic.state.showGridCoordinates.value = v)"
      :hide-markers="mapLogic.state.hideMarkers.value"
      @update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
      :selected-map-id="mapLogic.state.selectedMapId.value"
      @update:selected-map-id="(v) => (mapLogic.state.selectedMapId.value = v)"
      :overlay-map-id="mapLogic.state.overlayMapId.value"
      @update:overlay-map-id="(v) => (mapLogic.state.overlayMapId.value = v)"
      :selected-marker-id="mapLogic.state.selectedMarkerId.value"
      @update:selected-marker-id="(v) => (mapLogic.state.selectedMarkerId.value = v)"
      :selected-player-id="mapLogic.state.selectedPlayerId.value"
      @update:selected-player-id="(v) => (mapLogic.state.selectedPlayerId.value = v)"
      :maps="maps"
      :quest-givers="questGivers"
      :players="players"
      @zoom-in="mapLogic.zoomIn(leafletMap)"
      @zoom-out="mapLogic.zoomOutControl(leafletMap)"
      @reset-view="mapLogic.resetView(leafletMap)"
    />
    <MapMapContextMenu
      :context-menu="mapLogic.contextMenu"
      @wipe-tile="onWipeTile"
      @rewrite-coords="onRewriteCoords"
      @hide-marker="onHideMarker"
    />
    <MapMapCoordSetModal
      :coord-set-from="mapLogic.coordSetFrom"
      :coord-set="mapLogic.coordSet"
      :open="mapLogic.coordSetModalOpen"
      @close="mapLogic.closeCoordSetModal()"
      @submit="onSubmitCoordSet"
    />
  </div>
</template>

<script setup lang="ts">
import MapControls from '~/components/map/MapControls.vue'
import { HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
import { initLeafletMap, type MapInitResult } from '~/composables/useMapInit'
import { startMapUpdates, type UseMapUpdatesReturn } from '~/composables/useMapUpdates'
import { createMapLayers, type MapLayersManager } from '~/composables/useMapLayers'
import type { MapInfo, ConfigResponse, MeResponse } from '~/types/api'
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 api = useMapApi()
const mapLogic = useMapLogic()

const maps = ref<MapInfo[]>([])
const mapsLoaded = ref(false)
const questGivers = ref<Array<{ id: number; name: string }>>([])
const players = ref<Array<{ id: number; name: string }>>([])
const auths = ref<string[]>([])

let leafletMap: L.Map | null = null
let mapInit: MapInitResult | null = null
let updatesHandle: UseMapUpdatesReturn | null = null
let layersManager: MapLayersManager | null = null
let intervalId: ReturnType<typeof setInterval> | null = null
let autoMode = false
let mapContainer: HTMLElement | null = null
let contextMenuHandler: ((ev: MouseEvent) => void) | null = null

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

function onWipeTile(coords: { x: number; y: number } | undefined) {
  if (!coords) return
  mapLogic.closeContextMenus()
  api.adminWipeTile({ map: mapLogic.state.mapid.value, x: coords.x, y: coords.y })
}

function onRewriteCoords(coords: { x: number; y: number } | undefined) {
  if (!coords) return
  mapLogic.closeContextMenus()
  mapLogic.openCoordSet(coords)
}

function onHideMarker(id: number | undefined) {
  if (id == null) return
  mapLogic.closeContextMenus()
  api.adminHideMarker({ id })
  const m = layersManager?.findMarkerById(id)
  if (m) m.remove({ map: leafletMap!, markerLayer: mapInit!.markerLayer, mapid: mapLogic.state.mapid.value })
}

function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: number }) {
  api.adminSetCoords({
    map: mapLogic.state.mapid.value,
    fx: from.x,
    fy: from.y,
    tx: to.x,
    ty: to.y,
  })
  mapLogic.closeCoordSetModal()
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape') mapLogic.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: MapInfo[] = []
  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 ConfigResponse
  if (config?.title) document.title = config.title
  const user = (await api.me().catch(() => null)) as MeResponse | null
  auths.value = user?.auths ?? config?.auths ?? []

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

  mapInit = await initLeafletMap(mapRef.value, mapsList, initialMapId)
  leafletMap = mapInit.map

  // Document-level capture so we get contextmenu before any map layer or iframe can swallow it
  mapContainer = leafletMap.getContainer()
  contextMenuHandler = (ev: MouseEvent) => {
    const target = ev.target as Node
    if (!mapContainer?.contains(target)) return
    const isAdmin = auths.value.includes('admin')
    if (import.meta.dev) console.log('[MapView contextmenu]', { isAdmin, auths: auths.value })
    if (isAdmin) {
      ev.preventDefault()
      ev.stopPropagation()
      const rect = mapContainer.getBoundingClientRect()
      const containerPoint = L.point(ev.clientX - rect.left, ev.clientY - rect.top)
      const latlng = leafletMap!.containerPointToLatLng(containerPoint)
      const point = leafletMap!.project(latlng, 6)
      const coords = { x: Math.floor(point.x / TileSize), y: Math.floor(point.y / TileSize) }
      mapLogic.openTileContextMenu(ev.clientX, ev.clientY, coords)
    }
  }
  document.addEventListener('contextmenu', contextMenuHandler, true)

  layersManager = createMapLayers({
    map: leafletMap,
    markerLayer: mapInit.markerLayer,
    layer: mapInit.layer,
    overlayLayer: mapInit.overlayLayer,
    getCurrentMapId: () => mapLogic.state.mapid.value,
    setCurrentMapId: (id: number) => { mapLogic.state.mapid.value = id },
    setSelectedMapId: (id: number) => { mapLogic.state.selectedMapId.value = id },
    getAuths: () => auths.value,
    getTrackingCharacterId: () => mapLogic.state.trackingCharacterId.value,
    setTrackingCharacterId: (id: number) => { mapLogic.state.trackingCharacterId.value = id },
    onMarkerContextMenu: mapLogic.openMarkerContextMenu,
  })

  updatesHandle = startMapUpdates({
    backendBase: mapInit.backendBase,
    layer: mapInit.layer,
    overlayLayer: mapInit.overlayLayer,
    map: leafletMap,
    getCurrentMapId: () => mapLogic.state.mapid.value,
    onMerge: (mapTo: number, shift: { x: number; y: number }) => {
      const latLng = toLatLng(shift.x * 100, shift.y * 100)
      layersManager!.changeMap(mapTo)
      api.getMarkers().then((body) => {
        layersManager!.updateMarkers(Array.isArray(body) ? body : [])
        questGivers.value = layersManager!.getQuestGivers()
      })
      leafletMap!.setView(latLng, leafletMap!.getZoom())
    },
  })

  layersManager.updateCharacters(Array.isArray(charactersData) ? charactersData : [])
  players.value = layersManager.getPlayers()

  if (props.characterId !== undefined && props.characterId >= 0) {
    mapLogic.state.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 (mapLogic.state.mapid.value !== props.mapId) layersManager.changeMap(props.mapId)
    mapLogic.state.selectedMapId.value = props.mapId
    leafletMap.setView(latLng, props.zoom, { animate: false })
  } else if (mapsList.length > 0) {
    const first = mapsList[0]
    if (first) {
      layersManager.changeMap(first.ID)
      mapLogic.state.selectedMapId.value = first.ID
      leafletMap.setView([0, 0], HnHDefaultZoom, { animate: false })
    }
  }

  nextTick(() => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (leafletMap) leafletMap.invalidateSize()
      })
    })
  })

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

  api.getMarkers().then((body) => {
    layersManager!.updateMarkers(Array.isArray(body) ? body : [])
    questGivers.value = layersManager!.getQuestGivers()
  })

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

  watch(mapLogic.state.hideMarkers, (v) => {
    layersManager?.refreshMarkersVisibility(v)
  })

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

  watch(mapLogic.state.selectedMapId, (value) => {
    if (value == null) return
    layersManager?.changeMap(value)
    const zoom = leafletMap!.getZoom()
    leafletMap!.setView([0, 0], zoom)
  })

  watch(mapLogic.state.overlayMapId, (value) => {
    layersManager?.refreshOverlayMarkers(value ?? -1)
  })

  watch(mapLogic.state.selectedMarkerId, (value) => {
    if (value == null) return
    const marker = layersManager?.findMarkerById(value)
    if (marker?.leafletMarker) leafletMap!.setView(marker.leafletMarker.getLatLng(), leafletMap!.getZoom())
  })

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

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

onBeforeUnmount(() => {
  if (import.meta.client) {
    window.removeEventListener('keydown', onKeydown)
  }
  if (contextMenuHandler) {
    document.removeEventListener('contextmenu', contextMenuHandler, true)
  }
  if (intervalId) clearInterval(intervalId)
  updatesHandle?.cleanup()
  if (leafletMap) leafletMap.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>
