import { join, dirname } from 'path'
import fs from 'fs'
import JSZip from 'jszip'
import type { Viewer } from 'prismarine-viewer/viewer/lib/viewer'
import { subscribeKey } from 'valtio/utils'
import { proxy, ref } from 'valtio'
import { getVersion } from 'prismarine-viewer/viewer/lib/version'
import blocksFileNames from '../generated/blocks.json'
import type { BlockStates } from './inventoryWindows'
import { copyFilesAsync, copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
import { setLoadingScreenStatus } from './utils'
import { showNotification } from './react/NotificationProvider'

export const resourcePackState = proxy({
  resourcePackInstalled: false,
  currentTexturesDataUrl: undefined as string | undefined,
  currentTexturesBlockStates: undefined as BlockStates | undefined,
})

function nextPowerOfTwo (n) {
  if (n === 0) return 1
  n--
  n |= n >> 1
  n |= n >> 2
  n |= n >> 4
  n |= n >> 8
  n |= n >> 16
  return n + 1
}

const texturePackBasePath = '/data/resourcePacks/default'
export const uninstallTexturePack = async () => {
  await removeFileRecursiveAsync(texturePackBasePath)
  setCustomTexturePackData(undefined, undefined)
}

export const getResourcePackName = async () => {
  // temp
  try {
    return await fs.promises.readFile(join(texturePackBasePath, 'name.txt'), 'utf8')
  } catch (err) {
    return '???'
  }
}

export const fromTexturePackPath = (path) => {
  return join(texturePackBasePath, path)
}

export const updateTexturePackInstalledState = async () => {
  try {
    resourcePackState.resourcePackInstalled = await existsAsync(texturePackBasePath)
  } catch {
  }
}

export const installTexturePackFromHandle = async () => {
  await mkdirRecursive(texturePackBasePath)
  await copyFilesAsyncWithProgress('/world', texturePackBasePath)
  await completeTexturePackInstall()
}

export const installTexturePack = async (file: File | ArrayBuffer, name = file['name']) => {
  try {
    await uninstallTexturePack()
  } catch (err) {
  }
  const status = 'Installing resource pack: copying all files'
  setLoadingScreenStatus(status)
  // extract the zip and write to fs every file in it
  const zip = new JSZip()
  const zipFile = await zip.loadAsync(file)
  if (!zipFile.file('pack.mcmeta')) throw new Error('Not a resource pack: missing pack.mcmeta')
  await mkdirRecursive(texturePackBasePath)

  const allFilesArr = Object.entries(zipFile.files)
  let done = 0
  const upStatus = () => {
    setLoadingScreenStatus(`${status} ${Math.round(done / allFilesArr.length * 100)}%`)
  }
  await Promise.all(allFilesArr.map(async ([path, file]) => {
    const writePath = join(texturePackBasePath, path)
    if (path.endsWith('/')) return
    await mkdirRecursive(dirname(writePath))
    await fs.promises.writeFile(writePath, Buffer.from(await file.async('arraybuffer')))
    done++
    upStatus()
  }))
  await completeTexturePackInstall(name)
}

export const completeTexturePackInstall = async (name?: string) => {
  await fs.promises.writeFile(join(texturePackBasePath, 'name.txt'), name ?? '??', 'utf8')

  if (viewer?.world.active) {
    await genTexturePackTextures(viewer.world.version!)
  }
  setLoadingScreenStatus(undefined)
  showNotification('Texturepack installed')
  await updateTexturePackInstalledState()
}

const existsAsync = async (path) => {
  try {
    await fs.promises.stat(path)
    return true
  } catch (err) {
    return false
  }
}

type TextureResolvedData = {
  blockSize: number
  // itemsUrlContent: string
}

const arrEqual = (a: any[], b: any[]) => a.length === b.length && a.every((x) => b.includes(x))

const applyTexturePackData = async (version: string, { blockSize }: TextureResolvedData, blocksUrlContent: string) => {
  const result = await fetch(`blocksStates/${getVersion(version)}.json`)
  const blockStates: BlockStates = await result.json()
  const factor = blockSize / 16

  // this will be refactored with generateTextures refactor
  const processObj = (x) => {
    if (typeof x !== 'object' || !x) return
    if (Array.isArray(x)) {
      for (const v of x) {
        processObj(v)
      }

    } else {
      const actual = Object.keys(x)
      const needed = ['u', 'v', 'su', 'sv']

      if (!arrEqual(actual, needed)) {
        for (const v of Object.values(x)) {
          processObj(v)
        }
        return
      }
      for (const k of needed) {
        x[k] *= factor
      }
    }
  }
  processObj(blockStates)
  setCustomTexturePackData(blocksUrlContent, blockStates)
}

const setCustomTexturePackData = (blockTextures, blockStates) => {
  resourcePackState.currentTexturesBlockStates = blockStates && ref(blockStates)
  resourcePackState.currentTexturesDataUrl = blockTextures
  resourcePackState.resourcePackInstalled = blockTextures !== undefined
}

const getSizeFromImage = async (filePath: string) => {
  const probeImg = new Image()
  const file = await fs.promises.readFile(filePath, 'base64')
  probeImg.src = `data:image/png;base64,${file}`
  await new Promise((resolve, reject) => {
    probeImg.addEventListener('load', resolve)
  })
  if (probeImg.width !== probeImg.height) throw new Error(`Probe texture ${filePath} is not square`)
  return probeImg.width
}

export const genTexturePackTextures = async (version: string) => {
  setCustomTexturePackData(undefined, undefined)
  let blocksBasePath = '/data/resourcePacks/default/assets/minecraft/textures/block'
  // todo not clear why this is needed
  const blocksBasePathAlt = '/data/resourcePacks/default/assets/minecraft/textures/blocks'
  const blocksGeneratedPath = `/data/resourcePacks/default/${version}.png`
  const generatedPathData = `/data/resourcePacks/default/${version}.json`
  if (!(await existsAsync(blocksBasePath))) {
    if (await existsAsync(blocksBasePathAlt)) {
      blocksBasePath = blocksBasePathAlt
    } else {
      return
    }
  }
  if (await existsAsync(blocksGeneratedPath)) {
    // make sure we await it, so we set properties in world renderer and it won't try to load default textures
    await applyTexturePackData(version, JSON.parse(await fs.promises.readFile(generatedPathData, 'utf8')), await fs.promises.readFile(blocksGeneratedPath, 'utf8'))
    return
  }

  setLoadingScreenStatus('Generating custom textures')

  const textureFiles = blocksFileNames.indexes[version].map(k => blocksFileNames.blockNames[k])
  textureFiles.unshift('missing_texture.png')

  const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length)))
  const originalTileSize = 16

  const firstBlockFile = (await fs.promises.readdir(blocksBasePath)).find(f => f.endsWith('.png'))
  if (!firstBlockFile) {
    return
  }

  // we get the size of image from the first block file, which is not ideal but works in 99% cases
  const tileSize = Math.max(originalTileSize, await getSizeFromImage(join(blocksBasePath, firstBlockFile)))

  const imgSize = texSize * tileSize

  const MAX_CANVAS_SIZE = 16_384
  if (imgSize > MAX_CANVAS_SIZE) {
    throw new Error(`Texture pack texture resolution is too big, max size is ${MAX_CANVAS_SIZE}x${MAX_CANVAS_SIZE}`)
    // texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length / 2)))
  }
  const canvas = document.createElement('canvas')
  canvas.width = imgSize
  canvas.height = imgSize
  const src = `textures/${getVersion(version)}.png`
  const ctx = canvas.getContext('2d')!
  ctx.imageSmoothingEnabled = false
  const img = new Image()
  img.src = src
  await new Promise((resolve, reject) => {
    img.onerror = reject
    img.addEventListener('load', resolve)
  })
  for (const [i, fileName] of textureFiles.entries()) {
    const x = (i % texSize) * tileSize
    const y = Math.floor(i / texSize) * tileSize
    const xOrig = (i % texSize) * originalTileSize
    const yOrig = Math.floor(i / texSize) * originalTileSize
    let imgCustom!: HTMLImageElement
    try {
      const fileBase64 = await fs.promises.readFile(join(blocksBasePath, fileName), 'base64')
      const _imgCustom = new Image()
      // I think it can crash otherwise
      // eslint-disable-next-line no-await-in-loop
      await new Promise<void>(resolve => {
        _imgCustom.addEventListener('load', () => {
          imgCustom = _imgCustom
          resolve()
        })
        _imgCustom.onerror = () => {
          console.log('Skipping issued texture', fileName)
          resolve()
        }
        _imgCustom.src = `data:image/png;base64,${fileBase64}`
      })
    } catch {
      console.log('Skipping not found texture', fileName)
    }

    if (imgCustom) {
      ctx.drawImage(imgCustom, x, y, tileSize, tileSize)
    } else {
      // todo this involves incorrect mappings for existing textures when the size is different
      ctx.drawImage(img, xOrig, yOrig, originalTileSize, originalTileSize, x, y, tileSize, tileSize)
    }
  }
  const blockDataUrl = canvas.toDataURL('image/png')
  const newData: TextureResolvedData = {
    blockSize: tileSize,
  }
  await fs.promises.writeFile(generatedPathData, JSON.stringify(newData), 'utf8')
  await fs.promises.writeFile(blocksGeneratedPath, blockDataUrl, 'utf8')
  await applyTexturePackData(version, newData, blockDataUrl)

  // const a = document.createElement('a')
  // a.href = dataUrl
  // a.download = 'pack.png'
  // a.click()
}

export const watchTexturepackInViewer = (viewer: Viewer) => {
  subscribeKey(resourcePackState, 'currentTexturesDataUrl', () => {
    console.log('applying resourcepack world data')
    viewer.world.customTexturesDataUrl = resourcePackState.currentTexturesDataUrl
    viewer.world.customBlockStatesData = resourcePackState.currentTexturesBlockStates
    if (!viewer?.world.active) return
    viewer.world.updateTexturesData()
  })
}
