/*
* Copyright (c) 2015-2016, Christoph Engelbert (aka noctarius) and
* contributors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.noctarius.tengi.spi.serialization.codec.impl;
import com.noctarius.tengi.spi.buffer.ReadableMemoryBuffer;
import com.noctarius.tengi.spi.buffer.WritableMemoryBuffer;
final class BitSetCompressor {
private static final byte BASE_CHUNK_SINGLE = (byte) (0b01000_000);
private static final short BASE_CHUNK_DOUBLE = (short) (0b1000_0000_0000_0000);
private static final int BASE_CHUNK_QUAD = 0b1100_0000_0000_0000_0000_0000_0000_0000;
private static final byte MASK_CHUNK_TYPE_SINGLE = (byte) 0b0100_0000;
private static final byte MASK_CHUNK_TYPE_DOUBLE = (byte) 0b1000_0000;
private static final byte MASK_CHUNK_TYPE_QUAD = (byte) 0b1100_0000;
private static final byte NULL_CHUNK = BASE_CHUNK_SINGLE;
private static final byte MASK_NULL_CHUNK = (byte) 0b0011_1110;
private static final int MASK_SIZE_SINGLE = 0b11;
private static final int MASK_SIZE_DOUBLE = 0b111;
private static final int MASK_SIZE_QUAD = 0b1111;
private static final int SHIFT_SIZE_SINGLE = 4;
private static final int SHIFT_SIZE_DOUBLE = 11;
private static final int SHIFT_SIZE_QUAD = 26;
private static final int MIN_SLOTS_SINGLE = 1;
private static final int MAX_SLOTS_SINGLE = 3;
private static final int MIN_SLOTS_DOUBLE = 4;
private static final int MAX_SLOTS_DOUBLE = 10;
private static final int MIN_SLOTS_QUAD = 11;
private static final int MAX_SLOTS_QUAD = 25;
private static final int MIN_SIZE_QUAD_CHUNK_SELECTOR = 14;
static void writeBitSet(boolean[] value, WritableMemoryBuffer memoryBuffer) {
int remainingSlots = value != null ? value.length : 0;
do {
ChunkType chunkType = selectChunkType(remainingSlots);
// Null chunks can be handled immediately
if (chunkType == ChunkType.Null) {
memoryBuffer.writeByte(NULL_CHUNK);
return;
}
// Calculate usable slots
int useSlots = minSlots(remainingSlots, chunkType);
// Need further chunks
boolean furtherChunks = useSlots < remainingSlots;
// Starting position inside the array
int start = value.length - remainingSlots;
// Write chunk
switch (chunkType) {
case Single:
writeSingleChunk(value, start, useSlots, furtherChunks, memoryBuffer);
break;
case Double:
writeDoubleChunk(value, start, useSlots, furtherChunks, memoryBuffer);
break;
default:
writeQuadChunk(value, start, useSlots, furtherChunks, memoryBuffer);
}
// Subtract written slots
remainingSlots -= useSlots;
} while (remainingSlots > 0);
}
static boolean[] readBitSet(ReadableMemoryBuffer memoryBuffer) {
// Read first byte to find out about the chunk type
int readerIndex = memoryBuffer.readerIndex();
byte header = memoryBuffer.readByte();
memoryBuffer.readerIndex(readerIndex);
// Find chunk type
ChunkType chunkType = findChunkType(header);
// If NULL chunk, return here
if (chunkType == ChunkType.Null) {
return null;
}
// Read the current chunk
int chunk = readChunk(chunkType, memoryBuffer);
// Lookup if chunks are to follow up
boolean furtherChunks = (chunk & 0x1) == 1;
// Read the number of stored slots in this chunk
int slots = readSlots(chunk, chunkType);
// Read values
boolean[] values = readValues(chunk, slots, chunkType);
// If no chunks follow up return here
if (!furtherChunks) {
return values;
}
// Otherwise read further chunks
boolean[] moreValues = readBitSet(memoryBuffer);
// Combine data and return
boolean[] combined = new boolean[values.length + moreValues.length];
System.arraycopy(values, 0, combined, 0, values.length);
System.arraycopy(moreValues, 0, combined, values.length, moreValues.length);
return combined;
}
private static boolean[] readValues(int chunk, int slots, ChunkType chunkType) {
boolean[] values = new boolean[slots];
int shiftBase = getShiftBase(chunkType);
for (int i = 0; i < slots; i++) {
int shiftFactor = shiftBase - i;
values[i] = ((chunk >> shiftFactor) & 0x1) == 1;
}
return values;
}
private static int getShiftBase(ChunkType chunkType) {
switch (chunkType) {
case Single:
return SHIFT_SIZE_SINGLE - 1;
case Double:
return SHIFT_SIZE_DOUBLE - 1;
default:
return SHIFT_SIZE_QUAD - 1;
}
}
private static int readSlots(int chunk, ChunkType chunkType) {
switch (chunkType) {
case Single:
return ((chunk >> SHIFT_SIZE_SINGLE) & MASK_SIZE_SINGLE);
case Double:
return ((chunk >> SHIFT_SIZE_DOUBLE) & MASK_SIZE_DOUBLE) + MIN_SLOTS_DOUBLE - 1;
default:
return ((chunk >> SHIFT_SIZE_QUAD) & MASK_SIZE_QUAD) + MIN_SLOTS_QUAD - 1;
}
}
private static int readChunk(ChunkType chunkType, ReadableMemoryBuffer memoryBuffer) {
switch (chunkType) {
case Single:
return memoryBuffer.readByte();
case Double:
return ByteOrderUtils.getShort(memoryBuffer);
default:
return ByteOrderUtils.getInt(memoryBuffer);
}
}
private static void writeQuadChunk(boolean[] value, int start, int useSlots, //
boolean furtherChunks, WritableMemoryBuffer memoryBuffer) {
int chunk = writeChunk(value, BASE_CHUNK_QUAD, start, useSlots, MIN_SLOTS_QUAD, SHIFT_SIZE_QUAD, furtherChunks);
ByteOrderUtils.putInt(chunk, memoryBuffer);
}
private static void writeDoubleChunk(boolean[] value, int start, int useSlots, //
boolean furtherChunks, WritableMemoryBuffer memoryBuffer) {
int chunk = writeChunk(value, BASE_CHUNK_DOUBLE, start, useSlots, MIN_SLOTS_DOUBLE, SHIFT_SIZE_DOUBLE, furtherChunks);
ByteOrderUtils.putShort((short) chunk, memoryBuffer);
}
private static void writeSingleChunk(boolean[] value, int start, int useSlots, //
boolean furtherChunks, WritableMemoryBuffer memoryBuffer) {
int chunk = writeChunk(value, BASE_CHUNK_SINGLE, start, useSlots, MIN_SLOTS_SINGLE, SHIFT_SIZE_SINGLE, furtherChunks);
memoryBuffer.writeByte(chunk);
}
private static int writeChunk(boolean[] value, int chunk, int start, int useSlots, //
int minSlots, int shiftBase, boolean furtherChunks) {
// Mark follow up if necessary
if (furtherChunks) {
chunk |= 0x1;
}
// Write size
chunk |= ((useSlots - minSlots + 1) << shiftBase);
// Write slots
for (int i = 0; i < useSlots; i++) {
int index = start + i;
int shift = shiftBase - i - 1;
chunk |= ((value[index] ? 1 : 0) << shift);
}
return chunk;
}
private static int minSlots(int remainingSlots, ChunkType chunkType) {
int maxSlots;
switch (chunkType) {
case Single:
maxSlots = MAX_SLOTS_SINGLE;
break;
case Double:
maxSlots = MAX_SLOTS_DOUBLE;
break;
default:
maxSlots = MAX_SLOTS_QUAD;
}
return Math.min(maxSlots, remainingSlots);
}
private static ChunkType findChunkType(byte header) {
if ((header & MASK_CHUNK_TYPE_QUAD) == MASK_CHUNK_TYPE_QUAD) {
return ChunkType.Quad;
} else if ((header & MASK_CHUNK_TYPE_DOUBLE) == MASK_CHUNK_TYPE_DOUBLE) {
return ChunkType.Double;
}
if ((header & MASK_CHUNK_TYPE_SINGLE) == MASK_CHUNK_TYPE_SINGLE) {
if ((header & MASK_NULL_CHUNK) == 0) {
return ChunkType.Null;
}
return ChunkType.Single;
}
throw new IllegalStateException("Illegal chunk type detected");
}
private static ChunkType selectChunkType(int requiredSlots) {
if (requiredSlots == 0) {
return ChunkType.Null;
} else if (requiredSlots <= MAX_SLOTS_SINGLE) {
return ChunkType.Single;
} else if (requiredSlots <= MAX_SLOTS_DOUBLE) {
return ChunkType.Double;
} else if (requiredSlots <= MIN_SIZE_QUAD_CHUNK_SELECTOR) {
// Will first select a Double chunk and a Single chunk in the next round
return ChunkType.Double;
}
return ChunkType.Quad;
}
private static enum ChunkType {
Null,
Single,
Double,
Quad
}
private BitSetCompressor() {
}
}