package edu.berkeley.nlp.lm.bits;

import edu.berkeley.nlp.lm.array.LongArray;

/**
 * Wraps a portion of a long[] array with iterator-like functionality over a
 * stream of bits.
 * 
 * @author adampauls
 * 
 */
public final class BitStream
{

	private static final long LOG_LONG_SIZE = Long.SIZE - Long.numberOfLeadingZeros(Long.SIZE - 1);

	private static final long LOG_LONG_MASK = (1L << LOG_LONG_SIZE) - 1;

	private static final long HIGH_BIT_MASK = (1L << (Long.SIZE - 1));

	private final LongArray data;

	private final long start;

	private long currLong = -1;

	private long currPos = -1;

	private int relBit;

	private long markedCurrPos = -1;

	private int markedRelBit = -1;

	private final int numBits;

	private final int startBit;

	public BitStream(final LongArray data, final long start, final int startBit, final int numBits) {
		this.data = data;
		this.start = start;
		this.currPos = 0;
		this.numBits = numBits;
		this.startBit = startBit;
		currLong = data.get(start) << startBit;
		this.relBit = startBit;
	}

	public boolean nextBit() {
		assert !finished();
		if (relBit == Long.SIZE) advanceToNextLong();
		final boolean ret = (currLong & HIGH_BIT_MASK) != 0;
		relBit++;
		currLong <<= 1;
		if (relBit == Long.SIZE) {
			advanceToNextLong();
		}
		return ret;
	}

	/**
	 * Reads a string of zeros followed by a 1.
	 * 
	 * @return the number of consecutive zeros (plus 1)
	 */
	public int nextConsecutiveZeros() {
		final int numberOfLeadingZerosOnThisWord = Long.numberOfLeadingZeros(currLong);
		final int numLeft = Long.SIZE - relBit;
		if (numberOfLeadingZerosOnThisWord >= numLeft) {
			advanceToNextLong();
			final int numberOfLeadingZerosOnNextWord = Long.numberOfLeadingZeros(currLong);
			advanceWithinCurrLong(numberOfLeadingZerosOnNextWord + 1);
			return numberOfLeadingZerosOnNextWord + 1 + numLeft;
		} else {
			final int headerLength = numberOfLeadingZerosOnThisWord + 1;
			advanceWithinCurrLong(headerLength);
			return headerLength;
		}
	}

	/**
	 * Read and return next n bits.
	 * 
	 * @param n
	 * @return
	 */
	public long next(final int n) {
		assert n <= Long.SIZE;
		assert !finished();
		final int leftOnThisWord = Math.min(n, Long.SIZE - relBit);
		final int onNextWord = n - leftOnThisWord;
		final int thisWordShift = Long.SIZE - leftOnThisWord;
		long ret = currLong >>> thisWordShift;
		if (onNextWord > 0) {
			advanceToNextLong();
			final int nextWordShift = Long.SIZE - onNextWord;
			ret <<= onNextWord;
			ret |= currLong >>> nextWordShift;
			advanceWithinCurrLong(onNextWord);
		} else {
			advanceWithinCurrLong(leftOnThisWord);
		}
		return ret;
	}

	/**
	 * 
	 */
	private void advanceWithinCurrLong(final int n) {
		assert relBit + n <= Long.SIZE;
		relBit += n;
		currLong <<= n;

	}

	private void advanceToNextLong() {
		currPos++;
		relBit = 0;
		currLong = data.get(start + currPos);
	}

	public boolean finished() {
		return numBitsLeft() <= 0;
	}

	public void rewind(final int i) {
		shiftAbsPosition(-i);
	}

	private void shiftAbsPosition(final int i) {
		final long absBit = (currPos << LOG_LONG_SIZE) + relBit;
		final long rewound = absBit + i;
		final int newRelBit = (int) (rewound & LOG_LONG_MASK);
		final long newPos = rewound >>> LOG_LONG_SIZE;
		reset(newRelBit, newPos);
	}

	private void reset(final int newRelBit, final long newPos) {
		relBit = newRelBit;
		currPos = newPos;
		currLong = data.get(start + currPos) << relBit;
	}

	public int numBitsLeft() {
		return (int) (numBits + startBit - ((currPos << LOG_LONG_SIZE) + relBit));
	}

	/**
	 * Advances without returning any bits.
	 * 
	 * @param n
	 */
	public void advance(final int n) {
		shiftAbsPosition(n);
	}

	/**
	 * Sets a mark at the current bit that can be returned to using
	 * rewindToMark.
	 */
	public void mark() {
		assert markedCurrPos < 0 : "Tried to double mark";
		markedCurrPos = currPos;
		markedRelBit = relBit;
	}

	public void rewindToMark() {
		reset(markedRelBit, markedCurrPos);
		markedCurrPos = -1;
		markedRelBit = -1;
	}

}