<template>
  <div class="yak-ol-map">
    <div :ref="setMapElement" class="yak-ol-map__canvas"></div>
    <div
      v-if="coordinateCopyTip.visible"
      class="yak-ol-map__copy-tip"
      :class="{ 'is-error': coordinateCopyTip.error }"
      :style="{ left: `${coordinateCopyTip.x}px`, top: `${coordinateCopyTip.y}px` }"
    >
      {{ coordinateCopyTip.text }}
    </div>
    <div
      v-if="coordinateCopyMenu.visible"
      class="yak-ol-map__copy-menu"
      :class="{ 'is-error': coordinateCopyMenu.error }"
      :style="{ left: `${coordinateCopyMenu.x}px`, top: `${coordinateCopyMenu.y}px` }"
      @click.stop
    >
      <span>{{ coordinateCopyMenu.text }}</span>
      <button type="button" @click="handleCoordinateMenuCopy">复制</button>
    </div>
  </div>
</template>

<script setup>
import "ol/ol.css"
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
import Map from "ol/Map.js"
import View from "ol/View.js"
import Feature from "ol/Feature.js"
import GeoJSON from "ol/format/GeoJSON.js"
import ImageLayer from "ol/layer/Image.js"
import TileLayer from "ol/layer/Tile.js"
import VectorLayer from "ol/layer/Vector.js"
import Point from "ol/geom/Point.js"
import ImageWMS from "ol/source/ImageWMS.js"
import TileWMS from "ol/source/TileWMS.js"
import XYZ from "ol/source/XYZ.js"
import VectorSource from "ol/source/Vector.js"
import { bbox as bboxStrategy } from "ol/loadingstrategy.js"
import { defaults as defaultControls } from "ol/control/defaults.js"
import { unByKey } from "ol/Observable.js"
import { fromLonLat, toLonLat } from "ol/proj.js"
import CircleStyle from "ol/style/Circle.js"
import Fill from "ol/style/Fill.js"
import RegularShape from "ol/style/RegularShape.js"
import Stroke from "ol/style/Stroke.js"
import Style from "ol/style/Style.js"
import Text from "ol/style/Text.js"
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"

const props = defineProps({
  active: {
    type: Boolean,
    default: true,
  },
  tileUrlTemplate: {
    type: String,
    default: "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png",
  },
  center: {
    type: Array,
    default: () => [103.07984, 33.029058],
  },
  minZoom: {
    type: Number,
    default: 3,
  },
  maxZoom: {
    type: Number,
    default: 21,
  },
  initialZoom: {
    type: Number,
    default: 21,
  },
  overlayLayer: {
    type: Object,
    default: () => ({
      sourceType: "wms",
      layerName: "ne:daping_yak_marks",
      styles: "yak_marks",
      cqlFilter: "gov_show = true",
      name: "牦牛识别标记",
      color: "#FFFF00",
      opacity: 0.96,
    }),
  },
})

const emit = defineEmits(["ready", "tile-status", "layer-status", "map-click"])

const HONGYUAN_CENTER = [103.07984, 33.029058]
const INITIAL_ZOOM = 21
const boundaryFormat = new GeoJSON()
const boundaryGlowStyle = new Style({
  stroke: new Stroke({
    color: "rgba(48, 220, 255, 0.32)",
    width: 5,
  }),
})
const boundaryLineStyle = new Style({
  fill: new Fill({
    color: "rgba(3, 18, 31, 0.02)",
  }),
  stroke: new Stroke({
    color: "rgba(126, 251, 246, 0.82)",
    width: 1.4,
  }),
})
const pastureFocusStyle = [
  new Style({
    fill: new Fill({ color: "rgba(255, 76, 96, 0.2)" }),
    stroke: new Stroke({ color: "rgba(255, 76, 96, 0.42)", width: 8 }),
  }),
  new Style({
    stroke: new Stroke({ color: "rgba(255, 48, 68, 0.98)", width: 2.8 }),
  }),
]
const labelPointStyle = new Style({
  image: new CircleStyle({
    radius: 2.4,
    fill: new Fill({ color: "rgba(234, 255, 255, 0.92)" }),
    stroke: new Stroke({ color: "rgba(48, 220, 255, 0.9)", width: 1 }),
  }),
})

const mapEl = ref(null)
let olMap = null
let baseSource = null
let overlaySource = null
let overlayMapLayer = null
let pastureFocusSource = null
let pastureFocusLayer = null
let contextMenuHandler = null
let rightPointerDownHandler = null
let coordinateTipTimer = null
let lastRightClickCopyTime = 0
let eventKeys = []
let wmsEventKeys = []
const coordinateCopyTip = ref({
  visible: false,
  text: "",
  x: 0,
  y: 0,
  error: false,
})
const coordinateCopyMenu = ref({
  visible: false,
  text: "",
  x: 0,
  y: 0,
  error: false,
})
const pendingTiles = new Set()
const tileState = {
  requested: 0,
  loaded: 0,
  failed: 0,
  zoom: INITIAL_ZOOM,
}

