/* 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.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;

import freenet.support.Base64;
import freenet.support.ByteArrayWrapper;
import freenet.support.Fields;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;

/**
 * Client level CHK. Can be converted into a FreenetURI, can be used to decrypt
 * a CHKBlock, can be produced by a CHKBlock. 
 */
public class ClientCHK extends ClientKey implements Serializable {
    
    private static final long serialVersionUID = 1L;
    /** Lazily constructed: the NodeCHK */
    transient NodeCHK nodeKey;
    /** Routing key */
    final byte[] routingKey;
    /** Decryption key */
    final byte[] cryptoKey;
    /** Is the data a control document? */
    final boolean controlDocument;
    /** Encryption algorithm */
    final byte cryptoAlgorithm;
    /** Compression algorithm, negative means uncompressed */
    final short compressionAlgorithm;
    final int hashCode;
    
    /* We use EXTRA_LENGTH above for consistency, rather than dis.read etc. Some code depends on this
     * being accurate. Change those uses if you like. */
    /** The length of the "extra" bytes in the key */
    public static final short EXTRA_LENGTH = 5;
    /** The length of the decryption key */
    public static final short CRYPTO_KEY_LENGTH = 32;
    
    /** Useful for e.g. length checks */
    public static final ClientCHK TEST_KEY;
    
    static {
        try {
            TEST_KEY = new ClientCHK(FreenetURI.generateRandomCHK(new Random()));
        } catch (MalformedURLException e) {
            throw new Error(e);
        }
    }
    
    private ClientCHK(ClientCHK key) {
    	this.routingKey = key.routingKey.clone();
    	this.nodeKey = null;
    	this.cryptoKey = key.cryptoKey.clone();
    	this.controlDocument = key.controlDocument;
    	this.cryptoAlgorithm = key.cryptoAlgorithm;
    	this.compressionAlgorithm = key.compressionAlgorithm;
        hashCode = key.hashCode;
    }
    
    /**
     * @param routingKey The routing key. This is the overall hash of the
     * header and content of the key.
     * @param encKey The decryption key. This is not passed to other nodes
     * and is extracted from the URI.
     * @param isCompressed True if the data was gzipped before encoding.
     * @param isControlDocument True if the document is a Control Document.
     * These carry metadata, whereas ordinary keys carry data, and have no
     * type.
     * @param algo The encryption algorithm's identifier. See ALGO_* for 
     * values.
     */
    public ClientCHK(byte[] routingKey, byte[] encKey,  
            boolean isControlDocument, byte algo, short compressionAlgorithm) {
        this.routingKey = routingKey;
        this.cryptoKey = encKey;
        this.controlDocument = isControlDocument;
        this.cryptoAlgorithm = algo;
        this.compressionAlgorithm = compressionAlgorithm;
        if(routingKey == null) throw new NullPointerException();
        hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(encKey) ^ compressionAlgorithm;
    }

    public ClientCHK(byte[] routingKey, byte[] encKey, byte[] extra) throws MalformedURLException {
    	this.routingKey = routingKey;
    	this.cryptoKey = encKey;
        if((extra == null) || (extra.length < 5))
            throw new MalformedURLException("No extra bytes in CHK - maybe a 0.5 key?");
        // byte 0 is reserved, for now
        cryptoAlgorithm = extra[1];
		if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
			throw new MalformedURLException("Invalid crypto algorithm");
        controlDocument = (extra[2] & 0x02) != 0;
        compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
        hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
    }
    
    /**
     * Create from a URI.
     */
    public ClientCHK(FreenetURI uri) throws MalformedURLException {
        if(!uri.getKeyType().equals("CHK"))
            throw new MalformedURLException("Not CHK");
        routingKey = uri.getRoutingKey();
        cryptoKey = uri.getCryptoKey();
        byte[] extra = uri.getExtra();
        if((extra == null) || (extra.length < 5))
            throw new MalformedURLException("No extra bytes in CHK - maybe a 0.5 key?");
        // byte 0 is reserved, for now
        cryptoAlgorithm = extra[1];
		if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
			throw new MalformedURLException("Invalid crypto algorithm");
        controlDocument = (extra[2] & 0x02) != 0;
        compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
        hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
    }

    /**
     * Create from a raw binary CHK. This expresses the key information
     * in as few bytes as possible.
     * @throws IOException 
     */
	public ClientCHK(DataInputStream dis) throws IOException {
		byte[] extra = new byte[EXTRA_LENGTH];
		dis.readFully(extra);
		// byte 0 is reserved, for now
        cryptoAlgorithm = extra[1];
		if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
			throw new MalformedURLException("Invalid crypto algorithm");
        compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
        controlDocument = (extra[2] & 0x02) != 0;
		routingKey = new byte[NodeCHK.KEY_LENGTH];
		dis.readFully(routingKey);
		cryptoKey = new byte[CRYPTO_KEY_LENGTH];
		dis.readFully(cryptoKey);
        hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
	}
	
