import { buildTileUrl, type TileItem } from '../../utils/tile'
import { isTileWithinBounds, type TileZoomBounds } from '../../utils/remoteMapConfig'
import { getTilePersistentCache, type TilePersistentCache } from '../renderer/tilePersistentCache'

const MAX_PARENT_FALLBACK_DEPTH = 2
const MAX_CHILD_FALLBACK_DEPTH = 1
const MAX_CONCURRENT_DOWNLOADS = 6
const MAX_MEMORY_CACHE_SIZE = 240
const ERROR_RETRY_DELAY_MS = 4000

export type TileStatus = 'idle' | 'loading' | 'ready' | 'error'

export interface TileStoreEntry {
  image: any
  status: TileStatus
  sourcePath: string
  downloadTask: WechatMiniprogram.DownloadTask | null
  priority: number
  lastUsedAt: number
  lastAttemptAt: number
  lastVisibleKey: string
  retryable: boolean
}

export interface TileStoreStats {
  visibleTileCount: number
  readyTileCount: number
  memoryTileCount: number
  diskTileCount: number
  memoryHitCount: number
  diskHitCount: number
  networkFetchCount: number
}

export interface ParentFallbackTileSource {
  entry: TileStoreEntry
  zoom: number
}

export interface ChildFallbackTileSource {
  division: number
  children: Array<{
    entry: TileStoreEntry
    offsetX: number
    offsetY: number
  }>
}

export interface TileStoreScene {
  tileSource: string
  zoom: number
  viewportWidth: number
  viewportHeight: number
  translateX: number
  translateY: number
  tileBoundsByZoom: Record<number, TileZoomBounds> | null
}

export interface TileStoreCallbacks {
  onTileReady?: () => void
  onTileError?: (message: string) => void
}

function positiveModulo(value: number, divisor: number): number {
  return ((value % divisor) + divisor) % divisor
}

function getTilePriority(tile: TileItem, scene: TileStoreScene): number {
  const viewportCenterX = scene.viewportWidth / 2 + scene.translateX
  const viewportCenterY = scene.viewportHeight / 2 + scene.translateY
  const tileCenterX = tile.leftPx + tile.sizePx / 2
  const tileCenterY = tile.topPx + tile.sizePx / 2
  const deltaX = tileCenterX - viewportCenterX
  const deltaY = tileCenterY - viewportCenterY

  return deltaX * deltaX + deltaY * deltaY
}

function bindImageLoad(
  image: any,
  src: string,
  onReady: () => void,
  onError: () => void,
): void {
  image.onload = onReady
  image.onerror = onError
  image.src = src
}

export class TileStore {
  canvas: any
  diskCache: TilePersistentCache
  tileCache: Map<string, TileStoreEntry>
  pendingUrls: string[]
  pendingSet: Set<string>
  pendingQueueDirty: boolean
  activeDownloadCount: number
  destroyed: boolean
  memoryHitCount: number
  diskHitCount: number
  networkFetchCount: number
  onTileReady?: () => void
  onTileError?: (message: string) => void

  constructor(callbacks?: TileStoreCallbacks) {
    this.canvas = null
    this.diskCache = getTilePersistentCache()
    this.tileCache = new Map<string, TileStoreEntry>()
    this.pendingUrls = []
    this.pendingSet = new Set<string>()
    this.pendingQueueDirty = false
    this.activeDownloadCount = 0
    this.destroyed = false
    this.memoryHitCount = 0
    this.diskHitCount = 0
    this.networkFetchCount = 0
    this.onTileReady = callbacks && callbacks.onTileReady ? callbacks.onTileReady : undefined
    this.onTileError = callbacks && callbacks.onTileError ? callbacks.onTileError : undefined
  }

  attachCanvas(canvas: any): void {
    this.canvas = canvas
  }

  destroy(): void {
    this.destroyed = true
    this.tileCache.forEach((entry) => {
      if (entry.downloadTask) {
        entry.downloadTask.abort()
      }
    })
    this.pendingUrls = []
    this.pendingSet.clear()
    this.pendingQueueDirty = false
    this.activeDownloadCount = 0
    this.tileCache.clear()
    this.canvas = null
  }

  getReadyMemoryTileCount(): number {
    let count = 0
    this.tileCache.forEach((entry) => {
      if (entry.status === 'ready' && entry.image) {
        count += 1
      }
    })
    return count
  }