function setMapElement(el) {
  mapEl.value = el || null
  if (el) initMap()
}

function tileCoordKey(event) {
  const coord = event?.tile?.getTileCoord?.()
  return Array.isArray(coord) ? coord.join("/") : ""
}

function currentZoom() {
  const zoom = Number(olMap?.getView?.()?.getZoom?.())
  return Number.isFinite(zoom) ? Math.round(zoom) : normalizeInitialZoom()
}

function normalizeCenter(center = props.center) {
  const [lng, lat] = Array.isArray(center) ? center : []
  const nextLng = Number(lng)
  const nextLat = Number(lat)
  return Number.isFinite(nextLng) && Number.isFinite(nextLat) ? [nextLng, nextLat] : HONGYUAN_CENTER
}

function locateMap(center = props.center) {
  const view = olMap?.getView?.()
  if (!view) return
  const zoom = normalizeInitialZoom()
  resetTileState(zoom)
  view.setCenter(fromLonLat(normalizeCenter(center)))
  view.setZoom(zoom)
  window.setTimeout(() => olMap?.updateSize?.(), 0)
}

function normalizeInitialZoom() {
  const zoom = Number(props.initialZoom)
  const minZoom = Number(props.minZoom)
  const maxZoom = Number(props.maxZoom)
  const safeZoom = Number.isFinite(zoom) ? zoom : INITIAL_ZOOM
  return Math.min(Number.isFinite(maxZoom) ? maxZoom : INITIAL_ZOOM, Math.max(Number.isFinite(minZoom) ? minZoom : 0, safeZoom))
}

function resetTileState(zoom = currentZoom()) {
  pendingTiles.clear()
  tileState.requested = 0
  tileState.loaded = 0
  tileState.failed = 0
  tileState.zoom = zoom
}

function emitTileStatus(type, text) {
  emit("tile-status", {
    type,
    text,
    loaded: tileState.loaded,
    failed: tileState.failed,
    total: tileState.requested,
    zoom: tileState.zoom,
    tileUrlTemplate: props.tileUrlTemplate,
  })
}

function emitLayerStatus(type, text) {
  emit("layer-status", {
    type,
    text,
  })
}

