/*
 * BitShuffler.java
 *
 * Copyright (c) 2007 by Daniel Strecker.
 * <daniel dot strecker R-REMOVE-THIS-R at gmx dot net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Change History:
 * 2007-12-07 created
 */


package exists_de;


import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.util.BitSet;


/**
 * Class with utility methods for bitwise data manipulation and dumping, mostly
 * for byte arrays.
 * 
 * @author Daniel Strecker
 * @version 2007-12-07
 */
public class BitShuffler {

	/**
	 * Test method.
	 */
	public static void main(String[] args) throws IOException {
		byte[] data = {1, 0x55, (byte)0x80, 32, 127, 127};

		System.out.println("data: " + toBinaryString(data));
		int shift = 17; 
		rsh(data, shift);
		lsh(data, shift);
		System.out.println("lsh2: " + toBinaryString(data));
	}

	/**
	 * Leftshifts the bits in the specified byte array by the specified shift.
	 */
	public static final void lsh(byte[] ba, int shift) {		
		if (shift == 0 || ba.length == 0) //if nothing to do
			return;
		
		if (shift < 0)
			throw new IllegalArgumentException(
				"shift may not be less than zero: " + shift);

		int yteShift = shift / 8;
		int bitShift = shift % 8;

		int b1, b2;
		int lastByte = ba.length - yteShift - 1;
		b2 = ba[yteShift] & 0xff;
		for (int i = 0; i < lastByte; i++) {
			b1 = b2;
			b2 = ba[i + yteShift + 1] & 0xff;
			ba[i] = (byte)((b1 << bitShift) | (b2 << bitShift >>> 8));
		}
		//the last byte that is actually used is now in b2
		ba[lastByte] = (byte)(b2 << bitShift);

		//zero the rest
		for (int i = ba.length - 1; i > lastByte; i--)
			ba[i] = 0;
	}

	/**
	 * Rightshifts the bits in the specified byte array by the specified shift.
	 */
	public static final void rsh(byte[] ba, int shift) {		
		if (shift == 0 || ba.length == 0) //if nothing to do
			return;
		
		if (shift < 0)
			throw new IllegalArgumentException(
				"shift may not be less than zero: " + shift);

		int yteShift = shift / 8;
		int bitShift = shift % 8;

		int b1, b2;
		int lastByte = yteShift;
		b2 = ba[ba.length - 1 - yteShift] & 0xff;
		for (int i = ba.length - 1; i > lastByte; i--) {
			b1 = b2;
			b2 = ba[i - yteShift - 1] & 0xff;
			ba[i] = (byte)((b1 >> bitShift) | (b2 << 8 >>> bitShift));
		}
		//the last byte that is actually used is now in b2
		ba[lastByte] = (byte)(b2 >> bitShift);

		//zero the rest
		for (int i = 0; i < lastByte; i++)
			ba[i] = 0;
	}
	
	/**
	 * Extracts a range of bits from the specified byte array.
	 */
	public static final BitSet extract(byte[] src, int bitOffset, int bitLength) {
		if (bitOffset < 0)
			throw new IllegalArgumentException("bitOffset may not be < 0");

		if (bitLength < 0)
			throw new IllegalArgumentException("bitLength may not be < 0");
		
		if (bitOffset + bitLength > src.length * 8)
			throw new IllegalArgumentException("bitOffset + bitLength exceeds src.length * 8.");
		
		BitSet bitset = new BitSet(bitLength);
		
		for (int i = 0; i < bitLength; i++, bitOffset++)
			if (getBitOfBArray(src, bitOffset)) bitset.set(i);
		
		return bitset;
	}
	
	public static String toBinaryString(BitSet bitset, int length) {
		char[] c = new char[length];

		for (int i = 0; i < length; i++)
			c[i] = bitset.get(i) ? '1' : '0';

		return new String(c);		
	}

	/**
	 * Extracts a range of bits from the specified file.
	 */
	public static final BitSet extract(String filename, long bitOffset, int bitLength) throws IOException {
		if (bitOffset < 0)
			throw new IllegalArgumentException("bitOffset may not be < 0");

		if (bitLength < 0)
			throw new IllegalArgumentException("bitLength may not be < 0");

		File file = new File(filename);
		long fileLenInBits = file.length() * 8;
		long bitRangeEnd = bitOffset + bitLength; //index of bit behind the last bit to extract
		if (bitRangeEnd > fileLenInBits)
			throw new IllegalArgumentException("botOffset + bitLength exceeds the file lengths.");

		long byteRangeStart = bitOffset / 8; //index of first byte to extract
		long byteRangeEnd   = bitRangeEnd / 8; if (bitRangeEnd % 8 != 0) byteRangeEnd++; //index of byte behind the last byte to extract
		int byteRangeLen    = (int)(byteRangeEnd - byteRangeStart);

		byte[] buf = read(filename, byteRangeStart, byteRangeLen);
		
		return extract(buf, (int)(bitOffset % 8), bitLength);
	}