	protected ClientCHK() {
	    // Only for serialization.
	    routingKey = null;
	    cryptoKey = null;
	    controlDocument = false;
	    cryptoAlgorithm = 0;
	    compressionAlgorithm = 0;
	    hashCode = 0;
	}

	/**
	 * Write an ultra-compact representation.
	 * @throws IOException If a write failed.
	 */
	public void writeRawBinaryKey(DataOutputStream dos) throws IOException {
		dos.write(getExtra());
		dos.write(routingKey);
		dos.write(cryptoKey);
	}
    
	static byte[] lastExtra;
	
	public byte[] getExtra() {
		return getExtra(cryptoAlgorithm, compressionAlgorithm, controlDocument);
	}
	
	public static byte[] getExtra(byte cryptoAlgorithm, short compressionAlgorithm, boolean controlDocument) {
		byte[] extra = new byte[EXTRA_LENGTH];
		extra[0] = (byte) (cryptoAlgorithm >> 8);
		extra[1] = cryptoAlgorithm;
		extra[2] = (byte) (controlDocument ? 2 : 0);
		extra[3] = (byte) (compressionAlgorithm >> 8);
		extra[4] = (byte) compressionAlgorithm;
		byte[] last = lastExtra;
		// No synchronization required IMHO
		if(Arrays.equals(last, extra)) return last;
		assert(extra.length == EXTRA_LENGTH);
		lastExtra = extra;
		return extra;
	}
	
	public static byte getCryptoAlgorithmFromExtra(byte[] extra) {
		return extra[1];
	}
	
	static HashSet<ByteArrayWrapper> standardExtras = new HashSet<ByteArrayWrapper>();
	static {
		for(byte cryptoAlgorithm = Key.ALGO_AES_PCFB_256_SHA256; cryptoAlgorithm <= Key.ALGO_AES_CTR_256_SHA256; cryptoAlgorithm++) {
			for(short compressionAlgorithm = -1; compressionAlgorithm <= (short)(COMPRESSOR_TYPE.countCompressors()); compressionAlgorithm++) {
				standardExtras.add(new ByteArrayWrapper(getExtra(cryptoAlgorithm, compressionAlgorithm, true)));
				standardExtras.add(new ByteArrayWrapper(getExtra(cryptoAlgorithm, compressionAlgorithm, false)));
			}
		}
	}
	
	public static byte[] internExtra(byte[] extra) {
		for(ByteArrayWrapper baw : standardExtras) {
			if(Arrays.equals(baw.get(), extra)) return baw.get();
		}
		return extra;
	}
	
    @Override
	public String toString() {
        return super.toString()+ ':' +Base64.encode(routingKey)+ ',' +
        	Base64.encode(cryptoKey)+ ',' +compressionAlgorithm+ ',' +controlDocument+
                ',' +cryptoAlgorithm;
    }

	@Override
	public Key getNodeKey(boolean cloneKey) {
		return cloneKey ? getNodeCHK().cloneKey() : getNodeCHK();
	}

	public synchronized NodeCHK getNodeCHK() {
		// This costs us more or less nothing: we have to keep the routingKey anyway.
		// Therefore, keeping a NodeCHK as well is a net saving, since it's frequently
		// asked for. (A SoftReference would cost more).
		if(nodeKey == null)
	        nodeKey = new NodeCHK(routingKey, cryptoAlgorithm);
	    return nodeKey;
	}
	
    /**
     * @return URI form of this key.
     */
    @Override
	public FreenetURI getURI() {
        byte[] extra = getExtra();
        return new FreenetURI("CHK", null, routingKey, cryptoKey, extra);
    }

    /**
     * Read a raw binary CHK. This is an ultra-compact representation, for
     * splitfile metadata etc.
     */
	public static ClientCHK readRawBinaryKey(DataInputStream dis) throws IOException {
		return new ClientCHK(dis);
	}

	public boolean isMetadata() {
		return controlDocument;
	}

	public boolean isCompressed() {
		return compressionAlgorithm >= 0;
	}

	@Override
	public ClientCHK cloneKey() {
		return new ClientCHK(this);
	}

	@Override
	public int hashCode() {
		return hashCode;
	}
	
	@Override
	public boolean equals(Object o) {
		if(!(o instanceof ClientCHK)) return false;
		ClientCHK key = (ClientCHK) o;
		if(controlDocument != key.controlDocument) return false;
		if(cryptoAlgorithm != key.cryptoAlgorithm) return false;
		if(compressionAlgorithm != key.compressionAlgorithm) return false;
		if(!Arrays.equals(routingKey, key.routingKey)) return false;
		if(!Arrays.equals(cryptoKey, key.cryptoKey)) return false;
		return true;
	}

	public byte[] getRoutingKey() {
		return routingKey;
	}
	
	public byte[] getCryptoKey() {
		return cryptoKey;
	}
	
    public byte getCryptoAlgorithm() {
        return cryptoAlgorithm;
    }
}