function buildBaseSource() {
  baseSource = new XYZ({
    url: props.tileUrlTemplate,
    minZoom: props.minZoom,
    maxZoom: props.maxZoom,
    crossOrigin: "anonymous",
    interpolate: true,
    transition: 0,
    wrapX: false,
  })

  eventKeys.push(
    baseSource.on("tileloadstart", (event) => {
      const key = tileCoordKey(event)
      if (key && pendingTiles.has(key)) return
      if (key) pendingTiles.add(key)
      tileState.requested += 1
      tileState.zoom = currentZoom()
      emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`)
    }),
  )
  eventKeys.push(
    baseSource.on("tileloadend", (event) => {
      const key = tileCoordKey(event)
      if (key && !pendingTiles.has(key)) return
      if (key) pendingTiles.delete(key)
      tileState.loaded += 1
      tileState.zoom = currentZoom()
      const done = pendingTiles.size === 0
      emitTileStatus(done ? "success" : "loading", done ? "hy-result 影像底图已加载" : `正在加载 hy-result ${tileState.zoom}级影像底图`)
    }),
  )
  eventKeys.push(
    baseSource.on("tileloaderror", (event) => {
      const key = tileCoordKey(event)
      if (key && !pendingTiles.has(key)) return
      if (key) pendingTiles.delete(key)
      tileState.failed += 1
      tileState.zoom = currentZoom()
      const type = tileState.loaded > 0 ? "warning" : "error"
      emitTileStatus(type, type === "warning" ? "hy-result 影像底图覆盖不足" : "hy-result 影像底图加载失败")
    }),
  )

  return baseSource
}

function normalizeOverlayLayer(layer = props.overlayLayer) {
  if (!layer || layer.visible === false || !layer.layerName) return null
  const sourceType = layer.sourceType || "wms"
  return {
    sourceType,
    url: layer.url || (sourceType === "wfs" ? "/geoserver/ne/wfs" : "/geoserver/ne/wms"),
    renderMode: layer.renderMode || "tile",
    layerName: layer.layerName,
    styles: layer.styles || "",
    sldBody: layer.sldBody || "",
    cqlFilter: layer.cqlFilter || "",
    name: layer.name || "业务图层",
    color: layer.color || layer.strokeColor || layer.fillColor || "#FFFF00",
    fillColor: layer.fillColor || layer.color || "rgba(255, 227, 90, 0.05)",
    strokeColor: layer.strokeColor || layer.color || "#FFFF00",
    glowColor: layer.glowColor || "rgba(255, 227, 90, 0.3)",
    opacity: Number.isFinite(Number(layer.opacity)) ? Number(layer.opacity) : 0.96,
    lineWidth: Number.isFinite(Number(layer.lineWidth)) ? Number(layer.lineWidth) : 2.6,
    zIndex: Number.isFinite(Number(layer.zIndex)) ? Number(layer.zIndex) : 20,
    maxFeatures: layer.maxFeatures || 12000,
    geometryField: layer.geometryField || "geom",
    loadingStrategy: layer.loadingStrategy || "",
    propertyName: layer.propertyName || (layer.boundaryOnly ? [layer.geometryField || "geom"] : undefined),
    bboxPadding: Number.isFinite(Number(layer.bboxPadding)) ? Number(layer.bboxPadding) : 0.003,
  }
}

function buildOverlayWmsParams(layer) {
  const params = {
    SERVICE: "WMS",
    VERSION: "1.1.1",
    LAYERS: layer.layerName,
    FORMAT: "image/png",
    TRANSPARENT: true,
    TILED: true,
  }
  if (layer.styles) params.STYLES = layer.styles
  if (layer.cqlFilter) params.CQL_FILTER = layer.cqlFilter
  if (layer.sldBody) params.SLD_BODY = layer.sldBody
  return params
}

function bindOverlayImageEvents(source, layer) {
  wmsEventKeys.push(source.on("imageloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
  wmsEventKeys.push(source.on("imageloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
  wmsEventKeys.push(source.on("imageloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
}

function bindOverlayTileEvents(source, layer) {
  wmsEventKeys.push(source.on("tileloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
  wmsEventKeys.push(source.on("tileloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
  wmsEventKeys.push(source.on("tileloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
}

function buildOverlayImageWmsSource(layer) {
  const source = new ImageWMS({
    url: layer.url,
    params: buildOverlayWmsParams(layer),
    serverType: "geoserver",
    crossOrigin: "anonymous",
    ratio: 1,
  })

  bindOverlayImageEvents(source, layer)
  return source
}

function buildOverlayWmsSource(layer) {
  const source = new TileWMS({
    url: layer.url,
    params: buildOverlayWmsParams(layer),
    serverType: "geoserver",
    crossOrigin: "anonymous",
    transition: 0,
  })

  bindOverlayTileEvents(source, layer)
  return source
}

function combineCqlFilters(...filters) {
  const clauses = filters.map((item) => String(item || "").trim()).filter(Boolean)
  if (!clauses.length) return ""
  return clauses.length === 1 ? clauses[0] : clauses.map((item) => `(${item})`).join(" AND ")
}

function buildOverlayWfsUrl(layer, options = {}) {
  const params = new URLSearchParams({
    service: "WFS",
    version: "1.0.0",
    request: "GetFeature",
    typeName: layer.layerName,
    outputFormat: "json",
    maxFeatures: String(layer.maxFeatures),
  })
  const propertyName = options.propertyName ?? layer.propertyName
  if (Array.isArray(propertyName) && propertyName.length) params.set("propertyName", propertyName.join(","))
  else if (propertyName) params.set("propertyName", propertyName)
  const cqlFilter = combineCqlFilters(layer.cqlFilter, options.cqlFilter)
  if (cqlFilter) params.set("CQL_FILTER", cqlFilter)
  return `${layer.url}?${params.toString()}`
}

function createBboxCqlFilter(layer, extent) {
  const bottomLeft = toLonLat([extent[0], extent[1]])
  const topRight = toLonLat([extent[2], extent[3]])
  const padding = layer.bboxPadding
  const minLng = Math.min(bottomLeft[0], topRight[0]) - padding
  const minLat = Math.min(bottomLeft[1], topRight[1]) - padding
  const maxLng = Math.max(bottomLeft[0], topRight[0]) + padding
  const maxLat = Math.max(bottomLeft[1], topRight[1]) + padding
  return `BBOX(${layer.geometryField},${minLng},${minLat},${maxLng},${maxLat})`
}

function buildOverlayBboxVectorSource(layer) {
  const source = new VectorSource({
    wrapX: false,
    strategy: bboxStrategy,
  })
  const format = new GeoJSON()
  const loadedFeatureIds = new Set()
  source.setLoader((extent, resolution, projection, success, failure) => {
    emitLayerStatus("loading", `正在加载${layer.name}`)
    fetch(buildOverlayWfsUrl(layer, { cqlFilter: createBboxCqlFilter(layer, extent) }), { credentials: "include" })
      .then((response) => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`)
        return response.json()
      })
      .then((geojson) => {
        if (source !== overlaySource) return
        const features = format.readFeatures(geojson, {
          dataProjection: "EPSG:4326",
          featureProjection: projection,
        })
        const nextFeatures = features.filter((feature) => {
          const id = feature.getId?.()
          if (!id || loadedFeatureIds.has(id)) return false
          loadedFeatureIds.add(id)
          return true
        })
        source.addFeatures(nextFeatures)
        success?.(nextFeatures)
        emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
      })
      .catch(() => {
        if (source !== overlaySource) return
        source.removeLoadedExtent?.(extent)
        failure?.()
        emitLayerStatus("error", `${layer.name}加载失败`)
      })
  })
  return source
}

