/* Copyright 2015 Google Inc. All Rights Reserved.

   Distributed under MIT license.
   See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
*/

package net.sourceforge.plantuml.brotli;

import java.io.IOException;
import java.io.InputStream;

/**
 * API for Brotli decompression.
 */
final class Decode {

  //----------------------------------------------------------------------------
  // RunningState
  //----------------------------------------------------------------------------
  private static final int UNINITIALIZED = 0;
  private static final int BLOCK_START = 1;
  private static final int COMPRESSED_BLOCK_START = 2;
  private static final int MAIN_LOOP = 3;
  private static final int READ_METADATA = 4;
  private static final int COPY_UNCOMPRESSED = 5;
  private static final int INSERT_LOOP = 6;
  private static final int COPY_LOOP = 7;
  private static final int COPY_WRAP_BUFFER = 8;
  private static final int TRANSFORM = 9;
  private static final int FINISHED = 10;
  private static final int CLOSED = 11;
  private static final int WRITE = 12;

  private static final int DEFAULT_CODE_LENGTH = 8;
  private static final int CODE_LENGTH_REPEAT_CODE = 16;
  private static final int NUM_LITERAL_CODES = 256;
  private static final int NUM_INSERT_AND_COPY_CODES = 704;
  private static final int NUM_BLOCK_LENGTH_CODES = 26;
  private static final int LITERAL_CONTEXT_BITS = 6;
  private static final int DISTANCE_CONTEXT_BITS = 2;

  private static final int HUFFMAN_TABLE_BITS = 8;
  private static final int HUFFMAN_TABLE_MASK = 0xFF;

  /**
   * Maximum possible Huffman table size for an alphabet size of 704, max code length 15 and root
   * table bits 8.
   */
  static final int HUFFMAN_TABLE_SIZE = 1080;

  private static final int CODE_LENGTH_CODES = 18;
  private static final int[] CODE_LENGTH_CODE_ORDER = {
      1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15,
  };

  private static final int NUM_DISTANCE_SHORT_CODES = 16;
  private static final int[] DISTANCE_SHORT_CODE_INDEX_OFFSET = {
      3, 2, 1, 0, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2
  };

  private static final int[] DISTANCE_SHORT_CODE_VALUE_OFFSET = {
      0, 0, 0, 0, -1, 1, -2, 2, -3, 3, -1, 1, -2, 2, -3, 3
  };

  /**
   * Static Huffman code for the code length code lengths.
   */
  private static final int[] FIXED_TABLE = {
      0x020000, 0x020004, 0x020003, 0x030002, 0x020000, 0x020004, 0x020003, 0x040001,
      0x020000, 0x020004, 0x020003, 0x030002, 0x020000, 0x020004, 0x020003, 0x040005
  };

  static final int[] DICTIONARY_OFFSETS_BY_LENGTH = {
    0, 0, 0, 0, 0, 4096, 9216, 21504, 35840, 44032, 53248, 63488, 74752, 87040, 93696, 100864,
    104704, 106752, 108928, 113536, 115968, 118528, 119872, 121280, 122016
  };

  static final int[] DICTIONARY_SIZE_BITS_BY_LENGTH = {
    0, 0, 0, 0, 10, 10, 11, 11, 10, 10, 10, 10, 10, 9, 9, 8, 7, 7, 8, 7, 7, 6, 6, 5, 5
  };

  static final int MIN_WORD_LENGTH = 4;

  static final int MAX_WORD_LENGTH = 24;

  static final int MAX_TRANSFORMED_WORD_LENGTH = 5 + MAX_WORD_LENGTH + 8;

  //----------------------------------------------------------------------------
  // Prefix code LUT.
  //----------------------------------------------------------------------------
  static final int[] BLOCK_LENGTH_OFFSET = {
      1, 5, 9, 13, 17, 25, 33, 41, 49, 65, 81, 97, 113, 145, 177, 209, 241, 305, 369, 497,
      753, 1265, 2289, 4337, 8433, 16625
  };

  static final int[] BLOCK_LENGTH_N_BITS = {
      2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12, 13, 24
  };

  static final int[] INSERT_LENGTH_OFFSET = {
      0, 1, 2, 3, 4, 5, 6, 8, 10, 14, 18, 26, 34, 50, 66, 98, 130, 194, 322, 578, 1090, 2114, 6210,
      22594
  };

  static final int[] INSERT_LENGTH_N_BITS = {
      0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 12, 14, 24
  };

  static final int[] COPY_LENGTH_OFFSET = {
      2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 18, 22, 30, 38, 54, 70, 102, 134, 198, 326, 582, 1094,
      2118
  };

  static final int[] COPY_LENGTH_N_BITS = {
      0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 24
  };

  static final int[] INSERT_RANGE_LUT = {
      0, 0, 8, 8, 0, 16, 8, 16, 16
  };

  static final int[] COPY_RANGE_LUT = {
      0, 8, 0, 8, 16, 0, 16, 8, 16
  };

