import CramCodec from './_base.ts'
import { CramBufferOverrunError, CramUnimplementedError } from '../../errors.ts'

import type { Cursor, Cursors } from './_base.ts'
import type { GammaEncoding } from '../encoding.ts'
import type { CramFileBlock } from '../file.ts'
import type CramSlice from '../slice/index.ts'

export default class GammaCodec extends CramCodec<
  'int',
  GammaEncoding['parameters']
> {
  constructor(parameters: GammaEncoding['parameters'], dataType: 'int') {
    super(parameters, dataType)
    if (this.dataType !== 'int') {
      throw new CramUnimplementedError(
        `${this.dataType} decoding not yet implemented by GAMMA codec`,
      )
    }
  }

  decode(
    _slice: CramSlice,
    coreDataBlock: CramFileBlock,
    _blocksByContentId: Record<number, CramFileBlock>,
    cursors: Cursors,
  ) {
    return decodeGammaInline(
      coreDataBlock.content,
      cursors.coreBlock,
      this.parameters.offset,
    )
  }
}

/**
 * Optimized gamma decoder with inlined bit reading.
 * Avoids function call overhead by inlining the getBits logic.
 */
function decodeGammaInline(
  data: Uint8Array,
  cursor: Cursor,
  offset: number,
): number {
  let { bytePosition, bitPosition } = cursor
  let length = 1

  // Count leading zeros (each 0 bit increases length). A truncated core block
  // reads as zeros forever, so guard against running off the end.
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
  while (true) {
    if (bytePosition >= data.length) {
      throw new CramBufferOverrunError(
        'read beyond end of core block; file seems truncated',
      )
    }
    const bit = (data[bytePosition]! >> bitPosition) & 1
    bitPosition -= 1
    if (bitPosition < 0) {
      bytePosition += 1
      bitPosition = 7
    }
    if (bit === 1) {
      break
    }
    length += 1
  }

  // Now read (length - 1) more bits for the value
  let readBits = 0
  const bitsToRead = length - 1
  if (bitsToRead > 0) {
    // Optimized multi-bit read
    for (let i = 0; i < bitsToRead; i++) {
      readBits <<= 1
      readBits |= (data[bytePosition]! >> bitPosition) & 1
      bitPosition -= 1
      if (bitPosition < 0) {
        bytePosition += 1
        bitPosition = 7
      }
    }
  }

  // Update cursor
  cursor.bytePosition = bytePosition
  cursor.bitPosition = bitPosition as Cursor['bitPosition']

  const value = readBits | (1 << (length - 1))
  return value - offset
}