  getStats(visibleTileCount: number, readyTileCount: number): TileStoreStats {
    return {
      visibleTileCount,
      readyTileCount,
      memoryTileCount: this.getReadyMemoryTileCount(),
      diskTileCount: this.diskCache.getCount(),
      memoryHitCount: this.memoryHitCount,
      diskHitCount: this.diskHitCount,
      networkFetchCount: this.networkFetchCount,
    }
  }

  getEntry(url: string): TileStoreEntry | undefined {
    return this.tileCache.get(url)
  }

  touchTile(url: string, priority: number, usedAt: number): TileStoreEntry {
    let entry = this.tileCache.get(url)
    if (!entry) {
      entry = {
        image: null,
        status: 'idle',
        sourcePath: '',
        downloadTask: null,
        priority,
        lastUsedAt: usedAt,
        lastAttemptAt: 0,
        lastVisibleKey: '',
        retryable: true,
      }
      this.tileCache.set(url, entry)
      return entry
    }

    if (entry.priority !== priority && this.pendingSet.has(url)) {
      this.pendingQueueDirty = true
    }

    entry.priority = priority
    entry.lastUsedAt = usedAt
    return entry
  }

  trimPendingQueue(protectedUrls: Set<string>): void {
    const nextPendingUrls: string[] = []
    for (const url of this.pendingUrls) {
      if (!protectedUrls.has(url)) {
        continue
      }
      nextPendingUrls.push(url)
    }

    this.pendingUrls = nextPendingUrls
    this.pendingSet = new Set<string>(nextPendingUrls)
    this.pendingQueueDirty = true
  }

  queueTile(url: string): void {
    if (this.pendingSet.has(url)) {
      return
    }

    const entry = this.tileCache.get(url)
    if (!entry || entry.status === 'loading' || entry.status === 'ready') {
      return
    }

    this.pendingSet.add(url)
    this.pendingUrls.push(url)
    this.pendingQueueDirty = true
  }

  queueVisibleTiles(tiles: TileItem[], scene: TileStoreScene, visibleKey: string): void {
    const usedAt = Date.now()
    const protectedUrls = new Set<string>()
    const parentPriorityMap = new Map<string, number>()
    const countedMemoryHits = new Set<string>()

    for (const tile of tiles) {
      const priority = getTilePriority(tile, scene)
      const entry = this.touchTile(tile.url, priority, usedAt)
      protectedUrls.add(tile.url)

      if (entry.status === 'ready' && entry.lastVisibleKey !== visibleKey && !countedMemoryHits.has(tile.url)) {
        this.memoryHitCount += 1
        entry.lastVisibleKey = visibleKey
        countedMemoryHits.add(tile.url)
      }

      for (let depth = 1; depth <= MAX_PARENT_FALLBACK_DEPTH; depth += 1) {
        const fallbackZoom = scene.zoom - depth
        if (fallbackZoom < 0) {
          break
        }

        const scale = Math.pow(2, depth)
        const fallbackX = Math.floor(tile.x / scale)
        const fallbackY = Math.floor(tile.y / scale)
        if (!isTileWithinBounds(scene.tileBoundsByZoom, fallbackZoom, fallbackX, fallbackY)) {
          continue
        }
        const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY)
        const fallbackPriority = priority / (depth + 1)
        const existingPriority = parentPriorityMap.get(fallbackUrl)

        if (typeof existingPriority !== 'number' || fallbackPriority < existingPriority) {
          parentPriorityMap.set(fallbackUrl, fallbackPriority)
        }
      }
    }

    parentPriorityMap.forEach((priority, url) => {
      const entry = this.touchTile(url, priority, usedAt)
      protectedUrls.add(url)
      if (entry.status === 'ready' && entry.lastVisibleKey !== visibleKey && !countedMemoryHits.has(url)) {
        this.memoryHitCount += 1
        entry.lastVisibleKey = visibleKey
        countedMemoryHits.add(url)
      }
    })

    this.trimPendingQueue(protectedUrls)

    parentPriorityMap.forEach((_priority, url) => {
      const entry = this.tileCache.get(url)
      if (!entry) {
        return
      }

      if (entry.status === 'idle' || (entry.status === 'error' && entry.retryable && usedAt - entry.lastAttemptAt > ERROR_RETRY_DELAY_MS)) {
        if (entry.status === 'error') {
          entry.status = 'idle'
          entry.retryable = true
        }
        this.queueTile(url)
      }
    })

