const VAP_CONFIG_BOX = 'vapc'
const TEXT_DECODER = typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8') : null

const configCache = new Map()
const containerBoxes = new Set([
  'moov',
  'trak',
  'mdia',
  'minf',
  'stbl',
  'edts',
  'udta',
  'meta',
  'ilst',
  'moof',
  'traf',
  'mvex',
  'mfra',
])

function readBoxType(view, offset) {
  return String.fromCharCode(
    view.getUint8(offset),
    view.getUint8(offset + 1),
    view.getUint8(offset + 2),
    view.getUint8(offset + 3)
  )
}

function readBoxSize(view, offset, end) {
  const smallSize = view.getUint32(offset)

  if (smallSize === 0) {
    return { size: end - offset, headerSize: 8 }
  }

  if (smallSize === 1) {
    const high = view.getUint32(offset + 8)
    const low = view.getUint32(offset + 12)
    return { size: high * 2 ** 32 + low, headerSize: 16 }
  }

  return { size: smallSize, headerSize: 8 }
}

function findBoxPayload(buffer, boxName, start = 0, end = buffer.byteLength) {
  const view = new DataView(buffer)
  let offset = start

  while (offset + 8 <= end) {
    const { size, headerSize } = readBoxSize(view, offset, end)
    const type = readBoxType(view, offset + 4)

    if (size < headerSize || offset + size > end) {
      break
    }

    const payloadStart = offset + headerSize
    const payloadEnd = offset + size

    if (type === boxName) {
      return buffer.slice(payloadStart, payloadEnd)
    }

    if (containerBoxes.has(type)) {
      const childStart = type === 'meta' ? payloadStart + 4 : payloadStart
      const payload = findBoxPayload(buffer, boxName, childStart, payloadEnd)
      if (payload) {
        return payload
      }
    }

    offset += size
  }

  return null
}

function decodeUtf8(buffer) {
  if (TEXT_DECODER) {
    return TEXT_DECODER.decode(buffer)
  }

  const bytes = new Uint8Array(buffer)
  let binary = ''

  for (let i = 0; i < bytes.length; i += 1) {
    binary += String.fromCharCode(bytes[i])
  }

  return decodeURIComponent(escape(binary))
}

function parseJsonPayload(payload) {
  const rawText = decodeUtf8(payload).replace(/^\uFEFF/, '').replace(/\0+$/g, '').trim()
  const jsonStart = rawText.search(/[\[{]/)

  if (jsonStart < 0) {
    throw new Error('vapc box does not contain json')
  }

  return JSON.parse(rawText.slice(jsonStart))
}

export async function getVapConfigFromMp4(mp4Url) {
  if (!mp4Url) {
    throw new Error('mp4 url is required')
  }

  if (configCache.has(mp4Url)) {
    return configCache.get(mp4Url)
  }

  const configPromise = fetch(mp4Url)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`mp4 request failed: ${response.status}`)
      }

      return response.arrayBuffer()
    })
    .then((buffer) => {
      const payload = findBoxPayload(buffer, VAP_CONFIG_BOX)

      if (!payload) {
        throw new Error('vapc box not found')
      }

      return parseJsonPayload(payload)
    })
    .catch((error) => {
      configCache.delete(mp4Url)
      throw error
    })

  configCache.set(mp4Url, configPromise)
  return configPromise
}

export function preloadVapConfig(mp4Url) {
  return getVapConfigFromMp4(mp4Url).catch(() => null)
}

export function clearVapConfigCache(mp4Url) {
  if (mp4Url) {
    configCache.delete(mp4Url)
    return
  }

  configCache.clear()
}
