/* 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 org.brotli.dec;

import static org.brotli.dec.RunningState.BLOCK_START;
import static org.brotli.dec.RunningState.CLOSED;
import static org.brotli.dec.RunningState.COMPRESSED_BLOCK_START;
import static org.brotli.dec.RunningState.COPY_LOOP;
import static org.brotli.dec.RunningState.COPY_UNCOMPRESSED;
import static org.brotli.dec.RunningState.COPY_WRAP_BUFFER;
import static org.brotli.dec.RunningState.FINISHED;
import static org.brotli.dec.RunningState.INSERT_LOOP;
import static org.brotli.dec.RunningState.MAIN_LOOP;
import static org.brotli.dec.RunningState.READ_METADATA;
import static org.brotli.dec.RunningState.TRANSFORM;
import static org.brotli.dec.RunningState.UNINITIALIZED;
import static org.brotli.dec.RunningState.WRITE;

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

  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;

  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
  };

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

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

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

  private static int readBlockLength(int[] table, int offset, BitReader br) {
    BitReader.fillBitWindow(br);
    int code = readSymbol(table, offset, br);
    int n = Prefix.BLOCK_LENGTH_N_BITS[code];
    return Prefix.BLOCK_LENGTH_OFFSET[code] + BitReader.readBits(br, 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, BitReader br) {
    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(br);
      BitReader.fillBitWindow(br);
      int p = (int) ((br.accumulator >>> br.bitOffset)) & 31;
      br.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;
        }
        repeat += BitReader.readBits(br, 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.fillWithZeroes(codeLengths, symbol, numSymbols - symbol);
  }

  // TODO: Use specialized versions for smaller tables.
  static void readHuffmanCode(int alphabetSize, int[] table, int offset, BitReader br) {
    boolean ok = true;
    int simpleCodeOrSkip;
    BitReader.readMoreInput(br);
    // TODO: Avoid allocation.
    int[] codeLengths = new int[alphabetSize];
    simpleCodeOrSkip = BitReader.readBits(br, 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.readBits(br, 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++) {
        symbols[i] = BitReader.readBits(br, maxBits) % alphabetSize;
        codeLengths[symbols[i]] = 2;
      }
      codeLengths[symbols[0]] = 1;
      switch (numSymbols) {
        case 1:
          break;
        case 2:
          ok = symbols[0] != symbols[1];
          codeLengths[symbols[1]] = 1;
          break;
        case 3:
          ok = symbols[0] != symbols[1] && symbols[0] != symbols[2] && symbols[1] != symbols[2];
          break;
        case 4:
        default:
          ok = symbols[0] != symbols[1] && symbols[0] != symbols[2] && symbols[0] != symbols[3]
              && symbols[1] != symbols[2] && symbols[1] != symbols[3] && symbols[2] != symbols[3];
          if (BitReader.readBits(br, 1) == 1) {
            codeLengths[symbols[2]] = 3;
            codeLengths[symbols[3]] = 3;
          } else {
            codeLengths[symbols[0]] = 2;
          }
          break;
      }
    } 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(br);
        int p = (int) (br.accumulator >>> br.bitOffset) & 15;
        // TODO: Demultiplex FIXED_TABLE.
        br.bitOffset += FIXED_TABLE[p] >> 16;
        int v = FIXED_TABLE[p] & 0xFFFF;
        codeLengthCodeLengths[codeLenIdx] = v;
        if (v != 0) {
          space -= (32 >> v);
          numCodes++;
        }
      }
      ok = (numCodes == 1 || space == 0);
      readHuffmanCodeLengths(codeLengthCodeLengths, alphabetSize, codeLengths, br);
    }
    if (!ok) {
      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, BitReader br) {
    BitReader.readMoreInput(br);
    int numTrees = decodeVarLenUnsignedByte(br) + 1;

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

    boolean useRleForZeros = BitReader.readBits(br, 1) == 1;
    int maxRunLengthPrefix = 0;
    if (useRleForZeros) {
      maxRunLengthPrefix = BitReader.readBits(br, 4) + 1;
    }
    int[] table = new int[Huffman.HUFFMAN_MAX_TABLE_SIZE];
    readHuffmanCode(numTrees + maxRunLengthPrefix, table, 0, br);
    for (int i = 0; i < contextMapSize; ) {
      BitReader.readMoreInput(br);
      BitReader.fillBitWindow(br);
      int code = readSymbol(table, 0, br);
      if (code == 0) {
        contextMap[i] = 0;
        i++;
      } else if (code <= maxRunLengthPrefix) {
        int reps = (1 << code) + BitReader.readBits(br, 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++;
      }
    }
    if (BitReader.readBits(br, 1) == 1) {
      inverseMoveToFrontTransform(contextMap, contextMapSize);
    }
    return numTrees;
  }

  private static void decodeBlockTypeAndLength(State state, int treeType) {
    final BitReader br = state.br;
    final int[] ringBuffers = state.blockTypeRb;
    final int offset = treeType * 2;
    BitReader.fillBitWindow(br);
    int blockType = readSymbol(
        state.blockTypeTrees, treeType * Huffman.HUFFMAN_MAX_TABLE_SIZE, br);
    state.blockLength[treeType] = readBlockLength(state.blockLenTrees,
        treeType * Huffman.HUFFMAN_MAX_TABLE_SIZE, br);

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

  private static void decodeLiteralBlockSwitch(State state) {
    decodeBlockTypeAndLength(state, 0);
    int literalBlockType = state.blockTypeRb[1];
    state.contextMapSlice = literalBlockType << LITERAL_CONTEXT_BITS;
    state.literalTreeIndex = state.contextMap[state.contextMapSlice] & 0xFF;
    state.literalTree = state.hGroup0.trees[state.literalTreeIndex];
    int contextMode = state.contextModes[literalBlockType];
    state.contextLookupOffset1 = Context.LOOKUP_OFFSETS[contextMode];
    state.contextLookupOffset2 = Context.LOOKUP_OFFSETS[contextMode + 1];
  }

  private static void decodeCommandBlockSwitch(State state) {
    decodeBlockTypeAndLength(state, 1);
    state.treeCommandOffset = state.hGroup1.trees[state.blockTypeRb[3]];
  }

  private static void decodeDistanceBlockSwitch(State state) {
    decodeBlockTypeAndLength(state, 2);
    state.distContextMapSlice = state.blockTypeRb[5] << DISTANCE_CONTEXT_BITS;
  }

  private static void maybeReallocateRingBuffer(State state) {
    int newSize = state.maxRingBufferSize;
    if ((long) newSize > state.expectedTotalSize) {
      /* TODO: Handle 2GB+ cases more gracefully. */
      int minimalNewSize = (int) state.expectedTotalSize + state.customDictionary.length;
      while ((newSize >> 1) > minimalNewSize) {
        newSize >>= 1;
      }
      if (!state.inputEnd && newSize < 16384 && state.maxRingBufferSize >= 16384) {
        newSize = 16384;
      }
    }
    if (newSize <= state.ringBufferSize) {
      return;
    }
    int ringBufferSizeWithSlack = newSize + Dictionary.MAX_TRANSFORMED_WORD_LENGTH;
    byte[] newBuffer = new byte[ringBufferSizeWithSlack];
    if (state.ringBuffer != null) {
      System.arraycopy(state.ringBuffer, 0, newBuffer, 0, state.ringBufferSize);
    } else {
      /* Prepend custom dictionary, if any. */
      if (state.customDictionary.length != 0) {
        int length = state.customDictionary.length;
        int offset = 0;
        if (length > state.maxBackwardDistance) {
          offset = length - state.maxBackwardDistance;
          length = state.maxBackwardDistance;
        }
        System.arraycopy(state.customDictionary, offset, newBuffer, 0, length);
        state.pos = length;
        state.bytesToIgnore = length;
      }
    }
    state.ringBuffer = newBuffer;
    state.ringBufferSize = newSize;
  }

  /**
   * Reads next metablock header.
   *
   * @param state decoding state
   */
  private static void readMetablockInfo(State state) {
    final BitReader br = state.br;

    if (state.inputEnd) {
      state.nextRunningState = FINISHED;
      state.bytesToWrite = state.pos;
      state.bytesWritten = 0;
      state.runningState = WRITE;
      return;
    }
    // TODO: Reset? Do we need this?
    state.hGroup0.codes = null;
    state.hGroup0.trees = null;
    state.hGroup1.codes = null;
    state.hGroup1.trees = null;
    state.hGroup2.codes = null;
    state.hGroup2.trees = null;

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

    if (state.isMetadata) {
      return;
    }
    state.expectedTotalSize += state.metaBlockLength;
    if (state.ringBufferSize < state.maxRingBufferSize) {
      maybeReallocateRingBuffer(state);
    }
  }

  private static void readMetablockHuffmanCodesAndContextMaps(State state) {
    final BitReader br = state.br;

    for (int i = 0; i < 3; i++) {
      state.numBlockTypes[i] = decodeVarLenUnsignedByte(br) + 1;
      state.blockLength[i] = 1 << 28;
      if (state.numBlockTypes[i] > 1) {
        readHuffmanCode(state.numBlockTypes[i] + 2, state.blockTypeTrees,
            i * Huffman.HUFFMAN_MAX_TABLE_SIZE, br);
        readHuffmanCode(NUM_BLOCK_LENGTH_CODES, state.blockLenTrees,
            i * Huffman.HUFFMAN_MAX_TABLE_SIZE, br);
        state.blockLength[i] = readBlockLength(state.blockLenTrees,
            i * Huffman.HUFFMAN_MAX_TABLE_SIZE, br);
      }
    }

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

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

    // TODO: Reuse?
    state.distContextMap = new byte[state.numBlockTypes[2] << DISTANCE_CONTEXT_BITS];
    int numDistTrees = decodeContextMap(state.numBlockTypes[2] << DISTANCE_CONTEXT_BITS,
        state.distContextMap, br);

    HuffmanTreeGroup.init(state.hGroup0, NUM_LITERAL_CODES, numLiteralTrees);
    HuffmanTreeGroup.init(state.hGroup1, NUM_INSERT_AND_COPY_CODES, state.numBlockTypes[1]);
    HuffmanTreeGroup.init(state.hGroup2, numDistanceCodes, numDistTrees);

    HuffmanTreeGroup.decode(state.hGroup0, br);
    HuffmanTreeGroup.decode(state.hGroup1, br);
    HuffmanTreeGroup.decode(state.hGroup2, br);

    state.contextMapSlice = 0;
    state.distContextMapSlice = 0;
    state.contextLookupOffset1 = Context.LOOKUP_OFFSETS[state.contextModes[0]];
    state.contextLookupOffset2 = Context.LOOKUP_OFFSETS[state.contextModes[0] + 1];
    state.literalTreeIndex = 0;
    state.literalTree = state.hGroup0.trees[0];
    state.treeCommandOffset = state.hGroup1.trees[0]; // TODO: == 0?

    state.blockTypeRb[0] = state.blockTypeRb[2] = state.blockTypeRb[4] = 1;
    state.blockTypeRb[1] = state.blockTypeRb[3] = state.blockTypeRb[5] = 0;
  }

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

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

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

    BitReader.reload(br);
    state.runningState = BLOCK_START;
  }

  private static boolean writeRingBuffer(State state) {
    /* Ignore custom dictionary bytes. */
    if (state.bytesToIgnore != 0) {
      state.bytesWritten += state.bytesToIgnore;
      state.bytesToIgnore = 0;
    }
    int toWrite = Math.min(state.outputLength - state.outputUsed,
        state.bytesToWrite - state.bytesWritten);
    if (toWrite != 0) {
      System.arraycopy(state.ringBuffer, state.bytesWritten, state.output,
          state.outputOffset + state.outputUsed, toWrite);
      state.outputUsed += toWrite;
      state.bytesWritten += toWrite;
    }

    return state.outputUsed < state.outputLength;
  }

  static void setCustomDictionary(State state, byte[] data) {
    state.customDictionary = (data == null) ? new byte[0] : data;
  }

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

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

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

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

          state.j = 0;
          state.runningState = INSERT_LOOP;

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

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

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

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

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

          if (state.copyLength > state.metaBlockLength) {
            throw new BrotliRuntimeException("Invalid backward reference"); // COV_NF_LINE
          }
          state.j = 0;
          state.runningState = COPY_LOOP;
          // fall through
        case COPY_LOOP:
          for (; state.j < state.copyLength;) {
            ringBuffer[state.pos] =
                ringBuffer[(state.pos - state.distance) & ringBufferMask];
            // TODO: condense
            state.metaBlockLength--;
            state.j++;
            if (state.pos++ == ringBufferMask) {
              state.nextRunningState = COPY_LOOP;
              state.bytesToWrite = state.ringBufferSize;
              state.bytesWritten = 0;
              state.runningState = WRITE;
              break;
            }
          }
          if (state.runningState == COPY_LOOP) {
            state.runningState = MAIN_LOOP;
          }
          continue;

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

        case COPY_WRAP_BUFFER:
          System.arraycopy(ringBuffer, state.ringBufferSize, ringBuffer, 0,
              state.copyDst - state.ringBufferSize);
          state.runningState = MAIN_LOOP;
          continue;

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


        case COPY_UNCOMPRESSED:
          copyUncompressedData(state);
          continue;

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

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