  private static int decodeWindowBits(State s) {
    BitReader.fillBitWindow(s);
    if (BitReader.readFewBits(s, 1) == 0) {
      return 16;
    }
    int n = BitReader.readFewBits(s, 3);
    if (n != 0) {
      return 17 + n;
    }
    n = BitReader.readFewBits(s, 3);
    if (n != 0) {
      return 8 + n;
    }
    return 17;
  }

  /**
   * Associate input with decoder state.
   *
   * @param s uninitialized state without associated input
   * @param input compressed data source
   */
  static void initState(State s, InputStream input) {
    if (s.runningState != UNINITIALIZED) {
      throw new IllegalStateException("State MUST be uninitialized");
    }
    s.blockTrees = new int[6 * HUFFMAN_TABLE_SIZE];
    s.input = input;
    BitReader.initBitReader(s);
    int windowBits = decodeWindowBits(s);
    if (windowBits == 9) { /* Reserved case for future expansion. */
      throw new BrotliRuntimeException("Invalid 'windowBits' code");
    }
    s.maxRingBufferSize = 1 << windowBits;
    s.maxBackwardDistance = s.maxRingBufferSize - 16;
    s.runningState = BLOCK_START;
  }

  static void close(State s) throws IOException {
    if (s.runningState == UNINITIALIZED) {
      throw new IllegalStateException("State MUST be initialized");
    }
    if (s.runningState == CLOSED) {
      return;
    }
    s.runningState = CLOSED;
    if (s.input != null) {
      Utils.closeInput(s.input);
      s.input = null;
    }
  }

  /**
   * Decodes a number in the range [0..255], by reading 1 - 11 bits.
   */
  private static int decodeVarLenUnsignedByte(State s) {
    BitReader.fillBitWindow(s);
    if (BitReader.readFewBits(s, 1) != 0) {
      int n = BitReader.readFewBits(s, 3);
      if (n == 0) {
        return 1;
      } else {
        return BitReader.readFewBits(s, n) + (1 << n);
      }
    }
    return 0;
  }

  private static void decodeMetaBlockLength(State s) {
    BitReader.fillBitWindow(s);
    s.inputEnd = BitReader.readFewBits(s, 1);
    s.metaBlockLength = 0;
    s.isUncompressed = 0;
    s.isMetadata = 0;
    if ((s.inputEnd != 0) && BitReader.readFewBits(s, 1) != 0) {
      return;
    }
    int sizeNibbles = BitReader.readFewBits(s, 2) + 4;
    if (sizeNibbles == 7) {
      s.isMetadata = 1;
      if (BitReader.readFewBits(s, 1) != 0) {
        throw new BrotliRuntimeException("Corrupted reserved bit");
      }
      int sizeBytes = BitReader.readFewBits(s, 2);
      if (sizeBytes == 0) {
        return;
      }
      for (int i = 0; i < sizeBytes; i++) {
        BitReader.fillBitWindow(s);
        int bits = BitReader.readFewBits(s, 8);
        if (bits == 0 && i + 1 == sizeBytes && sizeBytes > 1) {
          throw new BrotliRuntimeException("Exuberant nibble");
        }
        s.metaBlockLength |= bits << (i * 8);
      }
    } else {
      for (int i = 0; i < sizeNibbles; i++) {
        BitReader.fillBitWindow(s);
        int bits = BitReader.readFewBits(s, 4);
        if (bits == 0 && i + 1 == sizeNibbles && sizeNibbles > 4) {
          throw new BrotliRuntimeException("Exuberant nibble");
        }
        s.metaBlockLength |= bits << (i * 4);
      }
    }
    s.metaBlockLength++;
    if (s.inputEnd == 0) {
      s.isUncompressed = BitReader.readFewBits(s, 1);
    }
  }

  /**
   * Decodes the next Huffman code from bit-stream.
   */
  private static int readSymbol(int[] table, int offset, State s) {
    int val = BitReader.peekBits(s);
    offset += val & HUFFMAN_TABLE_MASK;
    int bits = table[offset] >> 16;
    int sym = table[offset] & 0xFFFF;
    if (bits <= HUFFMAN_TABLE_BITS) {
      s.bitOffset += bits;
      return sym;
    }
    offset += sym;
    int mask = (1 << bits) - 1;
    offset += (val & mask) >>> HUFFMAN_TABLE_BITS;
    s.bitOffset += ((table[offset] >> 16) + HUFFMAN_TABLE_BITS);
    return table[offset] & 0xFFFF;
  }

  private static int readBlockLength(int[] table, int offset, State s) {
    BitReader.fillBitWindow(s);
    int code = readSymbol(table, offset, s);
    int n = BLOCK_LENGTH_N_BITS[code];
    BitReader.fillBitWindow(s);
    return BLOCK_LENGTH_OFFSET[code] + BitReader.readBits(s, n);
  }

