const STORAGE_KEY = 'cmr-tile-disk-cache-index-v1'
const ROOT_DIR = `${wx.env.USER_DATA_PATH}/cmr-tile-cache`
const MAX_PERSISTED_TILES = 600
const PERSIST_DELAY_MS = 300

export interface TilePersistentCacheRecord {
  filePath: string
  lastAccessedAt: number
}

type TilePersistentCacheIndex = Record<string, TilePersistentCacheRecord>

let sharedTilePersistentCache: TilePersistentCache | null = null

function getFileExtension(url: string): string {
  const matched = url.match(/\.(png|jpg|jpeg|webp)(?:$|\?)/i)
  if (!matched) {
    return '.tile'
  }

  return `.${matched[1].toLowerCase()}`
}

function hashUrl(url: string): string {
  let hash = 2166136261
  for (let index = 0; index < url.length; index += 1) {
    hash ^= url.charCodeAt(index)
    hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
  }

  return (hash >>> 0).toString(36)
}

function cloneIndex(rawIndex: any): TilePersistentCacheIndex {
  const nextIndex: TilePersistentCacheIndex = {}
  if (!rawIndex || typeof rawIndex !== 'object') {
    return nextIndex
  }

  Object.keys(rawIndex).forEach((url) => {
    const record = rawIndex[url]
    if (!record || typeof record.filePath !== 'string') {
      return
    }

    nextIndex[url] = {
      filePath: record.filePath,
      lastAccessedAt: typeof record.lastAccessedAt === 'number' ? record.lastAccessedAt : 0,
    }
  })

  return nextIndex
}

export class TilePersistentCache {
  fs: WechatMiniprogram.FileSystemManager
  rootDir: string
  index: TilePersistentCacheIndex
  persistTimer: number

  constructor() {
    this.fs = wx.getFileSystemManager()
    this.rootDir = ROOT_DIR
    this.index = {}
    this.persistTimer = 0

    this.ensureRootDir()
    this.loadIndex()
    this.pruneMissingFiles()
    this.pruneIfNeeded()
  }

  ensureRootDir(): void {
    try {
      this.fs.accessSync(this.rootDir)
    } catch {
      this.fs.mkdirSync(this.rootDir, true)
    }
  }

  loadIndex(): void {
    try {
      this.index = cloneIndex(wx.getStorageSync(STORAGE_KEY))
    } catch {
      this.index = {}
    }
  }

  getCount(): number {
    return Object.keys(this.index).length
  }

  schedulePersist(): void {
    if (this.persistTimer) {
      return
    }

    this.persistTimer = setTimeout(() => {
      this.persistTimer = 0
      wx.setStorageSync(STORAGE_KEY, this.index)
    }, PERSIST_DELAY_MS) as unknown as number
  }

  pruneMissingFiles(): void {
    let changed = false

    Object.keys(this.index).forEach((url) => {
      const record = this.index[url]
      try {
        this.fs.accessSync(record.filePath)
      } catch {
        delete this.index[url]
        changed = true
      }
    })

    if (changed) {
      this.schedulePersist()
    }
  }

  getCachedPath(url: string): string | null {
    const record = this.index[url]
    if (!record) {
      return null
    }

    try {
      this.fs.accessSync(record.filePath)
      record.lastAccessedAt = Date.now()
      this.schedulePersist()
      return record.filePath
    } catch {
      delete this.index[url]
      this.schedulePersist()
      return null
    }
  }

  getTargetPath(url: string): string {
    const existingRecord = this.index[url]
    if (existingRecord) {
      existingRecord.lastAccessedAt = Date.now()
      this.schedulePersist()
      return existingRecord.filePath
    }

    const filePath = `${this.rootDir}/${hashUrl(url)}${getFileExtension(url)}`
    this.index[url] = {
      filePath,
      lastAccessedAt: Date.now(),
    }
    this.schedulePersist()
    return filePath
  }

  markReady(url: string, filePath: string): void {
    this.index[url] = {
      filePath,
      lastAccessedAt: Date.now(),
    }
    this.pruneIfNeeded()
    this.schedulePersist()
  }

  remove(url: string): void {
    const record = this.index[url]
    if (!record) {
      return
    }

    try {
      this.fs.unlinkSync(record.filePath)
    } catch {
      // Ignore unlink errors for already-missing files.
    }

    delete this.index[url]
    this.schedulePersist()
  }

  pruneIfNeeded(): void {
    const urls = Object.keys(this.index)
    if (urls.length <= MAX_PERSISTED_TILES) {
      return
    }

    const removableUrls = urls.sort((leftUrl, rightUrl) => {
      return this.index[leftUrl].lastAccessedAt - this.index[rightUrl].lastAccessedAt
    })

    while (removableUrls.length > MAX_PERSISTED_TILES) {
      const nextUrl = removableUrls.shift() as string
      const record = this.index[nextUrl]
      if (record) {
        try {
          this.fs.unlinkSync(record.filePath)
        } catch {
          // Ignore unlink errors for already-missing files.
        }
      }
      delete this.index[nextUrl]
    }
  }
}

export function getTilePersistentCache(): TilePersistentCache {
  if (!sharedTilePersistentCache) {
    sharedTilePersistentCache = new TilePersistentCache()
  }

  return sharedTilePersistentCache
}
