/**
 * Parse and print the `bun.lockb` file in yarn lockfile v1 format.
 * ```js
 * // in Node.js
 * parse(fs.readFileSync('bun.lockb')) //=> "# yarn lockfile v1\n..."
 * // in Browser
 * parse(await file.arrayBuffer())
 * ```
 */
export function parse(buf: Uint8Array | ArrayBuffer): string {
  let pos = 0
  let view = buf instanceof ArrayBuffer ? new DataView(buf) : new DataView(buf.buffer, buf.byteOffset, buf.byteLength)

  const header_bytes = new TextEncoder().encode('#!/usr/bin/env bun\nbun-lockfile-format-v0\n')

  const u32 = (): number => {
    if (pos + 4 > view.byteLength) throw new TypeError('too short')
    return view.getUint32((pos += 4) - 4, true)
  }

  const u64 = (): number => {
    if (pos + 8 > view.byteLength) throw new TypeError("too short")
    const a = view.getUint32((pos += 4) - 4, true)
    const b = view.getUint32((pos += 4) - 4, true)
    return a + b * (2**32)
  }

  const to_u32 = (a: Uint8Array): Uint32Array => {
    if ((a.byteOffset % 4) === 0) {
      return new Uint32Array(a.buffer, a.byteOffset, a.byteLength / 4)
    } else {
      const view = new DataView(a.buffer, a.byteOffset, a.byteLength)
      return Uint32Array.from({ length: a.byteLength / 4 }, (_, i) => view.getUint32(i * 4, true))
    }
  }

  const read = (n: number): Uint8Array => {
    if (pos + n > view.byteLength) throw new TypeError("too short")
    return new Uint8Array(view.buffer, view.byteOffset + (pos += n) - n, n)
  }

  const eq = (a: Uint8Array, b: Uint8Array): boolean => {
    if (a.byteLength !== b.byteLength) return false
    for (let i = a.byteLength - 1; i >= 0; i--) {
      if (a[i] !== b[i]) return false
    }
    return true
  }

  const assert = (truthy: unknown, message = 'assert failed') => {
    if (truthy) return
    throw new TypeError(message)
  }

  const header_buf = read(header_bytes.byteLength)
  assert(eq(header_buf, header_bytes), 'invalid lockfile')

  const format = u32()
  assert(format === 2, 'outdated lockfile version')

  const meta_hash = read(32)

  const end = u64()
  assert(end <= view.byteLength, 'lockfile is missing data')

  const list_len = u64()
  assert(list_len < 2**32, 'lockfile validation failed: list is impossibly long')

  const input_alignment = u64()
  assert(input_alignment === 8)

  const field_count = u64()
  assert(field_count === 8)

  const begin_at = u64()
  const end_at = u64()
  assert(begin_at <= end && end_at <= end && begin_at <= end_at, 'lockfile validation failed: invalid package list range')

  pos = begin_at
  const packages = Object.entries({
    name: 8,
    name_hash: 8,
    resolution: 64,
    dependencies: 8,
    resolutions: 8,
    meta: 88,
    bin: 20,
    scripts: 48,
  }).reduce((list, [field, len]) => {
      const data = read(len * list_len)
      list.forEach((a, i) => { a[field] = data.subarray(i * len, i * len + len) })
      return list
    }, Array.from({ length: list_len }, () => ({} as any)))

  pos = end_at
  const buffers = [
    'trees',
    'hoisted_dependencies',
    'resolutions', // u32[]
    'dependencies', // name(8) + name_hash(8) + behavior(1) + tag(1) + literal(8) = 26[]
    'extern_strings',
    'string_bytes',
  ].reduce((a, key) => {
      const start = u64()
      const end = u64()
      pos = start
      a[key] = read(end - start)
      pos = end
      return a
    }, {} as any)

  const decoder = new TextDecoder()
  const str = (a: Uint8Array): string => {
    if ((a[7] & 0x80) === 0) {
      let i = a.indexOf(0)
      if (i >= 0) a = a.subarray(0, i)
      return decoder.decode(a)
    } else {
      let [off, len] = to_u32(a)
      len &= ~0x80000000
      return decoder.decode(buffers.string_bytes.subarray(off, off + len))
    }
  }

  const requested_versions: Uint8Array[][] = new Array(list_len)
  requested_versions[0] = []
  for (let i = 1; i < list_len; i++) {
    let resolutions = to_u32(buffers.resolutions.subarray())
    let dependencies = buffers.dependencies.subarray()

    let k = -1
    let all_requested_versions: Uint8Array[] = []
    while ((k = resolutions.indexOf(i)) >= 0) {
      all_requested_versions.push(dependencies.subarray(k * 26, k * 26 + 26))
      dependencies = dependencies.subarray(k * 26 + 26)
      resolutions = resolutions.subarray(k + 1)
    }

    requested_versions[i] = all_requested_versions
  }

  const hex = (a: number) => (0x100 + a).toString(16).slice(1)
  const fmt_hash = (a: Uint8Array): string => {
    if (a.byteLength < 32) throw new TypeError('meta_hash too short')
    let hash = ''
    for (let i = 0; i < 32; i++) {
      let c = hex(a[i])
      if (i < 8 || 16 <= i && i < 24) c = c.toUpperCase()
      hash += c
      if (i < 31 && (i + 1) % 8 === 0) hash += '-'
    }
    return hash
  }

  const enum ResolutionTag {
    uninitialized = 0, root = 1, npm = 2, folder = 4,
    local_tarball = 8,
    github = 16, gitlab = 24,
    git = 32,
    symlink = 64,
    workspace = 72,
    remote_tarball = 80,
    single_file_module = 100,
  }

  const is_scp = (s: string): boolean => {
    if (s.length < 3) return false
    let at = -1
    for (let i = 0; i < s.length; i++) {
      if (s[i] === '@') {
        if (at < 0) at = i
      } else if (s[i] === ':') {
        if (s.slice(i).startsWith('://')) return false
        return at >= 0 ? i > at + 1 : i > 0
      } else if (s[i] === '/') {
        return at >= 0 && i > at + 1
      }
    }
    return false
  }

  const fmt_resolution = (a: Uint8Array): string => {
    if (a.byteLength < 64) throw new TypeError('resolution too short')
    const tag = a[0]
    const view = new DataView(a.buffer, a.byteOffset, a.byteLength)
    let pos = 8
    // url(string) + version
    if (tag === ResolutionTag.npm) {
      pos += 8 // skip string
      const major = view.getUint32((pos += 4) - 4, true)
      const minor = view.getUint32((pos += 4) - 4, true)
      const patch = view.getUint32((pos += 4) - 4, true)
      pos += 4 // skip padding
      const version_tag = new Uint8Array(view.buffer, view.byteOffset + pos, 32)
      const pre = str(version_tag.subarray(0, 8))
      const build = str(version_tag.subarray(16, 24))
      let v = `${major}.${minor}.${patch}`
      if (pre) v += '-' + pre
      if (build) v += '+' + build
      return v
    }
    // string
    if (tag === ResolutionTag.folder || tag === ResolutionTag.local_tarball ||
        tag === ResolutionTag.remote_tarball || tag === ResolutionTag.workspace ||
        tag === ResolutionTag.symlink || tag === ResolutionTag.single_file_module) {
      let v = str(new Uint8Array(view.buffer, view.byteOffset + pos, 8))
      if (tag === ResolutionTag.workspace) v = `workspace:${v}`
      if (tag === ResolutionTag.symlink) v = `link:${v}`
      if (tag === ResolutionTag.single_file_module) v = `module:${v}`
      return v
    }
    // owner(string) + repo(string) + commitish(SHA) + resolved(SHA) + package_name(string)
    if (tag === ResolutionTag.git || tag === ResolutionTag.github || tag === ResolutionTag.gitlab) {
      let out = tag === ResolutionTag.git ? 'git+' : tag === ResolutionTag.github ? 'github:' : 'gitlab:'
      let owner = str(new Uint8Array(view.buffer, view.byteOffset + pos, 8))
      let repo = str(new Uint8Array(view.buffer, view.byteOffset + pos + 8, 8))
      if (owner) out += owner + '/'
      else if (is_scp(repo)) out += 'ssh://'
      out += repo
      pos += 16
      let commitish = str(new Uint8Array(view.buffer, view.byteOffset + pos, 8))
      let resolved = str(new Uint8Array(view.buffer, view.byteOffset + pos + 8, 8))
      if (resolved) {
        out += '#'
        let i = -1
        if ((i = resolved.lastIndexOf('-')) >= 0) {
          resolved = resolved.slice(i + 1)
        }
        out += resolved
      } else if (commitish) {
        out += '#' + commitish
      }
      return out
    }
    return ""
  }

  const fmt_url = (a: Uint8Array): string => {
    if (a.byteLength < 64) throw new TypeError('resolution too short')
    // url(string) + version
    if (a[0] === ResolutionTag.npm) {
      return str(new Uint8Array(a.buffer, a.byteOffset + 8, 8))
    } else {
      return fmt_resolution(a)
    }
  }

  const slice = (data: Uint8Array, a: Uint8Array, item: number): Uint8Array[] => {
    const [off, length] = to_u32(a)
    return Array.from({ length }, (_, i) => data.subarray(
      item * off + item * i,
      item * off + item * i + item,
    ))
  }

  const base64 = (a: Uint8Array): string => {
    let ret: string
    if (a.length < 65535) {
      ret = globalThis.btoa(String.fromCodePoint.apply(String, a as any))
    } else {
      ret = ''
      for (let value of a) {
        ret += String.fromCodePoint(value)
      }
      ret = globalThis.btoa(ret)
    }
    return ret
  }

  const fmt_integrity = (a: Uint8Array): string => {
    if (a.byteLength < 65) throw new TypeError('integrity too short')
    const tag = a[0] // [0, sha1, sha256, sha384, sha512]
    a = a.subarray(1)
    let out: string
    if (tag === 1) out = 'sha1-'
    else if (tag === 2) out = 'sha256-'
    else if (tag === 3) out = 'sha384-'
    else if (tag === 4) out = 'sha512-'
    else return ''
    out += base64(a)
    return out
  }

  const quote = (s: string): string => {
    if (s.startsWith('true') || s.startsWith('false') ||
        /[:\s\n\\",\[\]|\t!]/g.test(s) || /^[0-9]/g.test(s) || !/^[a-zA-Z]/g.test(s))
      return JSON.stringify(s)
    else
      return s
  }
  const fmt_specs = (name: string, specs: string[], version: string): string => {
    specs = Array.from(new Set(specs.map(e => e || `^${version}`)))
    specs.sort((a, b) => a.localeCompare(b))
    let out = '', comma = false
    for (const spec of specs) {
      const item = name + '@' + spec
      if (comma) out += ', '
      out += quote(item)
      comma = true
    }
    return out + ':'
  }

  let out = [
    '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.',
    '# yarn lockfile v1',
    '# bun ./bun.lockb --hash: ' + fmt_hash(meta_hash),
    '',
  ]

  const order = Array.from({ length: list_len }, (_, i) => i).slice(1).sort((a, b) => {
    const pa = packages[a]
    const pb = packages[b]
    return str(pa.name).localeCompare(str(pb.name)) ||
      fmt_resolution(pa.resolution).localeCompare(fmt_resolution(pb.resolution))
  })

  for (const i of order) {
    const a = packages[i]
    const name = str(a.name)
    const resolution = a.resolution
    const meta = a.meta
    const dependencies = slice(buffers.dependencies, a.dependencies, 26)
    const dependency_versions = requested_versions[i]
    const version = fmt_resolution(resolution)
    const versions = dependency_versions.map((b) => str(b.subarray(18, 18 + 8)))
    const url = fmt_url(resolution)
    const integrity = fmt_integrity(meta.subarray(20, 85))

    out.push('')
    out.push(fmt_specs(name, versions, version))
    out.push(`  version ${JSON.stringify(version)}`)
    out.push(`  resolved ${JSON.stringify(url)}`)
    if (integrity) {
      out.push(`  integrity ${integrity}`)
    }
    if (dependencies.length > 0) {
      const enum Behavior {
        _ = 0,
        normal = 0b10,
        optional = 0b100,
        dev = 0b1000,
        peer = 0b10000,
        workspace = 0b100000,
      }
      let behavior = Behavior._
      for (let dependency of dependencies) {
        let dep_behavior = dependency[16]
        if (behavior !== dep_behavior) {
          if ((dep_behavior & Behavior.optional) > 0) {
            out.push("  optionalDependencies:")
          } else if ((dep_behavior & Behavior.normal) > 0) {
            out.push("  dependencies:")
          } else if ((dep_behavior & Behavior.dev) > 0) {
            out.push("  devDependencies:")
          } else continue
          behavior = dep_behavior
        }
        let dep_name = str(dependency.subarray(0, 8))
        let literal = str(dependency.subarray(18, 18 + 8))
        out.push(`    ${quote(dep_name)} "${literal}"`)
      }
    }
  }

  out.push('')
  return out.join('\n')
}