  private static int translateShortCodes(int code, int[] ringBuffer, int index) {
    if (code < NUM_DISTANCE_SHORT_CODES) {
      index += DISTANCE_SHORT_CODE_INDEX_OFFSET[code];
      index &= 3;
      return ringBuffer[index] + DISTANCE_SHORT_CODE_VALUE_OFFSET[code];
    }
    return code - NUM_DISTANCE_SHORT_CODES + 1;
  }

  private static void moveToFront(int[] v, int index) {
    int value = v[index];
    for (; index > 0; index--) {
      v[index] = v[index - 1];
    }
    v[0] = value;
  }

  private static void inverseMoveToFrontTransform(byte[] v, int vLen) {
    int[] mtf = new int[256];
    for (int i = 0; i < 256; i++) {
      mtf[i] = i;
    }
    for (int i = 0; i < vLen; i++) {
      int index = v[i] & 0xFF;
      v[i] = (byte) mtf[index];
      if (index != 0) {
        moveToFront(mtf, index);
      }
    }
  }

  private static void readHuffmanCodeLengths(
      int[] codeLengthCodeLengths, int numSymbols, int[] codeLengths, State s) {
    int symbol = 0;
    int prevCodeLen = DEFAULT_CODE_LENGTH;
    int repeat = 0;
    int repeatCodeLen = 0;
    int space = 32768;
    int[] table = new int[32];

    Huffman.buildHuffmanTable(table, 0, 5, codeLengthCodeLengths, CODE_LENGTH_CODES);

    while (symbol < numSymbols && space > 0) {
      BitReader.readMoreInput(s);
      BitReader.fillBitWindow(s);
      int p = BitReader.peekBits(s) & 31;
      s.bitOffset += table[p] >> 16;
      int codeLen = table[p] & 0xFFFF;
      if (codeLen < CODE_LENGTH_REPEAT_CODE) {
        repeat = 0;
        codeLengths[symbol++] = codeLen;
        if (codeLen != 0) {
          prevCodeLen = codeLen;
          space -= 32768 >> codeLen;
        }
      } else {
        int extraBits = codeLen - 14;
        int newLen = 0;
        if (codeLen == CODE_LENGTH_REPEAT_CODE) {
          newLen = prevCodeLen;
        }
        if (repeatCodeLen != newLen) {
          repeat = 0;
          repeatCodeLen = newLen;
        }
        int oldRepeat = repeat;
        if (repeat > 0) {
          repeat -= 2;
          repeat <<= extraBits;
        }
        BitReader.fillBitWindow(s);
        repeat += BitReader.readFewBits(s, extraBits) + 3;
        int repeatDelta = repeat - oldRepeat;
        if (symbol + repeatDelta > numSymbols) {
          throw new BrotliRuntimeException("symbol + repeatDelta > numSymbols"); // COV_NF_LINE
        }
        for (int i = 0; i < repeatDelta; i++) {
          codeLengths[symbol++] = repeatCodeLen;
        }
        if (repeatCodeLen != 0) {
          space -= repeatDelta << (15 - repeatCodeLen);
        }
      }
    }
    if (space != 0) {
      throw new BrotliRuntimeException("Unused space"); // COV_NF_LINE
    }
    // TODO: Pass max_symbol to Huffman table builder instead?
    Utils.fillIntsWithZeroes(codeLengths, symbol, numSymbols);
  }

  static int checkDupes(int[] symbols, int length) {
    for (int i = 0; i < length - 1; ++i) {
      for (int j = i + 1; j < length; ++j) {
        if (symbols[i] == symbols[j]) {
          return 0;
        }
      }
    }
    return 1;
  }

