import {
  BufferAttribute,
  CanvasTexture,
  ClampToEdgeWrapping,
  LinearFilter,
  SRGBColorSpace,
} from "three"
import { geoMercator } from "d3-geo"

const TILE_SIZE = 256
const DEFAULT_SATELLITE_TILE_URL = "https://map.shuxitech.com/vt/lyrs=s&hl=zh-CN&x={x}&y={y}&z={z}"
const TILE_IMAGE_CACHE_LIMIT = 900
const TILE_LOAD_CONCURRENCY = 10
const tileImageCache = new Map()

const HONGYUAN_BOUNDS = {
  west: 101.852492,
  south: 31.851389,
  east: 103.347518,
  north: 33.319796,
}

function lngLatToTile(lng, lat, zoom) {
  const scale = 2 ** zoom
  const sinLat = Math.sin((lat * Math.PI) / 180)
  return {
    x: ((lng + 180) / 360) * scale,
    y: (0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)) * scale,
  }
}

function tileToLngLat(x, y, zoom) {
  const scale = 2 ** zoom
  const lng = (x / scale) * 360 - 180
  const n = Math.PI - (2 * Math.PI * y) / scale
  const lat = (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
  return [lng, lat]
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value))
}

function getTileRange(bounds, zoom, { centerLngLat, maxCols, maxRows } = {}) {
  const northWest = lngLatToTile(bounds.west, bounds.north, zoom)
  const southEast = lngLatToTile(bounds.east, bounds.south, zoom)
  const fullMinX = Math.floor(northWest.x)
  const fullMaxX = Math.floor(southEast.x)
  const fullMinY = Math.floor(northWest.y)
  const fullMaxY = Math.floor(southEast.y)
  let minX = fullMinX
  let maxX = fullMaxX
  let minY = fullMinY
  let maxY = fullMaxY
  let centerX = Math.floor((fullMinX + fullMaxX) / 2)
  let centerY = Math.floor((fullMinY + fullMaxY) / 2)

  if (centerLngLat && Number.isFinite(maxCols) && Number.isFinite(maxRows)) {
    const centerLng = clamp(Number(centerLngLat[0]), bounds.west, bounds.east)
    const centerLat = clamp(Number(centerLngLat[1]), bounds.south, bounds.north)
    const centerTile = lngLatToTile(centerLng, centerLat, zoom)
    centerX = Math.floor(centerTile.x)
    centerY = Math.floor(centerTile.y)
    const cols = Math.max(1, Math.min(Math.floor(maxCols), fullMaxX - fullMinX + 1))
    const rows = Math.max(1, Math.min(Math.floor(maxRows), fullMaxY - fullMinY + 1))
    minX = clamp(centerX - Math.floor(cols / 2), fullMinX, fullMaxX - cols + 1)
    minY = clamp(centerY - Math.floor(rows / 2), fullMinY, fullMaxY - rows + 1)
    maxX = minX + cols - 1
    maxY = minY + rows - 1
  }

  return {
    zoom,
    minX,
    maxX,
    minY,
    maxY,
    centerX,
    centerY,
    cols: maxX - minX + 1,
    rows: maxY - minY + 1,
  }
}

export function getHongyuanSatelliteTileRange({
  zoom = 11,
  centerLngLat,
  maxCols,
  maxRows,
  bounds = HONGYUAN_BOUNDS,
} = {}) {
  return getTileRange(bounds, zoom, { centerLngLat, maxCols, maxRows })
}

function createProjectedBounds(tileRange, projection) {
  const [west, north] = tileToLngLat(tileRange.minX, tileRange.minY, tileRange.zoom)
  const [east, south] = tileToLngLat(tileRange.maxX + 1, tileRange.maxY + 1, tileRange.zoom)
  const [left] = projection([west, HONGYUAN_BOUNDS.south])
  const [right] = projection([east, HONGYUAN_BOUNDS.south])
  const [, projectedNorth] = projection([HONGYUAN_BOUNDS.west, north])
  const [, projectedSouth] = projection([HONGYUAN_BOUNDS.west, south])

  return {
    left,
    right,
    bottom: -projectedSouth,
    top: -projectedNorth,
  }
}

