package org.limewire.collection;
import java.util.Locale;
import java.util.Arrays;
/**
* Allows storage & retrieval of numbers based on the index of an
* on or off bit in a byte[] or a hexadecimal String representation
* of that byte[].
*/
public class BitNumbers {
private static final byte[] EMPTY = new byte[0];
/** A set of masks for finding if the bit is set at the right index. */
private static final byte[] MASKS = new byte[] {
(byte)0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1
};
/** The bits that are stored. */
private byte[] data;
/** The total size of this bitNumbers. */
private final int size;
/** A convenient shared immutable empty BitNumbers. */
public static final BitNumbers EMPTY_BN = new ImmutableBitNumbers();
/** Constructs a BitNumbers backed by the given byte[]. */
public BitNumbers(byte[] data) {
this.data = data;
this.size = data.length * 8;
}
/** Constructs a BitNumbers large enough to store numbers up to size. */
public BitNumbers(int size) {
this.size = size;
}
/**
* Constructs a BitNumbers based on the given hex string.
* This accepts a nibble for the last element,
* thus:
* <pre>
* FF corresponds to elements 0 through 8 being on
* FFF corresponds to elements 0 through 12 being on (implies below)
* FFF0 corresponds to elements 0 through 12 being on also
* </pre>
*/
public BitNumbers(String hexString) throws IllegalArgumentException {
this.data = new byte[(int)Math.ceil(hexString.length() / 2d)];
this.size = data.length * 8;
// Now fill up data, decoding the string...
for(int i = 0; i < hexString.length(); i+=2) {
// Will throw NFE (which extends IAE) if data is invalid
boolean nibble = i == hexString.length() - 1;
int j = Integer.parseInt(hexString.substring(i, nibble ? i+1 : i+2), 16);
if(nibble) // the last element may just be a nibble
j <<= 4;
assert j <= 0xFF;
data[i/2] = (byte)j;
}
}
/** Returns true if the correct bit is set. */
public boolean isSet(int idx) {
if(idx >= size)
return false;
int index = (int)Math.floor(idx / 8d);
int offset = idx % 8;
return data != null && index < data.length && (data[index] & MASKS[offset]) != 0;
}
/** Returns the maximum number that can be stored in this BitNumbers. */
public int getMax() {
return size;
}
/** Sets the bit corresponding to the index. */
public void set(int idx) {
if(idx >= size)
throw new IndexOutOfBoundsException("idx: " + idx + ", max: " + size);
int index = (int)Math.floor(idx / 8d);
int offset = idx % 8;
if(data == null)
data = new byte[(int)Math.ceil(size / 8d)];
data[index] |= MASKS[offset];
}
/** Returns the byte array that BitNumbers is backed off of. */
public byte[] toByteArray() {
if(data == null) {
return EMPTY;
} else {
int lastNonZero = getLastNonZeroIndex();
if(lastNonZero == -1) { // completely empty
return EMPTY;
} else if(lastNonZero == data.length - 1) { // uses full width
return data;
} else { // must strip out the extra bytes.
byte[] shortened = new byte[lastNonZero+1];
System.arraycopy(data, 0, shortened, 0, lastNonZero+1);
return shortened;
}
}
}
/** Returns true if no set bits exist. */
public boolean isEmpty() {
if(data == null)
return true;
for (byte b : data)
if (b != 0)
return false;
return true;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BitNumbers) {
BitNumbers other = (BitNumbers)obj;
return Arrays.equals(toByteArray(), other.toByteArray());
}
return false;
}
@Override
public int hashCode() {
return Arrays.hashCode(toByteArray());
}
/** A hexadecimal representation of the byte[]. */
public String toHexString() {
if(isEmpty()) {
return "";
} else {
StringBuilder sb = new StringBuilder(data.length * 2);
int lastNonZero = 0;
for(int i = 0; i < data.length; i++) {
if(data[i] != 0)
lastNonZero = i;
String hex = Integer.toHexString(data[i] & 0x00FF);
if(hex.length() == 1)
sb.append("0");
sb.append(hex.toUpperCase(Locale.US));
}
sb.setLength(lastNonZero * 2 + 2); // erase empty fields.
if(sb.length() > 1 && sb.charAt(sb.length()-1) == '0')
sb.setLength(sb.length()-1);
return sb.toString();
}
}
/** Returns the last non-empty index. */
private int getLastNonZeroIndex() {
for(int i = data.length - 1; i >= 0; i--) {
if(data[i] != 0)
return i;
}
return -1;
}
@Override
public String toString() {
return toHexString();
}
/** A BitNumbers that is empty and non-settable. */
private static class ImmutableBitNumbers extends BitNumbers {
@Override
public void set(int idx) {
throw new UnsupportedOperationException("immutable!");
}
ImmutableBitNumbers() {
super(0);
}
}
public static BitNumbers synchronizedBitNumbers(BitNumbers delegate) {
return new SynchronizedBitNumbers(delegate);
}
private static class SynchronizedBitNumbers extends BitNumbers {
private final BitNumbers delegate;
private SynchronizedBitNumbers(BitNumbers delegate) {
super(0);
this.delegate = delegate;
}
@Override
public synchronized boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public synchronized int getMax() {
return delegate.getMax();
}
@Override
public synchronized boolean isSet(int idx) {
return delegate.isSet(idx);
}
@Override
public synchronized void set(int idx) {
delegate.set(idx);
}
@Override
public synchronized byte[] toByteArray() {
return delegate.toByteArray();
}
@Override
public synchronized String toHexString() {
return delegate.toHexString();
}
@Override
public synchronized String toString() {
return delegate.toString();
}
}
}