import { readUint64LE } from './long.ts'

import type { GenericFilehandle } from 'generic-filehandle2'

const ENTRY_SIZE = 16

function parseEntries(buf: Uint8Array, numEntries: number) {
  const entries: [number, number][] = new Array(numEntries + 1)
  entries[0] = [0, 0]
  const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
  for (let i = 0; i < numEntries; i += 1) {
    const offset = i * ENTRY_SIZE
    entries[i + 1] = [readUint64LE(view, offset), readUint64LE(view, offset + 8)]
  }
  return entries
}

// lower-bound binary search: first index whose uncompressed start > position
function findUpperBound(entries: [number, number][], position: number) {
  let lo = 0
  let hi = entries.length
  while (lo < hi) {
    const mid = (lo + hi) >>> 1
    if (entries[mid]![1] <= position) {
      lo = mid + 1
    } else {
      hi = mid
    }
  }
  return lo
}

export default class GziIndex {
  filehandle: GenericFilehandle

  index?: Promise<[number, number][]>

  constructor({ filehandle }: { filehandle: GenericFilehandle }) {
    this.filehandle = filehandle
  }

  _getIndex() {
    if (!this.index) {
      this.index = this._readIndex().catch((error: unknown) => {
        this.index = undefined
        throw error
      })
    }
    return this.index
  }

  private async _readIndex(): Promise<[number, number][]> {
    const header = await this.filehandle.read(8, 0)
    const numEntries = readUint64LE(
      new DataView(header.buffer, header.byteOffset, header.byteLength),
    )
    if (numEntries === 0) {
      return [[0, 0]]
    }
    if (numEntries > Number.MAX_SAFE_INTEGER / ENTRY_SIZE) {
      throw new TypeError('integer overflow')
    }
    const body = await this.filehandle.read(ENTRY_SIZE * numEntries, 8)
    return parseEntries(body, numEntries)
  }

  async getRelevantBlocksForRead(
    length: number,
    position: number,
  ): Promise<{
    blocks: [number, number][]
    nextCompressedPosition: number | undefined
  }> {
    if (length === 0) {
      return { blocks: [], nextCompressedPosition: undefined }
    }
    const entries = await this._getIndex()
    const readEnd = position + length

    const startBlock = findUpperBound(entries, position) - 1
    let endBlock = startBlock + 1
    while (endBlock < entries.length && entries[endBlock]![1] < readEnd) {
      endBlock += 1
    }
    return {
      blocks: entries.slice(startBlock, endBlock),
      nextCompressedPosition: entries[endBlock]?.[0],
    }
  }
}
