import { IOBuffer } from 'iobuffer';

const CLEAR_CODE = 256;
const EOI_CODE = 257;
// 0-255 from the table + 256 for clear code + 257 for end of information code.
const TABLE_START = 258;
const MIN_BIT_LENGTH = 9;

let stringTable: number[][] = [];
function initializeStringTable() {
  if (stringTable.length === 0) {
    for (let i = 0; i < 256; i++) {
      stringTable.push([i]);
    }
    // Fill the table with dummy data.
    // Elements at indices > 257 will be replaced during decompression.
    const dummyString: number[] = [];
    for (let i = 256; i < 4096; i++) {
      stringTable.push(dummyString);
    }
  }
}

const andTable = [511, 1023, 2047, 4095];
const bitJumps = [0, 0, 0, 0, 0, 0, 0, 0, 0, 511, 1023, 2047, 4095];

class LzwDecoder {
  private stripArray: Uint8Array;
  private nextData = 0;
  private nextBits = 0;
  private bytePointer = 0;
  private tableLength = TABLE_START;
  private currentBitLength = MIN_BIT_LENGTH;
  private outData: IOBuffer;

  public constructor(data: DataView) {
    this.stripArray = new Uint8Array(
      data.buffer,
      data.byteOffset,
      data.byteLength,
    );
    this.outData = new IOBuffer(data.byteLength);
    this.initializeTable();
  }

  public decode(): DataView {
    let code = 0;
    let oldCode = 0;
    while ((code = this.getNextCode()) !== EOI_CODE) {
      if (code === CLEAR_CODE) {
        this.initializeTable();
        code = this.getNextCode();
        if (code === EOI_CODE) {
          break;
        }
        this.writeString(this.stringFromCode(code));
        oldCode = code;
      } else if (this.isInTable(code)) {
        this.writeString(this.stringFromCode(code));
        this.addStringToTable(
          this.stringFromCode(oldCode).concat(this.stringFromCode(code)[0]),
        );
        oldCode = code;
      } else {
        const outString = this.stringFromCode(oldCode).concat(
          this.stringFromCode(oldCode)[0],
        );
        this.writeString(outString);
        this.addStringToTable(outString);
        oldCode = code;
      }
    }
    const outArray = this.outData.toArray();

    return new DataView(
      outArray.buffer,
      outArray.byteOffset,
      outArray.byteLength,
    );
  }

  private initializeTable(): void {
    initializeStringTable();
    this.tableLength = TABLE_START;
    this.currentBitLength = MIN_BIT_LENGTH;
  }

  private writeString(string: number[]): void {
    this.outData.writeBytes(string);
  }

  private stringFromCode(code: number): number[] {
    // At this point, `code` must be defined in the table.
    return stringTable[code];
  }

  private isInTable(code: number): boolean {
    return code < this.tableLength;
  }

  private addStringToTable(string: number[]): void {
    stringTable[this.tableLength++] = string;
    if (stringTable.length > 4096) {
      stringTable = [];
      throw new Error(
        'LZW decoding error. Please open an issue at https://github.com/image-js/tiff/issues/new/choose (include a test image).',
      );
    }
    if (this.tableLength === bitJumps[this.currentBitLength]) {
      this.currentBitLength++;
    }
  }

  private getNextCode(): number {
    this.nextData =
      (this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
    this.nextBits += 8;

    if (this.nextBits < this.currentBitLength) {
      this.nextData =
        (this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
      this.nextBits += 8;
    }

    const code =
      (this.nextData >> (this.nextBits - this.currentBitLength)) &
      andTable[this.currentBitLength - 9];
    this.nextBits -= this.currentBitLength;

    // This should not really happen but is present in other codes as well.
    // See: https://github.com/sugark/Tiffus/blob/15a60123813d1612f4ae9e4fab964f9f7d71cf63/src/org/eclipse/swt/internal/image/TIFFLZWDecoder.java
    if (this.bytePointer > this.stripArray.length) {
      return 257;
    }

    return code;
  }
}

export function decompressLzw(stripData: DataView): DataView {
  return new LzwDecoder(stripData).decode();
}
