/*
 * noVNC: HTML5 VNC client
 * Copyright (C) 2019 The noVNC Authors
 * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
 * Licensed under MPL 2.0 (see LICENSE.txt)
 *
 * See README.md for usage and integration instructions.
 *
 */

import * as Log from '../util/logging.js';
import Inflator from '../inflator.js';

export default class TightDecoder {
  constructor() {
    this._ctl = null;
    this._filter = null;
    this._numColors = 0;
    this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
    this._len = 0;

    this._zlibs = [];
    for (let i = 0; i < 4; i++) {
      this._zlibs[i] = new Inflator();
    }
  }

  decodeRect(x, y, width, height, sock, display, depth) {
    if (this._ctl === null) {
      if (sock.rQwait('TIGHT compression-control', 1)) {
        return false;
      }

      this._ctl = sock.rQshift8();

      // Reset streams if the server requests it
      for (let i = 0; i < 4; i++) {
        if ((this._ctl >> i) & 1) {
          this._zlibs[i].reset();
          Log.Info('Reset zlib stream ' + i);
        }
      }

      // Figure out filter
      this._ctl = this._ctl >> 4;
    }

    let ret;

    if (this._ctl === 0x08) {
      ret = this._fillRect(x, y, width, height, sock, display, depth);
    } else if (this._ctl === 0x09) {
      ret = this._jpegRect(x, y, width, height, sock, display, depth);
    } else if (this._ctl === 0x0a) {
      ret = this._pngRect(x, y, width, height, sock, display, depth);
    } else if ((this._ctl & 0x08) == 0) {
      ret = this._basicRect(this._ctl, x, y, width, height, sock, display, depth);
    } else {
      throw new Error('Illegal tight compression received (ctl: ' + this._ctl + ')');
    }

    if (ret) {
      this._ctl = null;
    }

    return ret;
  }