function buildOverlayVectorSource(layer) {
  if (layer.loadingStrategy === "bbox") return buildOverlayBboxVectorSource(layer)
  const source = new VectorSource({ wrapX: false })
  const format = new GeoJSON()
  emitLayerStatus("loading", `正在加载${layer.name}`)
  fetch(buildOverlayWfsUrl(layer), { credentials: "include" })
    .then((response) => {
      if (!response.ok) throw new Error(`HTTP ${response.status}`)
      return response.json()
    })
    .then((geojson) => {
      if (source !== overlaySource) return
      const features = format.readFeatures(geojson, {
        dataProjection: "EPSG:4326",
        featureProjection: "EPSG:3857",
      })
      source.clear(true)
      source.addFeatures(features)
      emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
    })
    .catch(() => {
      if (source !== overlaySource) return
      emitLayerStatus("error", `${layer.name}加载失败`)
    })
  return source
}

function createOverlayVectorStyle(layer) {
  const lineWidth = layer.lineWidth
  return [
    new Style({
      fill: new Fill({ color: layer.fillColor }),
      stroke: new Stroke({ color: layer.glowColor, width: lineWidth + 5 }),
    }),
    new Style({
      stroke: new Stroke({ color: layer.strokeColor, width: lineWidth }),
    }),
  ]
}

function createOverlayMapLayer() {
  const layer = normalizeOverlayLayer()
  if (!layer) return null
  if (layer.sourceType === "wfs") {
    overlaySource = buildOverlayVectorSource(layer)
    const overlayVectorStyle = createOverlayVectorStyle(layer)
    return new VectorLayer({
      source: overlaySource,
      style: () => overlayVectorStyle,
      zIndex: layer.zIndex,
      renderBuffer: 160,
      updateWhileAnimating: true,
      updateWhileInteracting: true,
    })
  }
  if (layer.renderMode === "image") {
    overlaySource = buildOverlayImageWmsSource(layer)
    return new ImageLayer({
      source: overlaySource,
      opacity: layer.opacity,
      zIndex: layer.zIndex,
    })
  }
  overlaySource = buildOverlayWmsSource(layer)
  return new TileLayer({
    source: overlaySource,
    opacity: layer.opacity,
    zIndex: layer.zIndex,
  })
}

function refreshOverlayLayer() {
  if (!olMap) return
  if (overlayMapLayer) {
    olMap.removeLayer(overlayMapLayer)
    overlayMapLayer = null
  }
  if (wmsEventKeys.length) {
    unByKey(wmsEventKeys)
    wmsEventKeys = []
  }
  overlaySource = null
  overlayMapLayer = createOverlayMapLayer()
  if (overlayMapLayer) olMap.addLayer(overlayMapLayer)
}

function createBoundaryLayers() {
  const boundaryFeatures = boundaryFormat.readFeatures(hongyuanTownshipsGeoJson, {
    dataProjection: "EPSG:4326",
    featureProjection: "EPSG:3857",
  })
  const boundarySource = new VectorSource({
    features: boundaryFeatures,
  })

  const labelFeatures = hongyuanTownshipLabelPoints
    .filter((item) => item.name && Array.isArray(item.center))
    .map((item) => {
      const feature = new Feature({
        geometry: new Point(fromLonLat(item.center)),
        name: item.name,
      })
      feature.setId(`yak-town-label-${item.name}`)
      return feature
    })
  const labelSource = new VectorSource({
    features: labelFeatures,
  })

  return [
    new VectorLayer({
      source: boundarySource,
      style: () => boundaryGlowStyle,
      zIndex: 28,
      renderBuffer: 80,
      updateWhileAnimating: true,
      updateWhileInteracting: true,
    }),
    new VectorLayer({
      source: boundarySource,
      style: () => boundaryLineStyle,
      zIndex: 29,
      renderBuffer: 80,
      updateWhileAnimating: true,
      updateWhileInteracting: true,
    }),
    new VectorLayer({
      source: labelSource,
      declutter: true,
      zIndex: 40,
      renderBuffer: 120,
      updateWhileAnimating: true,
      updateWhileInteracting: true,
      style: createTownLabelStyle,
    }),
  ]
}

function createPastureFocusLayer() {
  pastureFocusSource = new VectorSource({ wrapX: false })
  pastureFocusLayer = new VectorLayer({
    source: pastureFocusSource,
    style: () => pastureFocusStyle,
    zIndex: 180,
    renderBuffer: 160,
    updateWhileAnimating: true,
    updateWhileInteracting: true,
  })
  return pastureFocusLayer
}

