import java.util.Arrays;



public class TIB3_512 implements TIB3Hash{
	public static final int STATE_WORDS = 8;
	public static final int BLOCK_WORDS = 16;
	public static final int BLOCK_BYTES = 128;
	public static final int BLOCK_BITS = 1024;
	
	public static final boolean DEBUG = false;
	public int roundNumber = 0;

	private int hashbitlen;
	private BitLen version;
	private long bits_processed;
	private int bits_waiting_for_process;
	private long[] state;
	private byte[] buffer;
	private long[] data_block;
	private long[] previous_block;
	private long[] D;
	private long[] temp;
	
	/* Bitmask for zeroing the unused bits of the last byte of the message */
	private static final byte[] BITMASK = {0x00, (byte) 0x80, (byte) 0xc0, (byte) 0xe0, (byte) 0xf0, (byte) 0xf8, (byte) 0xfc, (byte) 0xfe};
	/* Bitmask for adding a 1 after the last bit of the message */
	private static final byte[] BITPADDING = { (byte) 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };

	private static final long[]  i512 = 
	{
	    0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL,
	    0x3c6ef372fe94f82bL, 0xa54ff53a5f1d36f1L,
	    0x510e527fade682d1L, 0x9b05688c2b3e6c1fL,
	    0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L
	};
	
	
	
	TIB3_512(BitLen len){
		this.version = len;
		switch(len){
		case TIB3_384:
			hashbitlen = 384;
			break;
		case TIB3_512:
			hashbitlen = 512;
			break;
		default: throw new UnsupportedOperationException();
		}
		buffer = new byte[BLOCK_BYTES];
		state = new long[STATE_WORDS];
		temp = new long[STATE_WORDS];
		data_block = new long[BLOCK_WORDS];
		previous_block = new long[BLOCK_WORDS];
		D = new long[BLOCK_WORDS*4];
		init();
	}
	
	public void init(){
		bits_processed = 0;
		bits_waiting_for_process = 0;
		switch(hashbitlen){
		case 512:
			System.arraycopy(i512, 0, state, 0, STATE_WORDS);
			System.arraycopy(i512, 4, previous_block, 0, 4);
			System.arraycopy(i512, 0, previous_block, 4, 8);
			System.arraycopy(i512, 0, previous_block, 12, 4);
			break;
		case 384:
			System.arraycopy(i512, 4, state, 0, 4);
			System.arraycopy(i512, 0, state, 4, 4);
			System.arraycopy(i512, 0, previous_block, 0, STATE_WORDS);
			System.arraycopy(i512, 0, previous_block, STATE_WORDS, STATE_WORDS);
			break;
		}
	}
	
	public void update(byte[] data, int databitlen){
		int index = bits_waiting_for_process >>> 3;
		int bytelen = databitlen >>> 3;
		int remaining = buffer.length - index;
		int offset = 0;
		if (index != 0 && bytelen >= remaining){
			System.arraycopy(data,0,buffer,index,remaining);
			bits_processed+=BLOCK_BITS;
			copybuffer(buffer,0);
			transform();
			offset += remaining;
			index = 0;
			bits_waiting_for_process = 0;
		}
		while (offset + BLOCK_BYTES <= bytelen) {
			copybuffer(data,offset);
			bits_processed+=BLOCK_BITS;
			transform();
			offset += BLOCK_BYTES;
		}
		if ((databitlen & 7) != 0)
			bytelen++; /* The number of bits is not a multiple of 8. This can only happen in the last call to update */

		/* If there are still bytes in data, we copy them to the buffer */
		System.arraycopy(data, offset, buffer, index, bytelen - offset);
		bits_waiting_for_process += (databitlen - 8 * offset);
	}
	
	
	
	private final void copybuffer(byte[] buffer, int index ){
		for (int i = 0; i < data_block.length; i++, index+=8){
			data_block[i] = (buffer[index] & 0xFF) |(buffer[index+1] & 0xFF) << 8 | (buffer[index+2] & 0xFF) << 16 | ((long)buffer[index+3] & 0xFF) <<24
					| ((long)buffer[index+4] &0xFF) << 32 | ((long)buffer[index+5] & 0xFF) << 40 | ((long)buffer[index+6] & 0xFF) << 48 | ((long)buffer[index+7]  & 0xFF) << 56;
		}
	}
	