function drawSatelliteTone(ctx, canvas) {
  const { width, height } = canvas
  ctx.save()
  ctx.globalCompositeOperation = "multiply"
  ctx.fillStyle = "rgba(5, 24, 34, 0.22)"
  ctx.fillRect(0, 0, width, height)
  ctx.globalCompositeOperation = "screen"
  const radial = ctx.createRadialGradient(
    width * 0.5,
    height * 0.48,
    width * 0.1,
    width * 0.5,
    height * 0.5,
    width * 0.72,
  )
  radial.addColorStop(0, "rgba(80, 230, 255, 0.16)")
  radial.addColorStop(1, "rgba(0, 18, 36, 0.03)")
  ctx.fillStyle = radial
  ctx.fillRect(0, 0, width, height)
  ctx.restore()
}

function drawPlaceholder(ctx, canvas) {
  const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
  gradient.addColorStop(0, "#061927")
  gradient.addColorStop(0.5, "#0b2c3c")
  gradient.addColorStop(1, "#07131f")
  ctx.fillStyle = gradient
  ctx.fillRect(0, 0, canvas.width, canvas.height)
}

function loadImage(url) {
  const cached = tileImageCache.get(url)
  if (cached) return cached
  return new Promise((resolve) => {
    const image = new Image()
    image.crossOrigin = "anonymous"
    image.onload = () => resolve(image)
    image.onerror = () => {
      tileImageCache.delete(url)
      resolve(null)
    }
    image.src = url
  })
}

function loadCachedImage(url) {
  const cached = tileImageCache.get(url)
  if (cached) return cached
  const request = loadImage(url)
  tileImageCache.set(url, request)
  while (tileImageCache.size > TILE_IMAGE_CACHE_LIMIT) {
    const oldestKey = tileImageCache.keys().next().value
    if (!oldestKey) break
    tileImageCache.delete(oldestKey)
  }
  return request
}

function tileUrl(x, y, z, template = DEFAULT_SATELLITE_TILE_URL) {
  return String(template || DEFAULT_SATELLITE_TILE_URL)
    .replace(/\{x\}/g, x)
    .replace(/\{y\}/g, y)
    .replace(/\{z\}/g, z)
}

async function loadTiles(ctx, canvas, texture, tileRange, { tileUrlTemplate, onTileStatus, applyTone = true } = {}) {
  const tasks = []
  const total = tileRange.cols * tileRange.rows
  let loaded = 0
  let failed = 0
  onTileStatus?.({
    type: "loading",
    text: `正在加载${tileRange.zoom}级影像底图`,
    loaded,
    failed,
    total,
    zoom: tileRange.zoom,
    tileRange,
    tileUrlTemplate,
  })
  ctx.save()
  ctx.filter = "brightness(0.92) contrast(1.22) saturate(1.06)"
  for (let x = tileRange.minX; x <= tileRange.maxX; x += 1) {
    for (let y = tileRange.minY; y <= tileRange.maxY; y += 1) {
      tasks.push({ x, y })
    }
  }
  tasks.sort((a, b) => (
    Math.abs(a.x - tileRange.centerX) + Math.abs(a.y - tileRange.centerY)
  ) - (
    Math.abs(b.x - tileRange.centerX) + Math.abs(b.y - tileRange.centerY)
  ))
  let taskIndex = 0
  async function worker() {
    while (taskIndex < tasks.length) {
      const task = tasks[taskIndex]
      taskIndex += 1
      const image = await loadCachedImage(tileUrl(task.x, task.y, tileRange.zoom, tileUrlTemplate))
      if (!image) {
        failed += 1
      } else {
        loaded += 1
        ctx.drawImage(image, (task.x - tileRange.minX) * TILE_SIZE, (task.y - tileRange.minY) * TILE_SIZE, TILE_SIZE, TILE_SIZE)
        texture.needsUpdate = true
      }
      onTileStatus?.({
        type: "loading",
        text: `正在加载${tileRange.zoom}级影像底图`,
        loaded,
        failed,
        total,
        zoom: tileRange.zoom,
        tileRange,
        tileUrlTemplate,
      })
    }
  }
  await Promise.all(Array.from({ length: Math.min(TILE_LOAD_CONCURRENCY, tasks.length) }, worker))
  ctx.restore()
  if (applyTone) drawSatelliteTone(ctx, canvas)
  texture.needsUpdate = true
  const coverageRatio = total > 0 ? loaded / total : 0
  const statusType = loaded === 0 ? "error" : coverageRatio >= 0.75 ? "success" : "warning"
  const statusText = loaded === total
    ? `${tileRange.zoom}级影像底图已加载`
    : loaded > 0
      ? `${tileRange.zoom}级影像底图已加载（${loaded}/${total}张有效瓦片）`
      : `${tileRange.zoom}级影像底图加载失败`
  onTileStatus?.({
    type: statusType,
    text: statusText,
    loaded,
    failed,
    total,
    zoom: tileRange.zoom,
    tileRange,
    tileUrlTemplate,
  })
}