function normalizePastureGrasslandRows(value) {
  if (!value) return []
  if (typeof value === "string") return [value]
  if (Array.isArray(value)) return value
  if (value?.type === "FeatureCollection" && Array.isArray(value.features)) return value.features
  if (value?.type === "Feature" || isGeoJsonGeometry(value)) return [value]
  if (Array.isArray(value?.rows)) return value.rows
  if (Array.isArray(value?.data)) return value.data
  if (value?.data) return normalizePastureGrasslandRows(value.data)
  if (Array.isArray(value?.list)) return value.list
  if (Array.isArray(value?.features)) return value.features
  if (value?.wkt || value?.geo || value?.geom || value?.geometryWkt || value?.geometry_wkt || value?.shape || value?.location || value?.geometry) return [value]
  return []
}

function normalizePastureGrasslandGeometry(row) {
  if (typeof row === "string") return parseGeometryText(row)
  if (row?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry)
  if (isGeoJsonGeometry(row)) return row
  if (typeof row?.geometry === "string") return parseGeometryText(row.geometry)
  if (row?.geometry?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry.geometry)
  if (isGeoJsonGeometry(row?.geometry)) return row.geometry
  return parseGeometryText(row?.wkt || row?.geo || row?.geom || row?.geometryWkt || row?.geometry_wkt || row?.shape || row?.location)
}

function normalizePastureGrasslandProperties(row) {
  if (!row || typeof row !== "object" || Array.isArray(row)) return { wkt: row }
  if (row.type === "Feature") return row.properties || {}
  return row
}

function parseGeometryText(value) {
  const text = String(value || "").trim()
  if (!text) return null
  if (text.startsWith("{") || text.startsWith("[")) {
    try {
      const parsed = JSON.parse(text)
      if (parsed?.type === "Feature") return normalizePastureGrasslandGeometry(parsed)
      if (isGeoJsonGeometry(parsed)) return parsed
    } catch (error) {
      return null
    }
  }
  return parseWktGeometry(text)
}

function isGeoJsonGeometry(value) {
  return Boolean(
    value &&
    ["Polygon", "MultiPolygon", "LineString", "MultiLineString", "Point", "MultiPoint"].includes(value.type) &&
    value.coordinates
  )
}