	private void printState(String stage, int a, int c, int e, int g){
		System.out.printf("%-12s:0x%016x 0x%016x 0x%016x 0x%016x\n", stage, state[a], state[c], state[e], state[g]);
	}

	
	
	private void transform() {
		if(DEBUG){
			System.out.println("Start Encrypt:   LEFT KEY            RIGHT KEY");
			for(int i = 0; i < data_block.length; i++)
				System.out.printf("              0x%016x 0x%016x\n", data_block[i], previous_block[i]);
			System.out.println("State       :        A/E                B/F              C/G                 D/H ");
		}
		for (int i = 0; i < BLOCK_WORDS; i++)
			D[i] = data_block[i] ^ previous_block[i];
		PHI(BLOCK_WORDS,BLOCK_WORDS+1,(D[6]^previous_block[0]),(D[7]^previous_block[1]),(D[8]^previous_block[2]),(D[9]^previous_block[3]),(D[10]^previous_block[4]),(D[11]^previous_block[5]),(D[2]^previous_block[6]),(D[3]^previous_block[7]));
	    PHI(BLOCK_WORDS+2,BLOCK_WORDS+3,(D[4]^0x428a2f98d728ae22L^previous_block[8]),(D[5]^previous_block[9]),(D[14]^bits_processed^previous_block[10]),(D[15]^previous_block[11]),(D[12]^previous_block[14]),(D[13]^previous_block[15]),(D[0]^previous_block[12]),(D[1]^previous_block[13]));
	    for(int i = BLOCK_WORDS+4; i < D.length; i=i+2) {
	        PHI(i,i+1,D[i-20],D[i-19],D[i-16],D[i-15],D[i-6],D[i-5],D[i-4],D[i-3]);
	    }
		System.arraycopy(state, 0, temp, 0, STATE_WORDS);
		
		round( 0,2,4,6, D[ 0], D[ 1], data_block[ 0], data_block[ 1], D[ 2], D[ 3]);
		round( 2,4,6,0, D[ 4], D[ 5], data_block[ 2], data_block[ 3], D[ 6], D[ 7]);
		round( 4,6,0,2, D[ 8], D[ 9], data_block[ 4], data_block[ 5], D[10], D[11]);
		round( 6,0,2,4, D[12], D[13], data_block[ 6], data_block[ 7], D[14], D[15]);

		round( 0,2,4,6, D[16], D[17], data_block[ 8], data_block[ 9], D[18], D[19]);
		round( 2,4,6,0, D[20], D[21], data_block[10], data_block[11], D[22], D[23]);
		round( 4,6,0,2, D[24], D[25], data_block[12], data_block[13], D[26], D[27]);
		round( 6,0,2,4, D[28], D[29], data_block[14], data_block[15], D[30], D[31]);

		round( 0,2,4,6, previous_block[ 0], previous_block[ 1], D[32], D[33], previous_block[ 2], previous_block[ 3]);
		round( 2,4,6,0, previous_block[ 4], previous_block[ 5], D[34], D[35], previous_block[ 6], previous_block[ 7]);
		round( 4,6,0,2, previous_block[ 8], previous_block[ 9], D[36], D[37], previous_block[10], previous_block[11]);
		round( 6,0,2,4, previous_block[12], previous_block[13], D[38], D[39], previous_block[14], previous_block[15]);

		round( 0,2,4,6, D[40], D[41], D[42], D[43], D[44], D[45]);
		round( 2,4,6,0, D[46], D[47], D[48], D[49], D[50], D[51]);
		round( 4,6,0,2, D[52], D[53], D[54], D[55], D[56], D[57]);
		round( 6,0,2,4, D[58], D[59], D[60], D[61], D[62], D[63]);
		if (DEBUG){
			printState("End Encrypt", 0, 1, 2, 3);
			printState("",4,5,6,7);
		}
		for(int i = 0; i < STATE_WORDS; i++)
			state[i]^=temp[i];
		if (DEBUG){
			printState("Davies-Meyer", 0, 1, 2, 3);
			printState("",4,5,6,7);
		}
		long[] t = data_block;
		data_block = previous_block;
		previous_block = t;
	}