export function createHongyuanSatelliteTexture({
  geoProjectionCenter,
  geoProjectionScale,
  zoom = 11,
  tileUrlTemplate = DEFAULT_SATELLITE_TILE_URL,
  onTileStatus,
  centerLngLat,
  maxCols,
  maxRows,
  transparentBackground = false,
  applyTone = true,
} = {}) {
  const tileRange = getTileRange(HONGYUAN_BOUNDS, zoom, { centerLngLat, maxCols, maxRows })
  const canvas = document.createElement("canvas")
  canvas.width = tileRange.cols * TILE_SIZE
  canvas.height = tileRange.rows * TILE_SIZE
  const ctx = canvas.getContext("2d")
  if (transparentBackground) {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
  } else {
    drawPlaceholder(ctx, canvas)
  }

  const texture = new CanvasTexture(canvas)
  texture.colorSpace = SRGBColorSpace
  texture.wrapS = ClampToEdgeWrapping
  texture.wrapT = ClampToEdgeWrapping
  texture.minFilter = LinearFilter
  texture.magFilter = LinearFilter
  texture.needsUpdate = true

  const projection = geoMercator()
    .center(geoProjectionCenter)
    .scale(geoProjectionScale)
    .translate([0, 0])
  const uvBounds = createProjectedBounds(tileRange, projection)

  loadTiles(ctx, canvas, texture, tileRange, { tileUrlTemplate, onTileStatus, applyTone }).catch((error) => {
    console.warn("load satellite tiles failed:", error)
    onTileStatus?.({
      type: "error",
      text: `${tileRange.zoom}级影像底图加载失败`,
      loaded: 0,
      failed: tileRange.cols * tileRange.rows,
      total: tileRange.cols * tileRange.rows,
      zoom: tileRange.zoom,
      tileRange,
      tileUrlTemplate,
    })
  })

  return {
    texture,
    uvBounds,
    tileRange,
  }
}

export function applyMapTextureUv(map, bounds) {
  const width = bounds.right - bounds.left
  const height = bounds.top - bounds.bottom
  map.mapGroup.traverse((child) => {
    const position = child.geometry?.attributes?.position
    if (!child.isMesh || !position) return
    const uv = new Float32Array(position.count * 2)
    for (let index = 0; index < position.count; index += 1) {
      const x = position.getX(index)
      const y = position.getY(index)
      uv[index * 2] = (x - bounds.left) / width
      uv[index * 2 + 1] = (y - bounds.bottom) / height
    }
    child.geometry.setAttribute("uv", new BufferAttribute(uv, 2))
    child.geometry.attributes.uv.needsUpdate = true
  })
}
