/**
	 *	Basic bit OutputStream utility based on the java.nio
	 *	package.  Uses multiple buffers to move optimized chunks
	 *	at a time.  Allows for writing up to 32 bits at a time.
	 *
	 *	@contributor Owen Astrachan
	 *	@author Brian Lavallee
	 *	@date 10 April 2016
	 */

	import java.io.File;
	import java.io.FileNotFoundException;
	import java.io.FileOutputStream;
	import java.io.IOException;
	import java.io.OutputStream;
	import java.nio.ByteBuffer;
	import java.nio.channels.Channels;
	import java.nio.channels.WritableByteChannel;

	public class BitOutputStream extends OutputStream {

		private static final int BYTE_SIZE = 8;
		private static final int INT_SIZE = 32;
		private static final int BUFFER_SIZE = 8192;

		private static final long[] bitMask = { 0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff, 0x1ff, 0x3ff, 0x7ff,
				0xfff, 0x1fff, 0x3fff, 0x7fff, 0xffff, 0x1ffff, 0x3ffff, 0x7ffff, 0xfffff, 0x1fffff, 0x3fffff, 0x7fffff,
				0xffffff, 0x1ffffff, 0x3ffffff, 0x7ffffff, 0xfffffff, 0x1fffffff, 0x3fffffff, 0x7fffffff, 0xffffffff,
				0x1ffffffffl, 0x3ffffffffl, 0x7ffffffffl, 0xfffffffffl, 0x1fffffffffl, 0x3fffffffffl, 0x7fffffffffl,
				0xffffffffffl, 0x1ffffffffffl, 0x3ffffffffffl, 0x7ffffffffffl, 0xfffffffffffl, 0x1fffffffffffl,
				0x3fffffffffffl, 0x7fffffffffffl, 0xffffffffffffl, 0x1ffffffffffffl, 0x3ffffffffffffl, 0x7ffffffffffffl,
				0xfffffffffffffl, 0x1fffffffffffffl, 0x3fffffffffffffl, 0x7fffffffffffffl, 0xffffffffffffffl,
				0x1ffffffffffffffl, 0x3ffffffffffffffl, 0x7ffffffffffffffl, 0xfffffffffffffffl, 0x1fffffffffffffffl,
				0x3fffffffffffffffl, 0x7fffffffffffffffl, 0xffffffffffffffffl };

		private OutputStream source;
		private int bitsWritten, available;
		private long bitBuffer;
		private ByteBuffer buffer;
		private WritableByteChannel output;

		public BitOutputStream(String filePath) {
			this(new File(filePath));
		}

		public BitOutputStream(File fileSource) {
			try {
				initialize(new FileOutputStream(fileSource));
			}
			catch (FileNotFoundException fnf) {
				throw new RuntimeException(fnf);
			}
		}

		public BitOutputStream(OutputStream out) {
			initialize(out);
		}

		public void initialize(OutputStream out) {
			source = out;
			bitsWritten = 0;
			available = 64;
			bitBuffer = 0;
			output = Channels.newChannel(source);
			buffer = ByteBuffer.allocate(BUFFER_SIZE);
		}

		public int bitsWritten() {
			return bitsWritten;
		}

		public void flush() {
			emptyBitBufferExact();
			emptyBuffer();
		}

		public void close() {
			try {
				flush();
				output.close();
				source.close();
			}
			catch (IOException io) {
				throw new RuntimeException(io);
			}

		}

		public void write(int value) {
			writeBits(BYTE_SIZE, value);
		}

		public void writeBits(int numBits, int value) {
			if (numBits > INT_SIZE || numBits < 1) {
				throw new RuntimeException("Illegal argument: numBits must be on [1, 32]");
			}

			bitsWritten += numBits;

			if (numBits > available) {
				bitBuffer |= Integer.toUnsignedLong(value) >>> (numBits - available);
				value &= bitMask[numBits - available];
				numBits -= available;
				emptyBitBuffer();
			}

			bitBuffer |= Integer.toUnsignedLong(value) << (available - numBits);
			available -= numBits;
		}

		private void emptyBitBuffer() {
			if (!buffer.hasRemaining()) {
				emptyBuffer();
			}

			buffer.putLong(bitBuffer);
			bitBuffer = 0;
			available = 64;
		}

		private void emptyBitBufferExact() {
			if (!buffer.hasRemaining()) {
				emptyBuffer();
			}

			while (available < 64) {
				buffer.put((byte) (bitBuffer >>> 56));
				bitBuffer <<= 8;
				available += 8;
			}
		}

		private void emptyBuffer() {
			try {
				buffer.flip();
				output.write(buffer);
				buffer.clear();
			}
			catch (IOException io) {
				throw new RuntimeException(io);
			}
		}
	}