  _fillRect(x, y, width, height, sock, display, depth) {
    if (sock.rQwait('TIGHT', 3)) {
      return false;
    }

    const rQi = sock.rQi;
    const rQ = sock.rQ;

    display.fillRect(x, y, width, height, [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
    sock.rQskipBytes(3);

    return true;
  }

  _jpegRect(x, y, width, height, sock, display, depth) {
    let data = this._readData(sock);
    if (data === null) {
      return false;
    }

    display.imageRect(x, y, width, height, 'image/jpeg', data);

    return true;
  }

  _pngRect(x, y, width, height, sock, display, depth) {
    throw new Error('PNG received in standard Tight rect');
  }

  _basicRect(ctl, x, y, width, height, sock, display, depth) {
    if (this._filter === null) {
      if (ctl & 0x4) {
        if (sock.rQwait('TIGHT', 1)) {
          return false;
        }

        this._filter = sock.rQshift8();
      } else {
        // Implicit CopyFilter
        this._filter = 0;
      }
    }

    let streamId = ctl & 0x3;

    let ret;

    switch (this._filter) {
      case 0: // CopyFilter
        ret = this._copyFilter(streamId, x, y, width, height, sock, display, depth);
        break;
      case 1: // PaletteFilter
        ret = this._paletteFilter(streamId, x, y, width, height, sock, display, depth);
        break;
      case 2: // GradientFilter
        ret = this._gradientFilter(streamId, x, y, width, height, sock, display, depth);
        break;
      default:
        throw new Error('Illegal tight filter received (ctl: ' + this._filter + ')');
    }

    if (ret) {
      this._filter = null;
    }

    return ret;
  }

  _copyFilter(streamId, x, y, width, height, sock, display, depth) {
    const uncompressedSize = width * height * 3;
    let data;

    if (uncompressedSize === 0) {
      return true;
    }

    if (uncompressedSize < 12) {
      if (sock.rQwait('TIGHT', uncompressedSize)) {
        return false;
      }

      data = sock.rQshiftBytes(uncompressedSize);
    } else {
      data = this._readData(sock);
      if (data === null) {
        return false;
      }

      this._zlibs[streamId].setInput(data);
      data = this._zlibs[streamId].inflate(uncompressedSize);
      this._zlibs[streamId].setInput(null);
    }

    let rgbx = new Uint8Array(width * height * 4);
    for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
      rgbx[i] = data[j];
      rgbx[i + 1] = data[j + 1];
      rgbx[i + 2] = data[j + 2];
      rgbx[i + 3] = 255; // Alpha
    }

    display.blitImage(x, y, width, height, rgbx, 0, false);

    return true;
  }

  _paletteFilter(streamId, x, y, width, height, sock, display, depth) {
    if (this._numColors === 0) {
      if (sock.rQwait('TIGHT palette', 1)) {
        return false;
      }

      const numColors = sock.rQpeek8() + 1;
      const paletteSize = numColors * 3;

      if (sock.rQwait('TIGHT palette', 1 + paletteSize)) {
        return false;
      }

      this._numColors = numColors;
      sock.rQskipBytes(1);

      sock.rQshiftTo(this._palette, paletteSize);
    }

    const bpp = this._numColors <= 2 ? 1 : 8;
    const rowSize = Math.floor((width * bpp + 7) / 8);
    const uncompressedSize = rowSize * height;

    let data;

    if (uncompressedSize === 0) {
      return true;
    }

    if (uncompressedSize < 12) {
      if (sock.rQwait('TIGHT', uncompressedSize)) {
        return false;
      }

      data = sock.rQshiftBytes(uncompressedSize);
    } else {
      data = this._readData(sock);
      if (data === null) {
        return false;
      }

      this._zlibs[streamId].setInput(data);
      data = this._zlibs[streamId].inflate(uncompressedSize);
      this._zlibs[streamId].setInput(null);
    }

    // Convert indexed (palette based) image data to RGB
    if (this._numColors == 2) {
      this._monoRect(x, y, width, height, data, this._palette, display);
    } else {
      this._paletteRect(x, y, width, height, data, this._palette, display);
    }

    this._numColors = 0;

    return true;
  }

  _monoRect(x, y, width, height, data, palette, display) {
    // Convert indexed (palette based) image data to RGB
    // TODO: reduce number of calculations inside loop
    const dest = this._getScratchBuffer(width * height * 4);
    const w = Math.floor((width + 7) / 8);
    const w1 = Math.floor(width / 8);

    for (let y = 0; y < height; y++) {
      let dp, sp, x;
      for (x = 0; x < w1; x++) {
        for (let b = 7; b >= 0; b--) {
          dp = (y * width + x * 8 + 7 - b) * 4;
          sp = ((data[y * w + x] >> b) & 1) * 3;
          dest[dp] = palette[sp];
          dest[dp + 1] = palette[sp + 1];
          dest[dp + 2] = palette[sp + 2];
          dest[dp + 3] = 255;
        }
      }

      for (let b = 7; b >= 8 - (width % 8); b--) {
        dp = (y * width + x * 8 + 7 - b) * 4;
        sp = ((data[y * w + x] >> b) & 1) * 3;
        dest[dp] = palette[sp];
        dest[dp + 1] = palette[sp + 1];
        dest[dp + 2] = palette[sp + 2];
        dest[dp + 3] = 255;
      }
    }

    display.blitImage(x, y, width, height, dest, 0, false);
  }

  _paletteRect(x, y, width, height, data, palette, display) {
    // Convert indexed (palette based) image data to RGB
    const dest = this._getScratchBuffer(width * height * 4);
    const total = width * height * 4;
    for (let i = 0, j = 0; i < total; i += 4, j++) {
      const sp = data[j] * 3;
      dest[i] = palette[sp];
      dest[i + 1] = palette[sp + 1];
      dest[i + 2] = palette[sp + 2];
      dest[i + 3] = 255;
    }

    display.blitImage(x, y, width, height, dest, 0, false);
  }

  _gradientFilter(streamId, x, y, width, height, sock, display, depth) {
    throw new Error('Gradient filter not implemented');
  }

  _readData(sock) {
    if (this._len === 0) {
      if (sock.rQwait('TIGHT', 3)) {
        return null;
      }

      let byte;

      byte = sock.rQshift8();
      this._len = byte & 0x7f;
      if (byte & 0x80) {
        byte = sock.rQshift8();
        this._len |= (byte & 0x7f) << 7;
        if (byte & 0x80) {
          byte = sock.rQshift8();
          this._len |= byte << 14;
        }
      }
    }

    if (sock.rQwait('TIGHT', this._len)) {
      return null;
    }

    let data = sock.rQshiftBytes(this._len);
    this._len = 0;

    return data;
  }

  _getScratchBuffer(size) {
    if (!this._scratchBuffer || this._scratchBuffer.length < size) {
      this._scratchBuffer = new Uint8Array(size);
    }
    return this._scratchBuffer;
  }
}