  // TODO: Use specialized versions for smaller tables.
  static void readHuffmanCode(int alphabetSize, int[] table, int offset, State s) {
    int ok = 1;
    int simpleCodeOrSkip;
    BitReader.readMoreInput(s);
    // TODO: Avoid allocation.
    int[] codeLengths = new int[alphabetSize];
    BitReader.fillBitWindow(s);
    simpleCodeOrSkip = BitReader.readFewBits(s, 2);
    if (simpleCodeOrSkip == 1) { // Read symbols, codes & code lengths directly.
      int maxBitsCounter = alphabetSize - 1;
      int maxBits = 0;
      int[] symbols = new int[4];
      int numSymbols = BitReader.readFewBits(s, 2) + 1;
      while (maxBitsCounter != 0) {
        maxBitsCounter >>= 1;
        maxBits++;
      }
      // TODO: uncomment when codeLengths is reused.
      // Utils.fillWithZeroes(codeLengths, 0, alphabetSize);
      for (int i = 0; i < numSymbols; i++) {
        BitReader.fillBitWindow(s);
        symbols[i] = BitReader.readFewBits(s, maxBits) % alphabetSize;
        codeLengths[symbols[i]] = 2;
      }
      codeLengths[symbols[0]] = 1;
      switch (numSymbols) {
        case 2:
          codeLengths[symbols[1]] = 1;
          break;
        case 4:
          if (BitReader.readFewBits(s, 1) == 1) {
            codeLengths[symbols[2]] = 3;
            codeLengths[symbols[3]] = 3;
          } else {
            codeLengths[symbols[0]] = 2;
          }
          break;
        default:
          break;
      }
      ok = checkDupes(symbols, numSymbols);
    } else { // Decode Huffman-coded code lengths.
      int[] codeLengthCodeLengths = new int[CODE_LENGTH_CODES];
      int space = 32;
      int numCodes = 0;
      for (int i = simpleCodeOrSkip; i < CODE_LENGTH_CODES && space > 0; i++) {
        int codeLenIdx = CODE_LENGTH_CODE_ORDER[i];
        BitReader.fillBitWindow(s);
        int p = BitReader.peekBits(s) & 15;
        // TODO: Demultiplex FIXED_TABLE.
        s.bitOffset += FIXED_TABLE[p] >> 16;
        int v = FIXED_TABLE[p] & 0xFFFF;
        codeLengthCodeLengths[codeLenIdx] = v;
        if (v != 0) {
          space -= (32 >> v);
          numCodes++;
        }
      }
      if (space != 0 && numCodes != 1) {
        ok = 0;
      }
      readHuffmanCodeLengths(codeLengthCodeLengths, alphabetSize, codeLengths, s);
    }
    if (ok == 0) {
      throw new BrotliRuntimeException("Can't readHuffmanCode"); // COV_NF_LINE
    }
    Huffman.buildHuffmanTable(table, offset, HUFFMAN_TABLE_BITS, codeLengths, alphabetSize);
  }

  private static int decodeContextMap(int contextMapSize, byte[] contextMap, State s) {
    BitReader.readMoreInput(s);
    int numTrees = decodeVarLenUnsignedByte(s) + 1;

    if (numTrees == 1) {
      Utils.fillBytesWithZeroes(contextMap, 0, contextMapSize);
      return numTrees;
    }

    BitReader.fillBitWindow(s);
    int useRleForZeros = BitReader.readFewBits(s, 1);
    int maxRunLengthPrefix = 0;
    if (useRleForZeros != 0) {
      maxRunLengthPrefix = BitReader.readFewBits(s, 4) + 1;
    }
    int[] table = new int[HUFFMAN_TABLE_SIZE];
    readHuffmanCode(numTrees + maxRunLengthPrefix, table, 0, s);
    for (int i = 0; i < contextMapSize; ) {
      BitReader.readMoreInput(s);
      BitReader.fillBitWindow(s);
      int code = readSymbol(table, 0, s);
      if (code == 0) {
        contextMap[i] = 0;
        i++;
      } else if (code <= maxRunLengthPrefix) {
        BitReader.fillBitWindow(s);
        int reps = (1 << code) + BitReader.readFewBits(s, code);
        while (reps != 0) {
          if (i >= contextMapSize) {
            throw new BrotliRuntimeException("Corrupted context map"); // COV_NF_LINE
          }
          contextMap[i] = 0;
          i++;
          reps--;
        }
      } else {
        contextMap[i] = (byte) (code - maxRunLengthPrefix);
        i++;
      }
    }
    BitReader.fillBitWindow(s);
    if (BitReader.readFewBits(s, 1) == 1) {
      inverseMoveToFrontTransform(contextMap, contextMapSize);
    }
    return numTrees;
  }

  private static int decodeBlockTypeAndLength(State s, int treeType, int numBlockTypes) {
    final int[] ringBuffers = s.rings;
    final int offset = 4 + treeType * 2;
    BitReader.fillBitWindow(s);
    int blockType = readSymbol(s.blockTrees, treeType * HUFFMAN_TABLE_SIZE, s);
    int result = readBlockLength(s.blockTrees, (treeType + 3) * HUFFMAN_TABLE_SIZE, s);

    if (blockType == 1) {
      blockType = ringBuffers[offset + 1] + 1;
    } else if (blockType == 0) {
      blockType = ringBuffers[offset];
    } else {
      blockType -= 2;
    }
    if (blockType >= numBlockTypes) {
      blockType -= numBlockTypes;
    }
    ringBuffers[offset] = ringBuffers[offset + 1];
    ringBuffers[offset + 1] = blockType;
    return result;
  }

  private static void decodeLiteralBlockSwitch(State s) {
    s.literalBlockLength = decodeBlockTypeAndLength(s, 0, s.numLiteralBlockTypes);
    int literalBlockType = s.rings[5];
    s.contextMapSlice = literalBlockType << LITERAL_CONTEXT_BITS;
    s.literalTreeIndex = s.contextMap[s.contextMapSlice] & 0xFF;
    s.literalTree = s.hGroup0[s.literalTreeIndex];
    int contextMode = s.contextModes[literalBlockType];
    s.contextLookupOffset1 = contextMode << 9;
    s.contextLookupOffset2 = s.contextLookupOffset1 + 256;
  }

