/* Copyright 2015 Google Inc. All Rights Reserved.

   https://github.com/google/brotli/blob/master/LICENSE

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