/*
 * Copyright (C) 2006 Steve Ratcliffe
 * 
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 * 
 * Author: Steve Ratcliffe
 * Create date: 14-Dec-2006
 */
package uk.me.parabola.imgfmt.app;

import uk.me.parabola.log.Logger;

/**
 * A class to write the bitstream.
 *
 * @author Steve Ratcliffe
 */
public class BitWriter {
	private static final Logger log = Logger.getLogger(BitWriter.class);

	// Choose so that most roads will not fill it.
	private static final int INITIAL_BUF_SIZE = 20;

	// The byte buffer and its current length (allocated length)
	private byte[] buf;  // The buffer
	private int bufsize;  // The allocated size
	private int buflen; // The actual used length

	// The bit offset into the byte array.
	private int bitoff;
	private static final int BUFSIZE_INC = 50;

	public BitWriter() {
		bufsize = INITIAL_BUF_SIZE;
		buf = new byte[bufsize];
	}

	/**
	 * Put exactly one bit into the buffer.
	 *
	 * @param b The bottom bit of the integer is set at the current bit position.
	 */
	private void put1(int b) {
		ensureSize(bitoff + 1);

		int off = getByteOffset(bitoff);

		// Get the remaining bits into the byte.
		int rem = bitoff - 8 * off;

		// Or it in, we are assuming that the position is never turned back.
		buf[off] |= (b & 0x1) << rem;

		// Increment position
		bitoff++;

		// If we are in a new byte, increase the byte length.
		if ((bitoff & 0x7) == 1)
			buflen++;

		debugPrint(b, 1);
	}
	
	public void put1(boolean b) {
		put1(b ? 1 : 0);
	}

	/**
	 * Put a number of bits into the buffer, growing it if necessary.
	 *
	 * @param bval The bits to add, the lowest <b>n</b> bits will be added to
	 * the buffer.
	 * @param nb The number of bits.
	 */
	public void putn(int bval, int nb) {
		int val = bval & ((1<<nb) - 1);
		int n = nb;

		// We need to be able to deal with more than 24 bits, but now we can't yet
		if (n >= 24)
			throw new IllegalArgumentException();

		ensureSize(bitoff + n);

		// Get each affected byte and set bits into it until we are done.
		while (n > 0) {
			int ind = getByteOffset(bitoff);
			int rem = bitoff - 8*ind;

			buf[ind] |= ((val << rem) & 0xff);

			// Shift down in preparation for next byte.
			val >>>= 8-rem;

			// Account for change so far
			int nput = 8 - rem;
			if (nput > n)
				nput = n;
			bitoff += nput;
			n -= nput;
		}

		buflen = (bitoff+7)/8;
	}
	
	/**
	 * Write a signed value. If the value doesn't fit into nb bits, write one or more 1 << (nb-1)  
	 * as a flag for extended range.
	 */

	public void sputn(int bval, int nb) {
		int top = 1 << (nb - 1);
		int mask = top - 1;
		int val = Math.abs(bval);
		while (val > mask) {
			putn(top, nb);
			val -= mask;
		}
		if (bval < 0) {
			putn((top - val) | top, nb);
		} else {
			putn(val, nb);
		}
	}
	
	public byte[] getBytes() {
		return buf;
	}

	public int getBitPosition() {
		return bitoff;
	}
	/**
	 * Get the number of bytes actually used to hold the bit stream. This therefore can be and usually
	 * is less than the length of the buffer returned by {@link #getBytes()}.
	 * @return Number of bytes required to hold the output.
	 */
	public int getLength() {
		return buflen;
	}

	/**
	 * Get the byte offset for the given bit number.
	 *
	 * @param boff The number of the bit in question.
	 * @return The index into the byte array where the bit resides.
	 */
	private int getByteOffset(int boff) {
		return boff/8;
	}

	/**
	 * Set everything up so that the given size can be accommodated.
	 * The buffer is re-sized if necessary.
	 *
	 * @param newlen The new length of the bit buffer in bits.
	 */
	private void ensureSize(int newlen) {
		if (newlen/8 >= bufsize)
			reallocBuffer();
	}

	/**
	 * Reallocate the byte buffer.
	 */
	private void reallocBuffer() {
		log.debug("reallocating buffer");
		bufsize += BUFSIZE_INC;
		byte[] newbuf = new byte[bufsize];

		System.arraycopy(this.buf, 0, newbuf, 0, this.buf.length);
		this.buf = newbuf;
	}

	private void debugPrint(int b, int i) {
		if (log.isDebugEnabled())
			log.debug("after put", i, "of", b, " bufsize=", bufsize, ", len=",
					buflen, ", pos=", bitoff);
	}


}