function parseWktGeometry(wkt) {
  const text = String(wkt || "").trim()
  if (!text) return null
  const typeMatch = text.match(/^([A-Z]+)(?:\s+[A-Z]+)?\s*\(/i)
  const type = typeMatch?.[1]?.toUpperCase()
  if (!type) return null
  const body = text.slice(text.indexOf("("))
  if (type === "POLYGON") return { type: "Polygon", coordinates: parseWktPolygonBody(body) }
  if (type === "MULTIPOLYGON") return { type: "MultiPolygon", coordinates: getWktChildGroups(body).map(parseWktPolygonBody).filter((polygon) => polygon.length) }
  if (type === "POINT") return { type: "Point", coordinates: parseWktPoint(body) }
  if (type === "MULTIPOINT") return { type: "MultiPoint", coordinates: getWktChildGroups(body).map(parseWktPoint).filter(Boolean) }
  if (type === "LINESTRING") return { type: "LineString", coordinates: parseWktLine(body) }
  if (type === "MULTILINESTRING") return { type: "MultiLineString", coordinates: getWktChildGroups(body).map(parseWktLine).filter((line) => line.length) }
  return null
}

function parseWktPolygonBody(body) {
  return getWktChildGroups(body)
    .map(parseWktLine)
    .filter((ring) => ring.length >= 3)
}

function parseWktLine(text) {
  return String(text || "").replace(/^\(+|\)+$/g, "").split(",").map(parseWktPoint).filter(Boolean)
}

function parseWktPoint(text) {
  const point = String(text || "").replace(/^\(+|\)+$/g, "").trim().split(/\s+/).slice(0, 2).map(Number)
  return point.length >= 2 && Number.isFinite(point[0]) && Number.isFinite(point[1]) ? [point[0], point[1]] : null
}

function getWktChildGroups(text) {
  const trimmed = String(text || "").trim()
  const inner = trimmed.startsWith("(") && trimmed.endsWith(")") ? trimmed.slice(1, -1) : trimmed
  const groups = []
  let depth = 0
  let start = -1
  Array.from(inner).forEach((char, index) => {
    if (char === "(") {
      if (depth === 0) start = index
      depth += 1
    } else if (char === ")") {
      depth -= 1
      if (depth === 0 && start >= 0) {
        groups.push(inner.slice(start, index + 1))
        start = -1
      }
    }
  })
  return groups.length ? groups : [inner]
}

function createPastureFocusFeatures(payload = {}) {
  const format = new GeoJSON()
  return [
    ...normalizePastureGrasslandRows(payload?.grasslands),
    ...normalizePastureGrasslandRows(payload?.wkt),
  ]
    .map((row, index) => {
      const geometry = normalizePastureGrasslandGeometry(row)
      if (!geometry) return null
      const properties = normalizePastureGrasslandProperties(row)
      const feature = format.readFeature(
        {
          type: "Feature",
          properties: {
            ...properties,
            id: properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`,
            pasture_name: payload?.name || properties.pastureName || properties.name,
          },
          geometry,
        },
        {
          dataProjection: "EPSG:4326",
          featureProjection: "EPSG:3857",
        },
      )
      feature.setId(properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`)
      return feature
    })
    .filter(Boolean)
}

function focusPasture(payload = {}) {
  if (!olMap) return false
  const features = createPastureFocusFeatures(payload)
  if (!features.length) {
    emitLayerStatus("warning", "当前牧户暂无可定位数据")
    return false
  }
  if (!pastureFocusSource) {
    olMap.addLayer(createPastureFocusLayer())
  }
  pastureFocusSource.clear(true)
  pastureFocusSource.addFeatures(features)
  const extent = pastureFocusSource.getExtent()
  if (!extent || extent.some((value) => !Number.isFinite(value))) {
    emitLayerStatus("warning", "当前牧户暂无可定位数据")
    return false
  }
  const size = olMap.getSize?.() || [1280, 720]
  const horizontalPadding = Math.min(360, Math.max(96, Number(size[0] || 0) * 0.24))
  olMap.getView()?.fit(extent, {
    duration: 700,
    maxZoom: 17,
    padding: [96, horizontalPadding, 96, horizontalPadding],
  })
  emitLayerStatus("success", "已定位具体草场")
  window.setTimeout(() => olMap?.updateSize?.(), 0)
  return true
}

function createTownLabelStyle(feature, resolution) {
  const name = feature.get("name") || ""
  const compact = resolution > 150
  const hidden = resolution > 420
  if (hidden) return labelPointStyle
  const fontSize = compact ? 12 : 13
  const minWidthPadding = compact ? [4, 8, 4, 8] : [5, 10, 5, 10]
  return [
    new Style({
      image: new RegularShape({
        points: 3,
        radius: compact ? 4 : 5,
        rotation: Math.PI,
        displacement: [0, 14],
        fill: new Fill({ color: "rgba(126, 251, 246, 0.92)" }),
        stroke: new Stroke({ color: "rgba(4, 28, 40, 0.95)", width: 1.2 }),
      }),
    }),
    new Style({
      text: new Text({
        text: name,
        font: `800 ${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`,
        fill: new Fill({ color: "#ffffff" }),
        stroke: new Stroke({ color: "rgba(2, 18, 28, 0.96)", width: 4 }),
        backgroundFill: new Fill({ color: "rgba(3, 21, 33, 0.78)" }),
        backgroundStroke: new Stroke({ color: "rgba(126, 251, 246, 0.36)", width: 1 }),
        padding: minWidthPadding,
        offsetY: -17,
      }),
    }),
  ]
}

function formatCoordinate(value) {
  const number = Number(value)
  if (!Number.isFinite(number)) return "0.000000"
  return number.toFixed(6)
}

async function copyTextToClipboard(text) {
  if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text)
      return
    } catch (error) {
      // Fall back to textarea copy below. Some browsers do not allow clipboard writes from contextmenu.
    }
  }
  const textarea = document.createElement("textarea")
  textarea.value = text
  textarea.setAttribute("readonly", "readonly")
  textarea.style.position = "fixed"
  textarea.style.left = "-9999px"
  textarea.style.top = "0"
  textarea.style.opacity = "0"
  try {
    document.body.appendChild(textarea)
    textarea.focus({ preventScroll: true })
    textarea.select()
    textarea.setSelectionRange(0, text.length)
    const success = document.execCommand("copy")
    if (!success) throw new Error("copy failed")
  } finally {
    document.body.removeChild(textarea)
  }
}

function showCoordinateCopyTip(event, text, error = false) {
  if (coordinateTipTimer) window.clearTimeout(coordinateTipTimer)
  coordinateCopyMenu.value = {
    ...coordinateCopyMenu.value,
    visible: false,
  }
  const tipWidth = 270
  const tipHeight = 32
  const viewportWidth = window.innerWidth || 1920
  const viewportHeight = window.innerHeight || 1080
  coordinateCopyTip.value = {
    visible: true,
    text,
    x: Math.min(Math.max(12, event.clientX + 12), Math.max(12, viewportWidth - tipWidth)),
    y: Math.min(Math.max(12, event.clientY + 12), Math.max(12, viewportHeight - tipHeight)),
    error,
  }
  coordinateTipTimer = window.setTimeout(() => {
    coordinateCopyTip.value = {
      ...coordinateCopyTip.value,
      visible: false,
    }
    coordinateTipTimer = null
  }, 1500)
}