  private static void decodeCommandBlockSwitch(State s) {
    s.commandBlockLength = decodeBlockTypeAndLength(s, 1, s.numCommandBlockTypes);
    s.treeCommandOffset = s.hGroup1[s.rings[7]];
  }

  private static void decodeDistanceBlockSwitch(State s) {
    s.distanceBlockLength = decodeBlockTypeAndLength(s, 2, s.numDistanceBlockTypes);
    s.distContextMapSlice = s.rings[9] << DISTANCE_CONTEXT_BITS;
  }

  private static void maybeReallocateRingBuffer(State s) {
    int newSize = s.maxRingBufferSize;
    if (newSize > s.expectedTotalSize) {
      /* TODO: Handle 2GB+ cases more gracefully. */
      int minimalNewSize = s.expectedTotalSize;
      while ((newSize >> 1) > minimalNewSize) {
        newSize >>= 1;
      }
      if ((s.inputEnd == 0) && newSize < 16384 && s.maxRingBufferSize >= 16384) {
        newSize = 16384;
      }
    }
    if (newSize <= s.ringBufferSize) {
      return;
    }
    int ringBufferSizeWithSlack = newSize + MAX_TRANSFORMED_WORD_LENGTH;
    byte[] newBuffer = new byte[ringBufferSizeWithSlack];
    if (s.ringBuffer.length != 0) {
      System.arraycopy(s.ringBuffer, 0, newBuffer, 0, s.ringBufferSize);
    }
    s.ringBuffer = newBuffer;
    s.ringBufferSize = newSize;
  }

  private static void readNextMetablockHeader(State s) {
    if (s.inputEnd != 0) {
      s.nextRunningState = FINISHED;
      s.bytesToWrite = s.pos;
      s.bytesWritten = 0;
      s.runningState = WRITE;
      return;
    }
    // TODO: Reset? Do we need this?
    s.hGroup0 = new int[0];
    s.hGroup1 = new int[0];
    s.hGroup2 = new int[0];

    BitReader.readMoreInput(s);
    decodeMetaBlockLength(s);
    if ((s.metaBlockLength == 0) && (s.isMetadata == 0)) {
      return;
    }
    if ((s.isUncompressed != 0) || (s.isMetadata != 0)) {
      BitReader.jumpToByteBoundary(s);
      s.runningState = (s.isMetadata != 0) ? READ_METADATA : COPY_UNCOMPRESSED;
    } else {
      s.runningState = COMPRESSED_BLOCK_START;
    }

    if (s.isMetadata != 0) {
      return;
    }
    s.expectedTotalSize += s.metaBlockLength;
    if (s.expectedTotalSize > 1 << 30) {
      s.expectedTotalSize = 1 << 30;
    }
    if (s.ringBufferSize < s.maxRingBufferSize) {
      maybeReallocateRingBuffer(s);
    }
  }

  private static int readMetablockPartition(State s, int treeType, int numBlockTypes) {
    if (numBlockTypes <= 1) {
      return 1 << 28;
    }
    readHuffmanCode(numBlockTypes + 2, s.blockTrees, treeType * HUFFMAN_TABLE_SIZE, s);
    readHuffmanCode(NUM_BLOCK_LENGTH_CODES, s.blockTrees, (treeType + 3) * HUFFMAN_TABLE_SIZE, s);
    return readBlockLength(s.blockTrees, (treeType + 3) * HUFFMAN_TABLE_SIZE, s);
  }