    for (const tile of tiles) {
      const entry = this.tileCache.get(tile.url)
      if (!entry) {
        continue
      }

      if (entry.status === 'idle' || (entry.status === 'error' && entry.retryable && usedAt - entry.lastAttemptAt > ERROR_RETRY_DELAY_MS)) {
        if (entry.status === 'error') {
          entry.status = 'idle'
          entry.retryable = true
        }
        this.queueTile(tile.url)
      }
    }

    this.pruneMemoryCache(protectedUrls)
    this.pumpTileQueue()
  }

  pumpTileQueue(): void {
    if (this.destroyed || !this.canvas) {
      return
    }

    if (this.pendingQueueDirty && this.pendingUrls.length > 1) {
      this.pendingUrls.sort((leftUrl, rightUrl) => {
        const leftEntry = this.tileCache.get(leftUrl)
        const rightEntry = this.tileCache.get(rightUrl)
        const leftPriority = leftEntry ? leftEntry.priority : Number.MAX_SAFE_INTEGER
        const rightPriority = rightEntry ? rightEntry.priority : Number.MAX_SAFE_INTEGER
        return leftPriority - rightPriority
      })
      this.pendingQueueDirty = false
    }

    while (this.activeDownloadCount < MAX_CONCURRENT_DOWNLOADS && this.pendingUrls.length) {
      const url = this.pendingUrls.shift() as string
      this.pendingSet.delete(url)

      const entry = this.tileCache.get(url)
      if (!entry || entry.status === 'loading' || entry.status === 'ready') {
        continue
      }

      this.startTileDownload(url, entry)
    }
  }

  startTileDownload(url: string, entry: TileStoreEntry): void {
    if (this.destroyed || !this.canvas) {
      return
    }

    entry.status = 'loading'
    entry.lastAttemptAt = Date.now()
    this.activeDownloadCount += 1

    let finished = false
    const finish = () => {
      if (finished) {
        return
      }

      finished = true
      entry.downloadTask = null
      this.activeDownloadCount = Math.max(0, this.activeDownloadCount - 1)
      this.pumpTileQueue()
    }

    const markReady = () => {
      entry.status = 'ready'
      finish()
      if (this.onTileReady) {
        this.onTileReady()
      }
    }

    const markError = (message: string, retryable = true) => {
      entry.status = 'error'
      entry.retryable = retryable
      finish()
      if (this.onTileError) {
        this.onTileError(`${message}: ${url}`)
      }
    }

    const loadLocalImage = (localPath: string, fromPersistentCache: boolean) => {
      if (this.destroyed || !this.canvas) {
        finish()
        return
      }

      const localImage = this.canvas.createImage()
      entry.image = localImage
      entry.sourcePath = localPath

      bindImageLoad(
        localImage,
        localPath,
        () => {
          this.diskCache.markReady(url, localPath)
          markReady()
        },
        () => {
          this.diskCache.remove(url)
          if (fromPersistentCache) {
            downloadToPersistentPath()
            return
          }
          markError('瓦片本地载入失败')
        },
      )
    }

    const tryRemoteImage = () => {
      if (this.destroyed || !this.canvas) {
        finish()
        return
      }

      const remoteImage = this.canvas.createImage()
      entry.image = remoteImage
      entry.sourcePath = url

      bindImageLoad(
        remoteImage,
        url,
        markReady,
        () => markError('瓦片远程载入失败'),
      )
    }

    const downloadToPersistentPath = () => {
      this.networkFetchCount += 1
      const filePath = this.diskCache.getTargetPath(url)
      const task = wx.downloadFile({
        url,
        filePath,
        success: (res) => {
          if (this.destroyed) {
            finish()
            return
          }

          const resolvedPath = res.filePath || filePath || res.tempFilePath
          if (res.statusCode >= 400 && res.statusCode < 500) {
            markError(`瓦片资源不存在(${res.statusCode})`, false)
            return
          }

          if (res.statusCode !== 200 || !resolvedPath) {
            tryRemoteImage()
            return
          }

          loadLocalImage(resolvedPath, false)
        },
        fail: () => {
          tryRemoteImage()
        },
      })

      entry.downloadTask = task
    }

    const cachedPath = this.diskCache.getCachedPath(url)
    if (cachedPath) {
      this.diskHitCount += 1
      loadLocalImage(cachedPath, true)
      return
    }

    downloadToPersistentPath()
  }

  pruneMemoryCache(protectedUrls: Set<string>): void {
    if (this.tileCache.size <= MAX_MEMORY_CACHE_SIZE) {
      return
    }

    const removableEntries: Array<{ url: string; lastUsedAt: number; priority: number }> = []
    this.tileCache.forEach((entry, url) => {
      if (protectedUrls.has(url) || this.pendingSet.has(url) || entry.status === 'loading') {
        return
      }

      removableEntries.push({
        url,
        lastUsedAt: entry.lastUsedAt,
        priority: entry.priority,
      })
    })

    removableEntries.sort((leftEntry, rightEntry) => {
      if (leftEntry.lastUsedAt !== rightEntry.lastUsedAt) {
        return leftEntry.lastUsedAt - rightEntry.lastUsedAt
      }
      return rightEntry.priority - leftEntry.priority
    })

    while (this.tileCache.size > MAX_MEMORY_CACHE_SIZE && removableEntries.length) {
      const nextEntry = removableEntries.shift() as { url: string }
      this.tileCache.delete(nextEntry.url)
    }
  }

  findParentFallback(tile: TileItem, scene: TileStoreScene): ParentFallbackTileSource | null {
    for (let depth = 1; depth <= MAX_PARENT_FALLBACK_DEPTH; depth += 1) {
      const fallbackZoom = scene.zoom - depth
      if (fallbackZoom < 0) {
        return null
      }

      const scale = Math.pow(2, depth)
      const fallbackX = Math.floor(tile.x / scale)
      const fallbackY = Math.floor(tile.y / scale)
        if (!isTileWithinBounds(scene.tileBoundsByZoom, fallbackZoom, fallbackX, fallbackY)) {
          continue
        }
        const fallbackUrl = buildTileUrl(scene.tileSource, fallbackZoom, fallbackX, fallbackY)
      const fallbackEntry = this.tileCache.get(fallbackUrl)

      if (fallbackEntry && fallbackEntry.status === 'ready' && fallbackEntry.image) {
        return {
          entry: fallbackEntry,
          zoom: fallbackZoom,
        }
      }
    }

    return null
  }

  getParentFallbackSlice(tile: TileItem, scene: TileStoreScene): {
    entry: TileStoreEntry
    sourceX: number
    sourceY: number
    sourceWidth: number
    sourceHeight: number
  } | null {
    const fallback = this.findParentFallback(tile, scene)
    if (!fallback) {
      return null
    }

    const zoomDelta = scene.zoom - fallback.zoom
    const division = Math.pow(2, zoomDelta)
    const imageWidth = fallback.entry.image.width || 256
    const imageHeight = fallback.entry.image.height || 256
    const sourceWidth = imageWidth / division
    const sourceHeight = imageHeight / division
    const offsetX = positiveModulo(tile.x, division)
    const offsetY = positiveModulo(tile.y, division)

    return {
      entry: fallback.entry,
      sourceX: offsetX * sourceWidth,
      sourceY: offsetY * sourceHeight,
      sourceWidth,
      sourceHeight,
    }
  }

  getChildFallback(tile: TileItem, scene: TileStoreScene): ChildFallbackTileSource | null {
    for (let depth = 1; depth <= MAX_CHILD_FALLBACK_DEPTH; depth += 1) {
      const childZoom = scene.zoom + depth
      const division = Math.pow(2, depth)
      const children: ChildFallbackTileSource['children'] = []

      for (let offsetY = 0; offsetY < division; offsetY += 1) {
        for (let offsetX = 0; offsetX < division; offsetX += 1) {
          const childX = tile.x * division + offsetX
          const childY = tile.y * division + offsetY
          if (!isTileWithinBounds(scene.tileBoundsByZoom, childZoom, childX, childY)) {
            continue
          }
          const childUrl = buildTileUrl(scene.tileSource, childZoom, childX, childY)
          const childEntry = this.tileCache.get(childUrl)
          if (!childEntry || childEntry.status !== 'ready' || !childEntry.image) {
            continue
          }

          children.push({
            entry: childEntry,
            offsetX,
            offsetY,
          })
        }
      }

      if (children.length) {
        return {
          division,
          children,
        }
      }
    }

    return null
  }
}