function showCoordinateCopyMenu(event, text, error = false) {
  if (coordinateTipTimer) {
    window.clearTimeout(coordinateTipTimer)
    coordinateTipTimer = null
  }
  const menuWidth = 300
  const menuHeight = 38
  const viewportWidth = window.innerWidth || 1920
  const viewportHeight = window.innerHeight || 1080
  coordinateCopyTip.value = {
    ...coordinateCopyTip.value,
    visible: false,
  }
  coordinateCopyMenu.value = {
    visible: true,
    text,
    x: Math.min(Math.max(12, event.clientX + 12), Math.max(12, viewportWidth - menuWidth)),
    y: Math.min(Math.max(12, event.clientY + 12), Math.max(12, viewportHeight - menuHeight)),
    error,
  }
}

function hideCoordinateCopyMenu() {
  coordinateCopyMenu.value = {
    ...coordinateCopyMenu.value,
    visible: false,
  }
}

async function copyCoordinateFromMapEvent(event) {
  if (!olMap) return
  const pixel = olMap.getEventPixel(event)
  const coordinate = olMap.getCoordinateFromPixel(pixel)
  if (!coordinate) return
  const [lng, lat] = toLonLat(coordinate)
  const text = `${formatCoordinate(lng)}, ${formatCoordinate(lat)}`
  try {
    await copyTextToClipboard(text)
    showCoordinateCopyTip(event, `已复制 ${text}`)
  } catch (error) {
    showCoordinateCopyMenu(event, text, true)
  }
}

function handleRightPointerDown(event) {
  if (event.button !== 2) {
    hideCoordinateCopyMenu()
    return
  }
  event.preventDefault()
  event.stopPropagation()
  lastRightClickCopyTime = Date.now()
  copyCoordinateFromMapEvent(event)
}

async function handleCoordinateMenuCopy() {
  const text = coordinateCopyMenu.value.text
  if (!text) return
  try {
    await copyTextToClipboard(text)
    coordinateCopyMenu.value = {
      ...coordinateCopyMenu.value,
      visible: false,
      error: false,
    }
    coordinateCopyTip.value = {
      visible: true,
      text: `已复制 ${text}`,
      x: coordinateCopyMenu.value.x,
      y: coordinateCopyMenu.value.y,
      error: false,
    }
    if (coordinateTipTimer) window.clearTimeout(coordinateTipTimer)
    coordinateTipTimer = window.setTimeout(() => {
      coordinateCopyTip.value = {
        ...coordinateCopyTip.value,
        visible: false,
      }
      coordinateTipTimer = null
    }, 1500)
  } catch (error) {
    coordinateCopyMenu.value = {
      ...coordinateCopyMenu.value,
      error: true,
    }
  }
}

function handleContextMenu(event) {
  event.preventDefault()
  event.stopPropagation()
  if (Date.now() - lastRightClickCopyTime < 800) return
  lastRightClickCopyTime = Date.now()
  copyCoordinateFromMapEvent(event)
}

function handleMapSingleClick(event) {
  if (!olMap || !event?.coordinate) return
  hideCoordinateCopyMenu()
  const [lng, lat] = toLonLat(event.coordinate)
  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
  const sourceEvent = event.originalEvent || {}
  emit("map-click", {
    lngLat: { lng, lat },
    coordinate: event.coordinate,
    pixel: event.pixel,
    screen: {
      x: Number(sourceEvent.clientX),
      y: Number(sourceEvent.clientY),
    },
    overlayLayer: props.overlayLayer,
  })
}

function bindContextMenuCopy() {
  const viewport = olMap?.getViewport?.()
  if (!viewport || contextMenuHandler) return
  rightPointerDownHandler = (event) => {
    handleRightPointerDown(event)
  }
  contextMenuHandler = (event) => {
    handleContextMenu(event)
  }
  viewport.addEventListener("pointerdown", rightPointerDownHandler)
  viewport.addEventListener("contextmenu", contextMenuHandler)
}

function unbindContextMenuCopy() {
  const viewport = olMap?.getViewport?.()
  if (viewport && rightPointerDownHandler) {
    viewport.removeEventListener("pointerdown", rightPointerDownHandler)
  }
  if (viewport && contextMenuHandler) {
    viewport.removeEventListener("contextmenu", contextMenuHandler)
  }
  rightPointerDownHandler = null
  contextMenuHandler = null
  if (coordinateTipTimer) {
    window.clearTimeout(coordinateTipTimer)
    coordinateTipTimer = null
  }
  hideCoordinateCopyMenu()
}