  private static void readMetablockHuffmanCodesAndContextMaps(State s) {
    s.numLiteralBlockTypes = decodeVarLenUnsignedByte(s) + 1;
    s.literalBlockLength = readMetablockPartition(s, 0, s.numLiteralBlockTypes);
    s.numCommandBlockTypes = decodeVarLenUnsignedByte(s) + 1;
    s.commandBlockLength = readMetablockPartition(s, 1, s.numCommandBlockTypes);
    s.numDistanceBlockTypes = decodeVarLenUnsignedByte(s) + 1;
    s.distanceBlockLength = readMetablockPartition(s, 2, s.numDistanceBlockTypes);

    BitReader.readMoreInput(s);
    BitReader.fillBitWindow(s);
    s.distancePostfixBits = BitReader.readFewBits(s, 2);
    s.numDirectDistanceCodes =
        NUM_DISTANCE_SHORT_CODES + (BitReader.readFewBits(s, 4) << s.distancePostfixBits);
    s.distancePostfixMask = (1 << s.distancePostfixBits) - 1;
    int numDistanceCodes = s.numDirectDistanceCodes + (48 << s.distancePostfixBits);
    // TODO: Reuse?
    s.contextModes = new byte[s.numLiteralBlockTypes];
    for (int i = 0; i < s.numLiteralBlockTypes;) {
      /* Ensure that less than 256 bits read between readMoreInput. */
      int limit = Math.min(i + 96, s.numLiteralBlockTypes);
      for (; i < limit; ++i) {
        BitReader.fillBitWindow(s);
        s.contextModes[i] = (byte) (BitReader.readFewBits(s, 2));
      }
      BitReader.readMoreInput(s);
    }

    // TODO: Reuse?
    s.contextMap = new byte[s.numLiteralBlockTypes << LITERAL_CONTEXT_BITS];
    int numLiteralTrees = decodeContextMap(s.numLiteralBlockTypes << LITERAL_CONTEXT_BITS,
        s.contextMap, s);
    s.trivialLiteralContext = 1;
    for (int j = 0; j < s.numLiteralBlockTypes << LITERAL_CONTEXT_BITS; j++) {
      if (s.contextMap[j] != j >> LITERAL_CONTEXT_BITS) {
        s.trivialLiteralContext = 0;
        break;
      }
    }

    // TODO: Reuse?
    s.distContextMap = new byte[s.numDistanceBlockTypes << DISTANCE_CONTEXT_BITS];
    int numDistTrees = decodeContextMap(s.numDistanceBlockTypes << DISTANCE_CONTEXT_BITS,
        s.distContextMap, s);

    s.hGroup0 = decodeHuffmanTreeGroup(NUM_LITERAL_CODES, numLiteralTrees, s);
    s.hGroup1 =
        decodeHuffmanTreeGroup(NUM_INSERT_AND_COPY_CODES, s.numCommandBlockTypes, s);
    s.hGroup2 = decodeHuffmanTreeGroup(numDistanceCodes, numDistTrees, s);

    s.contextMapSlice = 0;
    s.distContextMapSlice = 0;
    s.contextLookupOffset1 = (int) (s.contextModes[0]) << 9;
    s.contextLookupOffset2 = s.contextLookupOffset1 + 256;
    s.literalTreeIndex = 0;
    s.literalTree = s.hGroup0[0];
    s.treeCommandOffset = s.hGroup1[0];

    s.rings[4] = 1;
    s.rings[5] = 0;
    s.rings[6] = 1;
    s.rings[7] = 0;
    s.rings[8] = 1;
    s.rings[9] = 0;
  }

  private static void copyUncompressedData(State s) {
    final byte[] ringBuffer = s.ringBuffer;

    // Could happen if block ends at ring buffer end.
    if (s.metaBlockLength <= 0) {
      BitReader.reload(s);
      s.runningState = BLOCK_START;
      return;
    }

    int chunkLength = Math.min(s.ringBufferSize - s.pos, s.metaBlockLength);
    BitReader.copyBytes(s, ringBuffer, s.pos, chunkLength);
    s.metaBlockLength -= chunkLength;
    s.pos += chunkLength;
    if (s.pos == s.ringBufferSize) {
        s.nextRunningState = COPY_UNCOMPRESSED;
        s.bytesToWrite = s.ringBufferSize;
        s.bytesWritten = 0;
        s.runningState = WRITE;
        return;
      }

    BitReader.reload(s);
    s.runningState = BLOCK_START;
  }

  private static int writeRingBuffer(State s) {
    int toWrite = Math.min(s.outputLength - s.outputUsed,
        s.bytesToWrite - s.bytesWritten);
    if (toWrite != 0) {
      System.arraycopy(s.ringBuffer, s.bytesWritten, s.output,
          s.outputOffset + s.outputUsed, toWrite);
      s.outputUsed += toWrite;
      s.bytesWritten += toWrite;
    }

    if (s.outputUsed < s.outputLength) {
      return 1;
    } else {
      return 0;
    }
  }

  private static int[] decodeHuffmanTreeGroup(int alphabetSize, int n, State s) {
    int[] group = new int[n + (n * HUFFMAN_TABLE_SIZE)];
    int next = n;
    for (int i = 0; i < n; i++) {
      group[i] = next;
      Decode.readHuffmanCode(alphabetSize, group, next, s);
      next += HUFFMAN_TABLE_SIZE;
    }
    return group;
  }

