/*
 * noVNC: HTML5 VNC client
 * Copyright (C) 2019 The noVNC Authors
 * Licensed under MPL 2.0 (see LICENSE.txt)
 *
 * See README.md for usage and integration instructions.
 *
 */

export default class JPEGDecoder {
  constructor() {
    // RealVNC will reuse the quantization tables
    // and Huffman tables, so we need to cache them.
    this._quantTables = [];
    this._huffmanTables = [];
    this._cachedQuantTables = [];
    this._cachedHuffmanTables = [];

    this._jpegLength = 0;
    this._segments = [];
  }

  decodeRect(x, y, width, height, sock, display, depth) {
    // A rect of JPEG encodings is simply a JPEG file
    if (!this._parseJPEG(sock.rQslice(0))) {
      return false;
    }
    const data = sock.rQshiftBytes(this._jpegLength);
    if (this._quantTables.length != 0 && this._huffmanTables.length != 0) {
      // If there are quantization tables and Huffman tables in the JPEG
      // image, we can directly render it.
      display.imageRect(x, y, width, height, 'image/jpeg', data);
      return true;
    } else {
      // Otherwise we need to insert cached tables.
      const sofIndex = this._segments.findIndex((x) => x[1] == 0xc0 || x[1] == 0xc2);
      if (sofIndex == -1) {
        throw new Error('Illegal JPEG image without SOF');
      }
      let segments = this._segments.slice(0, sofIndex);
      segments = segments.concat(this._quantTables.length ? this._quantTables : this._cachedQuantTables);
      segments.push(this._segments[sofIndex]);
      segments = segments.concat(
        this._huffmanTables.length ? this._huffmanTables : this._cachedHuffmanTables,
        this._segments.slice(sofIndex + 1)
      );
      let length = 0;
      for (let i = 0; i < segments.length; i++) {
        length += segments[i].length;
      }
      const data = new Uint8Array(length);
      length = 0;
      for (let i = 0; i < segments.length; i++) {
        data.set(segments[i], length);
        length += segments[i].length;
      }
      display.imageRect(x, y, width, height, 'image/jpeg', data);
      return true;
    }
  }

  _parseJPEG(buffer) {
    if (this._quantTables.length != 0) {
      this._cachedQuantTables = this._quantTables;
    }
    if (this._huffmanTables.length != 0) {
      this._cachedHuffmanTables = this._huffmanTables;
    }
    this._quantTables = [];
    this._huffmanTables = [];
    this._segments = [];
    let i = 0;
    let bufferLength = buffer.length;
    while (true) {
      let j = i;
      if (j + 2 > bufferLength) {
        return false;
      }
      if (buffer[j] != 0xff) {
        throw new Error('Illegal JPEG marker received (byte: ' + buffer[j] + ')');
      }
      const type = buffer[j + 1];
      j += 2;
      if (type == 0xd9) {
        this._jpegLength = j;
        this._segments.push(buffer.slice(i, j));
        return true;
      } else if (type == 0xda) {
        // start of scan
        let hasFoundEndOfScan = false;
        for (let k = j + 3; k + 1 < bufferLength; k++) {
          if (buffer[k] == 0xff && buffer[k + 1] != 0x00 && !(buffer[k + 1] >= 0xd0 && buffer[k + 1] <= 0xd7)) {
            j = k;
            hasFoundEndOfScan = true;
            break;
          }
        }
        if (!hasFoundEndOfScan) {
          return false;
        }
        this._segments.push(buffer.slice(i, j));
        i = j;
        continue;
      } else if ((type >= 0xd0 && type < 0xd9) || type == 0x01) {
        // No length after marker
        this._segments.push(buffer.slice(i, j));
        i = j;
        continue;
      }
      if (j + 2 > bufferLength) {
        return false;
      }
      const length = (buffer[j] << 8) + buffer[j + 1] - 2;
      if (length < 0) {
        throw new Error('Illegal JPEG length received (length: ' + length + ')');
      }
      j += 2;
      if (j + length > bufferLength) {
        return false;
      }
      j += length;
      const segment = buffer.slice(i, j);
      if (type == 0xc4) {
        // Huffman tables
        this._huffmanTables.push(segment);
      } else if (type == 0xdb) {
        // Quantization tables
        this._quantTables.push(segment);
      }
      this._segments.push(segment);
      i = j;
    }
  }
}