async function initMap() {
  if (olMap) return
  await nextTick()
  if (!mapEl.value || olMap) return

  const initialZoom = normalizeInitialZoom()
  resetTileState(initialZoom)
  const view = new View({
    center: fromLonLat(normalizeCenter()),
    zoom: initialZoom,
    minZoom: props.minZoom,
    maxZoom: props.maxZoom,
    enableRotation: false,
    constrainResolution: false,
    smoothResolutionConstraint: true,
  })

  olMap = new Map({
    target: mapEl.value,
    controls: defaultControls({
      attribution: false,
      rotate: false,
      zoom: false,
    }),
    layers: [
      new TileLayer({
        source: buildBaseSource(),
        preload: 1,
        zIndex: 1,
      }),
      ...createBoundaryLayers(),
      createPastureFocusLayer(),
    ],
    view,
  })
  refreshOverlayLayer()
  bindContextMenuCopy()

  eventKeys.push(
    olMap.on("singleclick", handleMapSingleClick),
  )

  eventKeys.push(
    view.on("change:resolution", () => {
      const zoom = currentZoom()
      if (zoom !== tileState.zoom) {
        resetTileState(zoom)
        emitTileStatus("loading", `正在加载 hy-result ${zoom}级影像底图`)
      }
    }),
  )

  window.setTimeout(() => {
    olMap?.updateSize?.()
    emit("ready")
  }, 0)
}

function disposeMap() {
  unbindContextMenuCopy()
  if (eventKeys.length) {
    unByKey(eventKeys)
    eventKeys = []
  }
  if (wmsEventKeys.length) {
    unByKey(wmsEventKeys)
    wmsEventKeys = []
  }
  pendingTiles.clear()
  baseSource = null
  overlaySource = null
  overlayMapLayer = null
  pastureFocusSource = null
  pastureFocusLayer = null
  if (olMap) {
    olMap.setTarget(null)
    olMap.dispose?.()
    olMap = null
  }
}

function refreshView() {
  locateMap()
  olMap?.updateSize?.()
  window.setTimeout(() => olMap?.updateSize?.(), 120)
}

function refreshTileSources() {
  if (baseSource) {
    resetTileState(currentZoom())
    baseSource.refresh?.()
    emitTileStatus("loading", `正在刷新 hy-result ${tileState.zoom}级影像底图`)
  }
  overlaySource?.refresh?.()
}

function resetView() {
  locateMap()
  refreshTileSources()
  olMap?.updateSize?.()
  window.setTimeout(() => olMap?.updateSize?.(), 80)
  window.setTimeout(() => olMap?.updateSize?.(), 220)
}

function hasMap() {
  return Boolean(olMap)
}

defineExpose({
  refreshView,
  resetView,
  hasMap,
  focusPasture,
})

onMounted(() => {
  initMap()
})

onBeforeUnmount(() => {
  disposeMap()
})

watch(
  () => props.active,
  (active) => {
    if (active) {
      initMap()
      window.setTimeout(() => {
        locateMap()
        olMap?.updateSize?.()
      }, 0)
    }
  },
)

watch(
  () => props.center,
  () => {
    locateMap()
  },
  { deep: true },
)

watch(
  () => props.tileUrlTemplate,
  () => {
    if (!baseSource) return
    resetTileState(currentZoom())
    baseSource.setUrl(props.tileUrlTemplate)
    baseSource.refresh()
    emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`)
  },
)

watch(
  () => props.overlayLayer,
  () => {
    refreshOverlayLayer()
  },
  { deep: true },
)
</script>

<style scoped lang="scss">
.yak-ol-map {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  background: #03121c;
}

.yak-ol-map__canvas {
  width: 100%;
  height: 100%;
}

.yak-ol-map__copy-tip {
  position: fixed;
  z-index: 30;
  max-width: 260px;
  padding: 7px 10px;
  border: 1px solid rgba(126, 251, 246, 0.4);
  border-radius: 3px;
  color: rgba(235, 255, 255, 0.94);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  background: rgba(3, 21, 33, 0.86);
  box-shadow: 0 0 14px rgba(48, 220, 255, 0.16);
  pointer-events: none;
  transform: translateY(-50%);
  white-space: nowrap;

  &.is-error {
    border-color: rgba(255, 143, 107, 0.55);
    color: #ffe0d6;
  }
}

.yak-ol-map__copy-menu {
  position: fixed;
  z-index: 30;
  display: flex;
  height: 34px;
  align-items: center;
  gap: 8px;
  padding: 0 8px 0 10px;
  border: 1px solid rgba(126, 251, 246, 0.42);
  border-radius: 3px;
  color: rgba(235, 255, 255, 0.94);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  background: rgba(3, 21, 33, 0.92);
  box-shadow: 0 0 14px rgba(48, 220, 255, 0.18);
  pointer-events: all;
  white-space: nowrap;

  span {
    font-variant-numeric: tabular-nums;
  }

  button {
    height: 22px;
    padding: 0 8px;
    border: 1px solid rgba(126, 251, 246, 0.38);
    border-radius: 2px;
    color: #031822;
    font-size: 12px;
    font-weight: 800;
    background: linear-gradient(180deg, #7efbf6, #30dcff);
    cursor: pointer;
  }

  &.is-error {
    border-color: rgba(255, 215, 108, 0.48);
  }
}

:deep(.ol-viewport) {
  background: #03121c;
}
</style>
