import type { Buffer } from 'node:buffer';
import type { FrameOptions, WriterOptions } from './types';

export declare class Writer {
  private buffer: Buffer
  private width: number
  private height: number
  private position: number = 0
  private ended: boolean = false
  private globalPalette: number[] | null

  constructor(buf: Buffer, width: number, height: number, options: WriterOptions = {}) {
    this.buffer = buf
    this.width = width
    this.height = height
    this.globalPalette = options.palette ?? null

    if (width <= 0 || height <= 0 || width > 65535 || height > 65535) {
      throw new Error('Width/Height invalid.')
    }

    this.writeHeader()
    this.writeLogicalScreenDescriptor(options)
    this.writeGlobalColorTable()
    this.writeNetscapeLoopingExtension(options.loop)
  }

  private checkPaletteAndNumColors(palette: number[]): number {
    const num_colors = palette.length

    if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors - 1)) {
      throw new Error('Invalid code/color length, must be power of 2 and 2 .. 256.')
    }

    return num_colors
  }

  private writeHeader(): void {
    this.buffer[this.position++] = 0x47 
    this.buffer[this.position++] = 0x49 
    this.buffer[this.position++] = 0x46 
    this.buffer[this.position++] = 0x38 
    this.buffer[this.position++] = 0x39 
    this.buffer[this.position++] = 0x61 
  }

  private writeLogicalScreenDescriptor(options: WriterOptions): void {
    let gp_num_colors_pow2 = 0
    let background = 0

    if (this.globalPalette !== null) {
      let gp_num_colors = this.checkPaletteAndNumColors(this.globalPalette)
      while (gp_num_colors >>= 1) ++gp_num_colors_pow2
      gp_num_colors = 1 << gp_num_colors_pow2
      --gp_num_colors_pow2

      if (options.background !== undefined) {
        background = options.background
        if (background >= gp_num_colors) {
          throw new Error('Background index out of range.')
        }
        if (background === 0) {
          throw new Error('Background index explicitly passed as 0.')
        }
      }
    }

    this.buffer[this.position++] = this.width & 0xFF
    this.buffer[this.position++] = this.width >> 8 & 0xFF
    this.buffer[this.position++] = this.height & 0xFF
    this.buffer[this.position++] = this.height >> 8 & 0xFF
    this.buffer[this.position++] = (this.globalPalette !== null ? 0x80 : 0) | gp_num_colors_pow2
    this.buffer[this.position++] = background
    this.buffer[this.position++] = 0 
  }

  private writeGlobalColorTable(): void {
    if (this.globalPalette !== null) {
      for (let i = 0; i < this.globalPalette.length; ++i) {
        const rgb = this.globalPalette[i]
        this.buffer[this.position++] = rgb >> 16 & 0xFF
        this.buffer[this.position++] = rgb >> 8 & 0xFF
        this.buffer[this.position++] = rgb & 0xFF
      }
    }
  }

  private writeNetscapeLoopingExtension(loopCount: number | null | undefined): void {
    if (loopCount !== null && loopCount !== undefined) {
      if (loopCount < 0 || loopCount > 65535) {
        throw new Error('Loop count invalid.')
      }

      this.buffer[this.position++] = 0x21 
      this.buffer[this.position++] = 0xFF 
      this.buffer[this.position++] = 0x0B 

      this.buffer[this.position++] = 0x4E 
      this.buffer[this.position++] = 0x45 
      this.buffer[this.position++] = 0x54 
      this.buffer[this.position++] = 0x53 
      this.buffer[this.position++] = 0x43 
      this.buffer[this.position++] = 0x41 
      this.buffer[this.position++] = 0x50 
      this.buffer[this.position++] = 0x45 
      this.buffer[this.position++] = 0x32 
      this.buffer[this.position++] = 0x2E 
      this.buffer[this.position++] = 0x30 

      this.buffer[this.position++] = 0x03 
      this.buffer[this.position++] = 0x01 
      this.buffer[this.position++] = loopCount & 0xFF 
      this.buffer[this.position++] = loopCount >> 8 & 0xFF
      this.buffer[this.position++] = 0x00 
    }
  }

  public addFrame(
    x: number,
    y: number,
    width: number,
    height: number,
    indexedPixels: Uint8Array,
    options: FrameOptions = {},
  ): number {
    if (this.ended) {
      --this.position
      this.ended = false
    }

    if (x < 0 || y < 0 || x > 65535 || y > 65535) {
      throw new Error('x/y invalid.')
    }

    if (width <= 0 || height <= 0 || width > 65535 || height > 65535) {
      throw new Error('Width/Height invalid.')
    }

    if (indexedPixels.length < width * height) {
      throw new Error('Not enough pixels for the frame size.')
    }

    const usingLocalPalette = options.palette !== undefined && options.palette !== null
    const palette = usingLocalPalette ? options.palette! : this.globalPalette

    if (!palette) {
      throw new Error('Must supply either a local or global palette.')
    }

    let numColors = this.checkPaletteAndNumColors(palette)

    let minCodeSize = 0
    while (numColors >>= 1) ++minCodeSize
    numColors = 1 << minCodeSize

    const delay = options.delay ?? 0
    const disposal = options.disposal ?? 0

    if (disposal < 0 || disposal > 3) {
      throw new Error('Disposal out of range.')
    }

    let useTransparency = false
    let transparentIndex = 0

    if (options.transparent !== undefined && options.transparent !== null) {
      useTransparency = true
      transparentIndex = options.transparent
      if (transparentIndex < 0 || transparentIndex >= numColors) {
        throw new Error('Transparent color index.')
      }
    }

    if (disposal !== 0 || useTransparency || delay !== 0) {
      this.buffer[this.position++] = 0x21 
      this.buffer[this.position++] = 0xF9 
      this.buffer[this.position++] = 4 
      this.buffer[this.position++] = disposal << 2 | (useTransparency ? 1 : 0)
      this.buffer[this.position++] = delay & 0xFF
      this.buffer[this.position++] = delay >> 8 & 0xFF
      this.buffer[this.position++] = transparentIndex
      this.buffer[this.position++] = 0 
    }

    this.buffer[this.position++] = 0x2C 
    this.buffer[this.position++] = x & 0xFF
    this.buffer[this.position++] = x >> 8 & 0xFF
    this.buffer[this.position++] = y & 0xFF
    this.buffer[this.position++] = y >> 8 & 0xFF
    this.buffer[this.position++] = width & 0xFF
    this.buffer[this.position++] = width >> 8 & 0xFF
    this.buffer[this.position++] = height & 0xFF
    this.buffer[this.position++] = height >> 8 & 0xFF
    this.buffer[this.position++] = usingLocalPalette ? (0x80 | (minCodeSize - 1)) : 0

    if (usingLocalPalette) {
      for (let i = 0; i < palette.length; ++i) {
        const rgb = palette[i]
        this.buffer[this.position++] = rgb >> 16 & 0xFF
        this.buffer[this.position++] = rgb >> 8 & 0xFF
        this.buffer[this.position++] = rgb & 0xFF
      }
    }

    this.position = writerOutputLZWCodeStream(
      this.buffer,
      this.position,
      minCodeSize < 2 ? 2 : minCodeSize,
      indexedPixels,
    )

    return this.position
  }

  public end(): number {
    if (!this.ended) {
      this.buffer[this.position++] = 0x3B 
      this.ended = true
    }
    return this.position
  }

  public getOutputBuffer(): Buffer {
    return this.buffer
  }

  public setOutputBuffer(buffer: Buffer): void {
    this.buffer = buffer
  }

  public getOutputBufferPosition(): number {
    return this.position
  }

  public setOutputBufferPosition(position: number): void {
    this.position = position
  }
}