  /**
   * Actual decompress implementation.
   */
  static void decompress(State s) {
    if (s.runningState == UNINITIALIZED) {
      throw new IllegalStateException("Can't decompress until initialized");
    }
    if (s.runningState == CLOSED) {
      throw new IllegalStateException("Can't decompress after close");
    }
    int ringBufferMask = s.ringBufferSize - 1;
    byte[] ringBuffer = s.ringBuffer;

    while (s.runningState != FINISHED) {
      // TODO: extract cases to methods for the better readability.
      switch (s.runningState) {
        case BLOCK_START:
          if (s.metaBlockLength < 0) {
            throw new BrotliRuntimeException("Invalid metablock length");
          }
          readNextMetablockHeader(s);
          /* Ring-buffer would be reallocated here. */
          ringBufferMask = s.ringBufferSize - 1;
          ringBuffer = s.ringBuffer;
          continue;

        case COMPRESSED_BLOCK_START:
          readMetablockHuffmanCodesAndContextMaps(s);
          s.runningState = MAIN_LOOP;
          // Fall through

        case MAIN_LOOP:
          if (s.metaBlockLength <= 0) {
            s.runningState = BLOCK_START;
            continue;
          }
          BitReader.readMoreInput(s);
          if (s.commandBlockLength == 0) {
            decodeCommandBlockSwitch(s);
          }
          s.commandBlockLength--;
          BitReader.fillBitWindow(s);
          int cmdCode = readSymbol(s.hGroup1, s.treeCommandOffset, s);
          int rangeIdx = cmdCode >>> 6;
          s.distanceCode = 0;
          if (rangeIdx >= 2) {
            rangeIdx -= 2;
            s.distanceCode = -1;
          }
          int insertCode = INSERT_RANGE_LUT[rangeIdx] + ((cmdCode >>> 3) & 7);
          BitReader.fillBitWindow(s);
          int insertBits = INSERT_LENGTH_N_BITS[insertCode];
          int insertExtra = BitReader.readBits(s, insertBits);
          s.insertLength = INSERT_LENGTH_OFFSET[insertCode] + insertExtra;
          int copyCode = COPY_RANGE_LUT[rangeIdx] + (cmdCode & 7);
          BitReader.fillBitWindow(s);
          int copyBits = COPY_LENGTH_N_BITS[copyCode];
          int copyExtra = BitReader.readBits(s, copyBits);
          s.copyLength = COPY_LENGTH_OFFSET[copyCode] + copyExtra;

          s.j = 0;
          s.runningState = INSERT_LOOP;

          // Fall through
        case INSERT_LOOP:
          if (s.trivialLiteralContext != 0) {
            while (s.j < s.insertLength) {
              BitReader.readMoreInput(s);
              if (s.literalBlockLength == 0) {
                decodeLiteralBlockSwitch(s);
              }
              s.literalBlockLength--;
              BitReader.fillBitWindow(s);
              ringBuffer[s.pos] =
                  (byte) readSymbol(s.hGroup0, s.literalTree, s);
              s.j++;
              if (s.pos++ == ringBufferMask) {
                s.nextRunningState = INSERT_LOOP;
                s.bytesToWrite = s.ringBufferSize;
                s.bytesWritten = 0;
                s.runningState = WRITE;
                break;
              }
            }
          } else {
            int prevByte1 = ringBuffer[(s.pos - 1) & ringBufferMask] & 0xFF;
            int prevByte2 = ringBuffer[(s.pos - 2) & ringBufferMask] & 0xFF;
            while (s.j < s.insertLength) {
              BitReader.readMoreInput(s);
              if (s.literalBlockLength == 0) {
                decodeLiteralBlockSwitch(s);
              }
              int literalTreeIndex = s.contextMap[s.contextMapSlice
                + (Context.LOOKUP[s.contextLookupOffset1 + prevByte1]
                    | Context.LOOKUP[s.contextLookupOffset2 + prevByte2])] & 0xFF;
              s.literalBlockLength--;
              prevByte2 = prevByte1;
              BitReader.fillBitWindow(s);
              prevByte1 = readSymbol(
                  s.hGroup0, s.hGroup0[literalTreeIndex], s);
              ringBuffer[s.pos] = (byte) prevByte1;
              s.j++;
              if (s.pos++ == ringBufferMask) {
                s.nextRunningState = INSERT_LOOP;
                s.bytesToWrite = s.ringBufferSize;
                s.bytesWritten = 0;
                s.runningState = WRITE;
                break;
              }
            }
          }
          if (s.runningState != INSERT_LOOP) {
            continue;
          }
          s.metaBlockLength -= s.insertLength;
          if (s.metaBlockLength <= 0) {
            s.runningState = MAIN_LOOP;
            continue;
          }
          if (s.distanceCode < 0) {
            BitReader.readMoreInput(s);
            if (s.distanceBlockLength == 0) {
              decodeDistanceBlockSwitch(s);
            }
            s.distanceBlockLength--;
            BitReader.fillBitWindow(s);
            s.distanceCode = readSymbol(s.hGroup2, s.hGroup2[
                s.distContextMap[s.distContextMapSlice
                    + (s.copyLength > 4 ? 3 : s.copyLength - 2)] & 0xFF], s);
            if (s.distanceCode >= s.numDirectDistanceCodes) {
              s.distanceCode -= s.numDirectDistanceCodes;
              int postfix = s.distanceCode & s.distancePostfixMask;
              s.distanceCode >>>= s.distancePostfixBits;
              int n = (s.distanceCode >>> 1) + 1;
              int offset = ((2 + (s.distanceCode & 1)) << n) - 4;
              BitReader.fillBitWindow(s);
              int distanceExtra = BitReader.readBits(s, n);
              s.distanceCode = s.numDirectDistanceCodes + postfix
                  + ((offset + distanceExtra) << s.distancePostfixBits);
            }
          }

          // Convert the distance code to the actual distance by possibly looking up past distances
          // from the ringBuffer.
          s.distance = translateShortCodes(s.distanceCode, s.rings, s.distRbIdx);
          if (s.distance < 0) {
            throw new BrotliRuntimeException("Negative distance"); // COV_NF_LINE
          }

          if (s.maxDistance != s.maxBackwardDistance
              && s.pos < s.maxBackwardDistance) {
            s.maxDistance = s.pos;
          } else {
            s.maxDistance = s.maxBackwardDistance;
          }

          s.copyDst = s.pos;
          if (s.distance > s.maxDistance) {
            s.runningState = TRANSFORM;
            continue;
          }

          if (s.distanceCode > 0) {
            s.rings[s.distRbIdx & 3] = s.distance;
            s.distRbIdx++;
          }

          if (s.copyLength > s.metaBlockLength) {
            throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
          }
          s.j = 0;
          s.runningState = COPY_LOOP;
          // fall through
        case COPY_LOOP:
          int src = (s.pos - s.distance) & ringBufferMask;
          int dst = s.pos;
          int copyLength = s.copyLength - s.j;
          int srcEnd = src + copyLength;
          int dstEnd = dst + copyLength;
          if ((srcEnd < ringBufferMask) && (dstEnd < ringBufferMask)) {
            if (copyLength < 12 || (srcEnd > dst && dstEnd > src)) {
              for (int k = 0; k < copyLength; ++k) {
                ringBuffer[dst++] = ringBuffer[src++];
              }
            } else {
              Utils.copyBytesWithin(ringBuffer, dst, src, srcEnd);
            }
            s.j += copyLength;
            s.metaBlockLength -= copyLength;
            s.pos += copyLength;
          } else {
            for (; s.j < s.copyLength;) {
              ringBuffer[s.pos] =
                  ringBuffer[(s.pos - s.distance) & ringBufferMask];
              s.metaBlockLength--;
              s.j++;
              if (s.pos++ == ringBufferMask) {
                s.nextRunningState = COPY_LOOP;
                s.bytesToWrite = s.ringBufferSize;
                s.bytesWritten = 0;
                s.runningState = WRITE;
                break;
              }
            }
          }
          if (s.runningState == COPY_LOOP) {
            s.runningState = MAIN_LOOP;
          }
          continue;

        case TRANSFORM:
          if (s.copyLength >= MIN_WORD_LENGTH
              && s.copyLength <= MAX_WORD_LENGTH) {
            int offset = DICTIONARY_OFFSETS_BY_LENGTH[s.copyLength];
            int wordId = s.distance - s.maxDistance - 1;
            int shift = DICTIONARY_SIZE_BITS_BY_LENGTH[s.copyLength];
            int mask = (1 << shift) - 1;
            int wordIdx = wordId & mask;
            int transformIdx = wordId >>> shift;
            offset += wordIdx * s.copyLength;
            if (transformIdx < Transform.NUM_TRANSFORMS) {
              int len = Transform.transformDictionaryWord(ringBuffer, s.copyDst,
                  Dictionary.getData(), offset, s.copyLength, transformIdx);
              s.copyDst += len;
              s.pos += len;
              s.metaBlockLength -= len;
              if (s.copyDst >= s.ringBufferSize) {
                s.nextRunningState = COPY_WRAP_BUFFER;
                s.bytesToWrite = s.ringBufferSize;
                s.bytesWritten = 0;
                s.runningState = WRITE;
                continue;
              }
            } else {
              throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
            }
          } else {
            throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
          }
          s.runningState = MAIN_LOOP;
          continue;

        case COPY_WRAP_BUFFER:
          Utils.copyBytesWithin(ringBuffer, 0, s.ringBufferSize, s.copyDst);
          s.runningState = MAIN_LOOP;
          continue;

        case READ_METADATA:
          while (s.metaBlockLength > 0) {
            BitReader.readMoreInput(s);
            // Optimize
            BitReader.fillBitWindow(s);
            BitReader.readFewBits(s, 8);
            s.metaBlockLength--;
          }
          s.runningState = BLOCK_START;
          continue;


        case COPY_UNCOMPRESSED:
          copyUncompressedData(s);
          continue;

        case WRITE:
          if (writeRingBuffer(s) == 0) {
            // Output buffer is full.
            return;
          }
          if (s.pos >= s.maxBackwardDistance) {
            s.maxDistance = s.maxBackwardDistance;
          }
          s.pos &= ringBufferMask;
          s.runningState = s.nextRunningState;
          continue;

        default:
          throw new BrotliRuntimeException("Unexpected state " + s.runningState);
      }
    }
    if (s.runningState == FINISHED) {
      if (s.metaBlockLength < 0) {
        throw new BrotliRuntimeException("Invalid metablock length");
      }
      BitReader.jumpToByteBoundary(s);
      BitReader.checkHealth(s, 1);
    }
  }
}