	/**
	 * Reads as many bytes from the specified file as fit into the buffer,
	 * starting to read at the specified position. If the buffer can't be
	 * filled, an exception is thrown.
	 */
	private final static byte[] read(String filename, long fileOffset, int length) throws IOException {
		byte[] buffer = CompleteReader.read(filename, fileOffset, length);

		if (buffer.length != length)
			throw new EOFException("couldn't read " + length + " bytes. read only " + buffer.length + " bytes.");
		
		return buffer;
	}

	/**
	 * Returns the value of the bit at position bitIndex in the specified byte
	 * array.
	 */
	private static final boolean getBitOfBArray(byte[] ba, int bitIndex) {
		return (ba[bitIndex / 8] & (0x80 >> (bitIndex % 8))) != 0;
	}	

	/**
	 * XORs the second byte array onto the first.
	 */
	public static final void xor(byte[] B, byte[] b) {
		//take the lesser of the two lengths
		int len = B.length < b.length ? B.length : b.length;
		
		//do the XORing
		for (int i = 0; i < len; i++)
			B[i] = (byte)(B[i] ^ b[i]);
	}
	
	/**
	 * @return int at specified pos in network byte order (big endian)
	 */
	public static int getInt(byte[] data, int pos) {
		return
			((data[pos++] & 0xff) << 24) |
			((data[pos++] & 0xff) << 16) |
			((data[pos++] & 0xff) <<  8) |
			((data[pos++] & 0xff)      )
		;
	}

	/**
	 * Converts an array of bytes to a string, converting all byte values < 0x20
	 * to a dot.
	 */
	public static String toString(byte[] b) {
		char[] c = new char[b.length];

		int x;
		for (int i = 0; i < b.length; i++) {
			x = b[i] & 0xff;
			c[i] = x < 32 ? '.' : (char)x;
		}
		
		return new String(c);
	}

	/**
	 * Converts an array of bytes to a binary string, most significant bit
	 * first. Every bit is converted to exactly one character.
	 */
	public static String toBinaryString(byte[] b) {
		//char[] c = new char[b.length * 9];
		char[] c = new char[b.length * 8];

		int j = 0;
		for (int i = 0; i < b.length; i++) {
			int value = b[i] & 0xff;
			c[j++] = ((value & 0x80) == 0) ? '0' : '1';
			c[j++] = ((value & 0x40) == 0) ? '0' : '1';
			c[j++] = ((value & 0x20) == 0) ? '0' : '1';
			c[j++] = ((value & 0x10) == 0) ? '0' : '1';
			c[j++] = ((value & 0x08) == 0) ? '0' : '1';
			c[j++] = ((value & 0x04) == 0) ? '0' : '1';
			c[j++] = ((value & 0x02) == 0) ? '0' : '1';
			c[j++] = ((value & 0x01) == 0) ? '0' : '1';
/*
			if (i % 8 == 7)
				c[j++] = '\n';
			//else
			//	c[j++] = ' ';
*/
		}
		
//		if (c[j - 1] != '0' && c[j - 1] != '1')
//			j--;

		return new String(c, 0, j);
	}

	/**
	 * Converts an int to a binary string, most significant byte and bit first.
	 * All 32 bits of the int are converted to exactly one character each.
	 */
	public static String toBinaryString(int value) {
		char[] c = new char[32];
		
		int mask = 0x80000000;
		for (int i = 0; i < 32; i++) {
			c[i] = ((value & mask) == 0) ? '0' : '1';
			mask >>>= 1;
		}
		
		return new String(c);
	}

	/**
	 * Pads the specified String to the specified length using spaces and
	 * returns the result.
	 */
	public static final String pad(String s, int length) {
		if (s.length() >= length)
			return s;

		char[] c = new char[length];
		int pads = length - s.length();
		int i = 0;
		for (; i < pads; i++)
			c[i] = ' ';
		for (; i < length; i++)
			c[i] = s.charAt(i - pads);
		
		return new String(c);
	}
}