	private void sbox(int m0, int m1, int m2){
		long temp0, temp1;
		temp0 = (~state[m0])^(state[m1]&(~state[m2]));
		temp1 = state[m2]^((~state[m0])&(~state[m1]));
		state[m2] = state[m1]^(state[m0]&state[m2]);
		state[m0] = temp0;
		state[m1] = temp1;
	}
	
	
	private void round(int a, int c, int e, int g, long k0, long k1, long k2, long k3,  long k4, long k5){
		int b = a+1; int d=c+1; int f=e+1; int h=g+1;
		if (DEBUG){
			System.out.printf("Round # %2d:\n", roundNumber+1);
			roundNumber = (roundNumber+1) & 0xF;
			printState("Start", a, b, c, d);
			printState("", e, f, g, h);
		}
		state[a] ^= k0;
		state[b] ^= k1;
		state[c] ^= k2;
		state[d] ^= k3;
		state[e] ^= k4;
		state[f] ^= k5;
		state[g] ^= state[c];
		state[h] ^= state[d];
		if (DEBUG){
			printState("kymx+CDxorGH", a, b, c, d);
			printState("", e, f, g, h);
		}
		phtxd(g,h);
		sbox(a,c,e);
		sbox(b,d,f);
		if (DEBUG){
			printState("sbx+phtx(GH)", a, b, c, d);
			printState("", e, f, g, h);
		}
		state[a]+=state[g];
		state[b]+=state[h];
		phtxd(c,d);
		state[g]+=state[e];
		state[h]+=state[f];
		if (DEBUG){
			printState("end wout rot", a, b, c, d);
			printState("", e, f, g, h);
		}
	}

	private void phtxd(int l, int h){
		state[h]^=state[l];
		phtx(h);
		state[l]^=state[h];
		phtx(l);
	}
	private void phtx(int d){
		state[d] += (state[d]<<32) + (state[d] << 47);
		state[d] ^= (state[d]>>>32) ^ (state[d] >>>43);
	}
	private void PHI(int wi, int wim1, long wi20, long wi19, long wi16, long wi15, long wi6, long wi5, long wi4, long wi3) {
    	D[wi]=wi20^wi16^wi6^wi3;
    	D[wim1]=((wi5+wi4)^wi19^wi15)+D[wi]+(D[wi]<<23);
    	D[wi]^=(D[wim1]>>>15);
	}
	
	public byte[] doFinal(){
		byte[] digest = new byte[hashbitlen>>3];
		
		if (bits_waiting_for_process > 0) {
			/* If bits_waiting_for_process is not a multiple of 8, index will be the last byte,
			 * and bits will be the number of valid bits in that byte.
			 */
			int index = bits_waiting_for_process >> 3;
			int bits =  bits_waiting_for_process & 7;

			/* We zero the unused bits of the last byte, and set the fist unused bit to 1 */
			buffer[index] = (byte) ((buffer[index] & BITMASK[bits]) | (BITPADDING[bits]));
			index++;
			Arrays.fill(buffer, index, buffer.length, (byte)0); /* We fill the rest of the block with zeros */

			/*the usual update of bits processed cannot be used here, since
			 *less than a full block of DATA has been processed
			 */
			bits_processed += bits_waiting_for_process; 
			copybuffer(buffer, 0);
			transform();
		} /*endif(bits_waiting_for_process)*/
	   
		/* All data (plus any padding) has been processed. A last call, so that the last block
		 * of data is treated like all the others (i.e., twice) is made. The new block is simply the number of
		 * bits processed, xor the last state. 
		 */
		data_block[0]=bits_processed;
		for(int j=1;j < STATE_WORDS;j++) data_block[j]=0; /* The size of the block is twice the size of the state */
		System.arraycopy(state, 0, data_block, STATE_WORDS, STATE_WORDS);
		bits_processed = 0; /* This ensures that the final transform is the same regardless of the bits processed */
		
		transform();
		
		/* We output the appropriate number of bytes, truncating the state if necessary */
		for (int j = 0; j < (hashbitlen >> 3); j++){
			int i = j >>> 3;
			int k = j & 7;
			digest[j] = (byte) ((state[i] >>> (8*k)) & 0xFF);
		}
		return digest;
	}

	
	public byte[] hash(byte[] data, int bitlen){
		init();
		update(data,bitlen);
		return doFinal();
	}
	
	
	public final String toString(){
		return version.toString();
	}

}
