/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.keys;

import java.io.IOException;
import java.util.Arrays;

import freenet.crypt.PCFBMode;
import freenet.crypt.UnsupportedCipherException;
import freenet.crypt.ciphers.Rijndael;
import freenet.support.Logger;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.io.ArrayBucket;
import freenet.support.io.ArrayBucketFactory;
import freenet.support.io.BucketTools;

public class ClientSSKBlock implements ClientKeyBlock {
	
	static final int DATA_DECRYPT_KEY_LENGTH = 32;
	
	static public final int MAX_DECOMPRESSED_DATA_LENGTH = 32768;
	
	private final SSKBlock block;
	/** Is metadata. Set on decode. */
	private boolean isMetadata;
	/** Has decoded? */
	private boolean decoded;
	/** Client-key. This contains the decryption key etc. */
	private final ClientSSK key;

	/** Compression algorithm from last time tried to decompress. */
	private short compressionAlgorithm = -1;
	
	public ClientSSKBlock(byte[] data, byte[] headers, ClientSSK key, boolean dontVerify) throws SSKVerifyException {
		block = new SSKBlock(data, headers, (NodeSSK) key.getNodeKey(true), dontVerify);
		this.key = key;
	}
	
	public static ClientSSKBlock construct(SSKBlock block, ClientSSK key) throws SSKVerifyException {
		// Constructor expects clientkey to have the pubkey.
		// In the case of binary blobs, the block may have it instead.
		if(key.getPubKey() == null)
			key.setPublicKey(block.getPubKey());
		return new ClientSSKBlock(block.data, block.headers, key, false);
	}
	
	/**
	 * Decode the data.
	 */
	@Override
	public Bucket decode(BucketFactory factory, int maxLength, boolean dontDecompress) throws KeyDecodeException, IOException {
		/* We know the signature is valid because it is checked in the constructor. */
		/* We also know e(h(docname)) is valid */
		byte[] decryptedHeaders = new byte[SSKBlock.ENCRYPTED_HEADERS_LENGTH];
		System.arraycopy(block.headers, block.headersOffset, decryptedHeaders, 0, SSKBlock.ENCRYPTED_HEADERS_LENGTH);
		Rijndael aes;
		try {
			Logger.minor(this, "cryptoAlgorithm="+key.cryptoAlgorithm+" for "+getClientKey().getURI());
			aes = new Rijndael(256,256);
		} catch (UnsupportedCipherException e) {
			throw new Error(e);
		}
		aes.initialize(key.cryptoKey);
		// ECB-encrypted E(H(docname)) serves as IV.
		PCFBMode pcfb = PCFBMode.create(aes, key.ehDocname);
		pcfb.blockDecipher(decryptedHeaders, 0, decryptedHeaders.length);
		// First 32 bytes are the key
		byte[] dataDecryptKey = Arrays.copyOf(decryptedHeaders, DATA_DECRYPT_KEY_LENGTH);
		aes.initialize(dataDecryptKey);
		byte[] dataOutput = block.data.clone();
		// Data decrypt key should be unique, so use it as IV
		pcfb.reset(dataDecryptKey);
		pcfb.blockDecipher(dataOutput, 0, dataOutput.length);
		// 2 bytes - data length
		int dataLength = ((decryptedHeaders[DATA_DECRYPT_KEY_LENGTH] & 0xff) << 8) +
			(decryptedHeaders[DATA_DECRYPT_KEY_LENGTH+1] & 0xff);
		// Metadata flag is top bit
		if((dataLength & 32768) != 0) {
			dataLength = dataLength & ~32768;
			isMetadata = true;
		}
		if(dataLength > dataOutput.length) {
			throw new SSKDecodeException("Data length: "+dataLength+" but data.length="+dataOutput.length);
		}
		
        compressionAlgorithm = (short)(((decryptedHeaders[DATA_DECRYPT_KEY_LENGTH+2] & 0xff) << 8) + (decryptedHeaders[DATA_DECRYPT_KEY_LENGTH+3] & 0xff));
        decoded = true;
        
        if(dontDecompress) {
        	if(compressionAlgorithm == (short)-1)
        		return BucketTools.makeImmutableBucket(factory, dataOutput, dataLength);
        	else if(dataLength < 2)
        		throw new SSKDecodeException("Data length is less than 2 yet compressed!");
        	else
        		return BucketTools.makeImmutableBucket(factory, dataOutput, 2, dataLength - 2);
        }

        Bucket b = Key.decompress(compressionAlgorithm >= 0, dataOutput, dataLength, factory, Math.min(MAX_DECOMPRESSED_DATA_LENGTH, maxLength), compressionAlgorithm, true);
        return b;
	}

	@Override
	public boolean isMetadata() {
		if(!decoded)
			throw new IllegalStateException("Cannot read isMetadata before decoded");
		return isMetadata;
	}

	@Override
	public ClientSSK getClientKey() {
		return key;
	}

	public short getCompressionCodec() {
		return compressionAlgorithm;
	}
	
	@Override
	public byte[] memoryDecode() throws KeyDecodeException {
		return memoryDecode(false);
	}
	
    /**
     * Decode into RAM, if short.
     * @throws KeyDecodeException 
     */
	public byte[] memoryDecode(boolean dontDecompress) throws KeyDecodeException {
		try {
			ArrayBucket a = (ArrayBucket) decode(new ArrayBucketFactory(), 32*1024, dontDecompress);
			return BucketTools.toByteArray(a); // FIXME
		} catch (IOException e) {
			throw new Error(e);
		}
	}

	@Override
	public int hashCode() {
		return block.hashCode() ^ key.hashCode();
	}

	/** Return true if this is the same block as the other ClientSSKBlock, *and* it is the same key */
	@Override
	public boolean equals(Object o) {
		if(!(o instanceof ClientSSKBlock)) return false;
		ClientSSKBlock block = (ClientSSKBlock) o;
		if(!key.equals(block.key)) return false;
		return this.block.equals(block.block);
	}

	@Override
	public KeyBlock getBlock() {
		return block;
	}

	@Override
	public Key getKey() {
		return block.getKey();
	}
	
}
