import type { Buffer } from 'node:buffer';
import type { Frame } from './types';

export declare class Reader {
  private frames: Frame[] = []
  private width: number
  private height: number
  private loop_count: number | null = null
  private buffer: Buffer

  constructor(buf: Buffer) {
    this.buffer = buf
    let p = 0

    if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46
      || buf[p++] !== 0x38 || (buf[p++] + 1 & 0xFD) !== 0x38 || buf[p++] !== 0x61) {
      throw new Error('Invalid GIF 87a/89a header.')
    }

    this.width = buf[p++] | buf[p++] << 8
    this.height = buf[p++] | buf[p++] << 8
    const pf0 = buf[p++] 
    const global_palette_flag = pf0 >> 7
    const num_global_colors_pow2 = pf0 & 0x7
    const num_global_colors = 1 << (num_global_colors_pow2 + 1)

    buf[p++] 

    let global_palette_offset = null
    let global_palette_size = null

    if (global_palette_flag) {
      global_palette_offset = p
      global_palette_size = num_global_colors
      p += num_global_colors * 3 
    }

    let no_eof = true
    let delay = 0
    let transparent_index = null
    let disposal = 0 

    while (no_eof && p < buf.length) {
      switch (buf[p++]) {
        case 0x21: 
          switch (buf[p++]) {
            case 0xFF: 
              if (buf[p] !== 0x0B 
                || buf[p + 1] === 0x4E && buf[p + 2] === 0x45 && buf[p + 3] === 0x54
                && buf[p + 4] === 0x53 && buf[p + 5] === 0x43 && buf[p + 6] === 0x41
                && buf[p + 7] === 0x50 && buf[p + 8] === 0x45 && buf[p + 9] === 0x32
                && buf[p + 10] === 0x2E && buf[p + 11] === 0x30
                && buf[p + 12] === 0x03 && buf[p + 13] === 0x01 && buf[p + 16] === 0) {
                p += 14
                this.loop_count = buf[p++] | buf[p++] << 8
                p++ 
              }
              else { 
                p += 12
                while (true) { 
                  const block_size = buf[p++]
                  if (!(block_size >= 0))
                    throw new Error('Invalid block size')
                  if (block_size === 0)
                    break 
                  p += block_size
                }
              }
              break

            case 0xF9: { 
              if (buf[p++] !== 0x4 || buf[p + 4] !== 0)
                throw new Error('Invalid graphics extension block.')

              const pf1 = buf[p++]

              delay = buf[p++] | buf[p++] << 8
              transparent_index = buf[p++]
              if ((pf1 & 1) === 0)
                transparent_index = null
              disposal = pf1 >> 2 & 0x7
              p++ 
              break
            }

            case 0x01: 
            case 0xFE: 
              while (true) { 
                const block_size = buf[p++]
                if (!(block_size >= 0))
                  throw new Error('Invalid block size')
                if (block_size === 0)
                  break
                p += block_size
              }
              break

            default:
              throw new Error(
                `Unknown graphic control label: 0x${buf[p - 1].toString(16)}`,
              )
          }
          break

        case 0x2C: { 
          const x = buf[p++] | buf[p++] << 8
          const y = buf[p++] | buf[p++] << 8
          const w = buf[p++] | buf[p++] << 8
          const h = buf[p++] | buf[p++] << 8
          const pf2 = buf[p++]
          const local_palette_flag = pf2 >> 7
          const interlace_flag = pf2 >> 6 & 1
          const num_local_colors_pow2 = pf2 & 0x7
          const num_local_colors = 1 << (num_local_colors_pow2 + 1)
          const data_offset = p

          let palette_offset = global_palette_offset
          let palette_size = global_palette_size
          let has_local_palette = false
          if (local_palette_flag) {
            has_local_palette = true
            palette_offset = p 
            palette_size = num_local_colors
            p += num_local_colors * 3 
          }

          p++ 
          while (true) {
            const block_size = buf[p++]
            if (!(block_size >= 0))
              throw new Error('Invalid block size')
            if (block_size === 0)
              break
            p += block_size
          }

          this.frames.push({
            x,
            y,
            width: w,
            height: h,
            has_local_palette,
            palette_offset,
            palette_size,
            data_offset,
            data_length: p - data_offset,
            transparent_index,
            interlaced: !!interlace_flag,
            delay,
            disposal,
          })
          break
        }

        case 0x3B: 
          no_eof = false
          break

        default:
          throw new Error(`Unknown gif block: 0x${buf[p - 1].toString(16)}`)
      }
    }
  }

  numFrames(): number {
    return this.frames.length
  }

  getLoopCount(): number | null {
    return this.loop_count
  }

  frameInfo(frame_num: number): Frame {
    if (frame_num < 0 || frame_num >= this.frames.length)
      throw new Error('Frame index out of range.')
    return this.frames[frame_num]
  }

  decodeAndBlitFrameBGRA(frame_num: number, pixels: Uint8Array): void {
    const frame = this.frameInfo(frame_num)
    const num_pixels = frame.width * frame.height
    const index_stream = new Uint8Array(num_pixels)

    readerLZWOutputIndexStream( 
      this.buffer,
      frame.data_offset,
      index_stream,
      num_pixels,
    )

    const palette_offset = frame.palette_offset

    let trans = frame.transparent_index
    if (trans === null)
      trans = 256

    const framewidth = frame.width
    const framestride = this.width - framewidth
    let xleft = framewidth 

    const opbeg = ((frame.y * this.width) + frame.x) * 4
    const opend = ((frame.y + frame.height) * this.width + frame.x) * 4
    let op = opbeg

    let scanstride = framestride * 4

    if (frame.interlaced === true) {
      scanstride += this.width * 4 * 7
    }

    let interlaceskip = 8

    for (let i = 0, il = index_stream.length; i < il; ++i) {
      const index = index_stream[i]

      if (xleft === 0) { 
        op += scanstride
        xleft = framewidth

        if (op >= opend) {
          scanstride = framestride * 4 + this.width * 4 * (interlaceskip - 1)
          op = opbeg + (framewidth + framestride) * (interlaceskip << 1)
          interlaceskip >>= 1
        }
      }

      if (index === trans) {
        op += 4
      }
      else {
        if (palette_offset === null) {
          throw new Error('No palette found for frame')
        }

        const r = this.buffer[palette_offset + index * 3]
        const g = this.buffer[palette_offset + index * 3 + 1]
        const b = this.buffer[palette_offset + index * 3 + 2]
        pixels[op++] = b
        pixels[op++] = g
        pixels[op++] = r
        pixels[op++] = 255
      }
      --xleft
    }
  }

  decodeAndBlitFrameRGBA(frame_num: number, pixels: Uint8Array): void {
    const frame = this.frameInfo(frame_num)
    const num_pixels = frame.width * frame.height
    const index_stream = new Uint8Array(num_pixels)

    readerLZWOutputIndexStream( 
      this.buffer,
      frame.data_offset,
      index_stream,
      num_pixels,
    )

    const palette_offset = frame.palette_offset
    if (palette_offset === null) {
      throw new Error('No palette found for frame')
    }

    let trans = frame.transparent_index
    if (trans === null)
      trans = 256

    const framewidth = frame.width
    const framestride = this.width - framewidth
    let xleft = framewidth

    const opbeg = ((frame.y * this.width) + frame.x) * 4
    const opend = ((frame.y + frame.height) * this.width + frame.x) * 4
    let op = opbeg

    let scanstride = framestride * 4

    if (frame.interlaced === true) {
      scanstride += this.width * 4 * 7
    }

    let interlaceskip = 8

    for (let i = 0, il = index_stream.length; i < il; ++i) {
      const index = index_stream[i]

      if (xleft === 0) {
        op += scanstride
        xleft = framewidth
        if (op >= opend) {
          scanstride = framestride * 4 + this.width * 4 * (interlaceskip - 1)
          op = opbeg + (framewidth + framestride) * (interlaceskip << 1)
          interlaceskip >>= 1
        }
      }

      if (index === trans) {
        op += 4
      }
      else {
        const r = this.buffer[palette_offset + index * 3]
        const g = this.buffer[palette_offset + index * 3 + 1]
        const b = this.buffer[palette_offset + index * 3 + 2]
        pixels[op++] = r
        pixels[op++] = g
        pixels[op++] = b
        pixels[op++] = 255
      }

      --xleft
    }
  }
}
export declare function readerLZWOutputIndexStream(code_stream: Buffer, p: number, output: Uint8Array, output_length: number): Uint8Array<ArrayBufferLike> | void;