package com.codecademy.eventhub.base;
import com.google.common.collect.Maps;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.TreeMap;
public class ByteBufferMap {
private static final int META_DATA_SIZE_IN_BYTES = 4; /* bytes */
private static final int RECORD_SIZE_IN_BYTES = Integer.SIZE / 8; /* bytes */
private final ByteBuffer byteBuffer;
public ByteBufferMap(ByteBuffer byteBuffer) {
this.byteBuffer = byteBuffer;
}
public String get(String key) {
ByteBuffer currentBuffer = byteBuffer.duplicate();
currentBuffer.position(0);
int numProperties = currentBuffer.getInt();
return get(key, 0, numProperties, numProperties);
}
public void enumerate(KeyValueCallback callback) {
ByteBuffer currentBuffer = byteBuffer.duplicate();
currentBuffer.position(0);
ByteBuffer pointersBuffer = currentBuffer.duplicate();
pointersBuffer.position(META_DATA_SIZE_IN_BYTES);
int numProperties = currentBuffer.getInt();
for (int i = 0; i < numProperties; i++) {
// TODO: can be optimized
callback.callback(getKey(currentBuffer, i, numProperties),
getValue(currentBuffer, i, numProperties));
}
}
public ByteBuffer toByteBuffer() {
ByteBuffer buffer = byteBuffer.duplicate();
buffer.position(0);
return buffer;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
enumerate(new KeyValueCallback() {
@Override
public void callback(String key, String value) {
sb.append(key).append(": ").append(value).append("\n");
}
});
return sb.toString();
}
// TODO: make return type Optional<String>
private String get(String targetKey, int start, int end, int numProperties) {
if (start >= end) {
//noinspection ReturnOfNull
return null;
}
ByteBuffer currentBuffer = byteBuffer.duplicate();
int currentRecordOffset = (start + end) >>> 1;
String key = getKey(currentBuffer, currentRecordOffset, numProperties);
int comparisonResult = key.compareTo(targetKey);
if (comparisonResult == 0) {
return getValue(currentBuffer, currentRecordOffset, numProperties);
} else if (comparisonResult < 0) {
return get(targetKey, currentRecordOffset + 1, end, numProperties);
} else {
return get(targetKey, start, currentRecordOffset, numProperties);
}
}
private int calculateByteOffset(int recordOffset) {
return META_DATA_SIZE_IN_BYTES + recordOffset * RECORD_SIZE_IN_BYTES;
}
private String getKey(ByteBuffer buffer, int recordOffset, int numProperties) {
buffer.position(calculateByteOffset(recordOffset));
int startOffsetInBytes;
if (recordOffset == 0) {
startOffsetInBytes = META_DATA_SIZE_IN_BYTES + 2 * numProperties * RECORD_SIZE_IN_BYTES;
} else {
buffer.position(calculateByteOffset((recordOffset - 1)));
startOffsetInBytes = buffer.getInt();
}
int finishOffsetInBytes = buffer.getInt();
return getString(buffer, startOffsetInBytes, finishOffsetInBytes);
}
private String getValue(ByteBuffer buffer, int recordOffset, int numProperties) {
buffer.position(calculateByteOffset((numProperties + recordOffset - 1)));
int startOffsetInBytes = buffer.getInt();
buffer.position(calculateByteOffset((numProperties + recordOffset)));
int finishOffsetInBytes = buffer.getInt();
return getString(buffer, startOffsetInBytes, finishOffsetInBytes);
}
private String getString(ByteBuffer buffer, int startOffsetInBytes, int finishOffsetInBytes) {
buffer.position(startOffsetInBytes);
byte[] keyBytes = new byte[finishOffsetInBytes - startOffsetInBytes];
buffer.get(keyBytes);
return new String(keyBytes);
}
public static ByteBufferMap build(Map<String, String> fromMap) {
TreeMap<String, String> sortedProperties = Maps.newTreeMap();
sortedProperties.putAll(fromMap);
int propertiesSizeInBytes = 0;
for (Map.Entry<String, String> entry : sortedProperties.entrySet()) {
propertiesSizeInBytes += entry.getKey().getBytes().length;
propertiesSizeInBytes += entry.getValue().getBytes().length;
}
int pointersSizeInBytes = 2 * sortedProperties.size() * RECORD_SIZE_IN_BYTES;
byte[] bytes = new byte[META_DATA_SIZE_IN_BYTES + pointersSizeInBytes + propertiesSizeInBytes];
// initialize metadata
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
byteBuffer.putInt(sortedProperties.size());
// initialize keys and key pointers
byteBuffer.position(META_DATA_SIZE_IN_BYTES);
ByteBuffer propertiesBuffer = byteBuffer.duplicate();
propertiesBuffer.position(META_DATA_SIZE_IN_BYTES + pointersSizeInBytes);
for (String key : sortedProperties.keySet()) {
propertiesBuffer.put(key.getBytes());
byteBuffer.putInt(propertiesBuffer.position());
}
// initialize values and value pointers
for (String value : sortedProperties.values()) {
propertiesBuffer.put(value.getBytes());
byteBuffer.putInt(propertiesBuffer.position());
}
return new ByteBufferMap(byteBuffer);
}
}