/* 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.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.Arrays;

import freenet.crypt.CryptFormatException;
import freenet.crypt.DSAPublicKey;
import freenet.crypt.SHA256;
import freenet.io.WritableToDataOutputStream;
import freenet.support.Fields;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.SimpleReadOnlyArrayBucket;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.compress.CompressionOutputSizeException;
import freenet.support.compress.InvalidCompressionCodecException;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.io.ArrayBucket;
import freenet.support.io.ArrayBucketFactory;
import freenet.support.io.BucketTools;

/**
 * @author amphibian
 *
 * Base class for node keys.
 * 
 * WARNING: Changing non-transient members on classes that are Serializable can result in 
 * restarting downloads or losing uploads.
 */
public abstract class Key implements WritableToDataOutputStream, Comparable<Key> {

    final int hash;
    double cachedNormalizedDouble;
    /** Whatever its type, it will need a routingKey ! */
    final byte[] routingKey;

    /** Code for 256-bit AES with PCFB and SHA-256 */
    public static final byte ALGO_AES_PCFB_256_SHA256 = 2;
    public static final byte ALGO_AES_CTR_256_SHA256 = 3;

    private static volatile boolean logMINOR;
    static {
        Logger.registerLogThresholdCallback(new LogThresholdCallback() {

            @Override
            public void shouldUpdate() {
                logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
            }
        });
    }

    protected Key(byte[] routingKey) {
    	this.routingKey = routingKey;
    	hash = Fields.hashCode(routingKey);
        cachedNormalizedDouble = -1;
    }

    protected Key(Key key) {
    	this.hash = key.hash;
    	this.cachedNormalizedDouble = key.cachedNormalizedDouble;
    	this.routingKey = new byte[key.routingKey.length];
    	System.arraycopy(key.routingKey, 0, routingKey, 0, routingKey.length);
    }

    public abstract Key cloneKey();

    /**
     * Write to disk.
     * Take up exactly 22 bytes.
     * @param _index
     */
    public abstract void write(DataOutput _index) throws IOException;

    /**
     * Read a Key from a RandomAccessFile
     * @param raf The file to read from.
     * @return a Key, or throw an exception, or return null if the key is not parsable.
     */
    public static Key read(DataInput raf) throws IOException {
    	byte type = raf.readByte();
    	byte subtype = raf.readByte();
        if(type == NodeCHK.BASE_TYPE) {
            return NodeCHK.readCHK(raf, subtype);
        } else if(type == NodeSSK.BASE_TYPE)
        	return NodeSSK.readSSK(raf, subtype);

        throw new IOException("Unrecognized format: "+type);
    }

	public static KeyBlock createBlock(short keyType, byte[] keyBytes, byte[] headersBytes, byte[] dataBytes, byte[] pubkeyBytes) throws KeyVerifyException {
		byte type = (byte)(keyType >> 8);
		byte subtype = (byte)(keyType & 0xFF);
		if(type == NodeCHK.BASE_TYPE) {
			// For CHKs, the subtype is the crypto algorithm.
			return CHKBlock.construct(dataBytes, headersBytes, subtype);
		} else if(type == NodeSSK.BASE_TYPE) {
			DSAPublicKey pubKey;
			try {
				pubKey = new DSAPublicKey(pubkeyBytes);
			} catch (IOException e) {
				throw new KeyVerifyException("Failed to construct pubkey: "+e, e);
			} catch (CryptFormatException e) {
				throw new KeyVerifyException("Failed to construct pubkey: "+e, e);
			}
			NodeSSK key = new NodeSSK(pubKey.asBytesHash(), keyBytes, pubKey, subtype);
			return new SSKBlock(dataBytes, headersBytes, key, false);
		} else {
			throw new KeyVerifyException("No such key type "+Integer.toHexString(type));
		}
	}

    /**
     * Convert the key to a double between 0.0 and 1.0.
     * Normally we will hash the key first, in order to
     * make chosen-key attacks harder.
     */
    public synchronized double toNormalizedDouble() {
        if(cachedNormalizedDouble > 0) return cachedNormalizedDouble;
        MessageDigest md = SHA256.getMessageDigest();
        if(routingKey == null) throw new NullPointerException();
        md.update(routingKey);
        int TYPE = getType();
        md.update((byte)(TYPE >> 8));
        md.update((byte)TYPE);
        byte[] digest = md.digest();
        SHA256.returnMessageDigest(md); md = null;
        long asLong = Math.abs(Fields.bytesToLong(digest));
        // Math.abs can actually return negative...
        if(asLong == Long.MIN_VALUE)
            asLong = Long.MAX_VALUE;
        cachedNormalizedDouble = ((double)asLong)/((double)Long.MAX_VALUE);
        return cachedNormalizedDouble;
    }

	/**
	 * Get key type
	 * <ul>
	 * <li>
	 * High 8 bit (<tt>(type >> 8) & 0xFF</tt>) is the base type ({@link NodeCHK#BASE_TYPE} or
	 * {@link NodeSSK#BASE_TYPE}).
	 * <li>Low 8 bit (<tt>type & 0xFF</tt>) is the crypto algorithm. (Currently only
	 * {@link #ALGO_AES_PCFB_256_SHA256} is supported).
	 * </ul>
	 */
	public abstract short getType();

	@Override
    public int hashCode() {
        return hash;
    }

	@Override
    public boolean equals(Object o){
    	if(o == null || !(o instanceof Key)) return false;
    	return Arrays.equals(routingKey, ((Key)o).routingKey);
    }

    static Bucket decompress(boolean isCompressed, byte[] input, int inputLength, BucketFactory bf, long maxLength, short compressionAlgorithm, boolean shortLength) throws CHKDecodeException, IOException {
	    if(maxLength < 0)
		    throw new IllegalArgumentException("maxlength="+maxLength);
		if(input.length < inputLength)
			throw new IndexOutOfBoundsException(""+input.length+"<"+inputLength);
        if(isCompressed) {
        	if(logMINOR)
        		Logger.minor(Key.class, "Decompressing "+inputLength+" bytes in decode with codec "+compressionAlgorithm);
			final int inputOffset = (shortLength ? 2 : 4);
            if(inputLength < inputOffset + 1) throw new CHKDecodeException("No bytes to decompress");
            // Decompress
            // First get the length
            int len;
            if(shortLength)
            	len = ((input[0] & 0xff) << 8) + (input[1] & 0xff);
            else
            	len = ((((((input[0] & 0xff) << 8) + (input[1] & 0xff)) << 8) + (input[2] & 0xff)) << 8) +
            		(input[3] & 0xff);
            if(len > maxLength)
                throw new TooBigException("Invalid precompressed size: "+len + " maxlength="+maxLength);
            COMPRESSOR_TYPE decompressor = COMPRESSOR_TYPE.getCompressorByMetadataID(compressionAlgorithm);
            if (decompressor==null)
            	throw new CHKDecodeException("Unknown compression algorithm: "+compressionAlgorithm);
            InputStream inputStream = null;
            OutputStream outputStream = null;
            Bucket inputBucket = new SimpleReadOnlyArrayBucket(input, inputOffset, inputLength-inputOffset);
            Bucket outputBucket = bf.makeBucket(maxLength);
            outputStream = outputBucket.getOutputStream();
            inputStream = inputBucket.getInputStream();
            try {
            	decompressor.decompress(inputStream, outputStream, maxLength, -1);
			}  catch (CompressionOutputSizeException e) {
				throw new TooBigException("Too big");
			} finally {
            inputStream.close();
            outputStream.close();
            inputBucket.free();
			}
            return outputBucket;
        } else {
        	return BucketTools.makeImmutableBucket(bf, input, inputLength);
        }
	}

    static class Compressed {
    	public Compressed(byte[] finalData, short compressionAlgorithm2) {
    		this.compressedData = finalData;
    		this.compressionAlgorithm = compressionAlgorithm2;
		}
		byte[] compressedData;
    	short compressionAlgorithm;
    }

    /**
     * Compress data.
     * @param sourceData
     * @param dontCompress
     * @param alreadyCompressedCodec
     * @param sourceLength
     * @param MAX_LENGTH_BEFORE_COMPRESSION
     * @param MAX_COMPRESSED_DATA_LENGTH
     * @param shortLength
     * @param compressordescriptor
     * @param pre1254 If this is set, use OldLZMA not NewLZMA.
     * @return
     * @throws KeyEncodeException
     * @throws IOException
     * @throws InvalidCompressionCodecException
     */
    static Compressed compress(Bucket sourceData, boolean dontCompress, short alreadyCompressedCodec, long sourceLength, long MAX_LENGTH_BEFORE_COMPRESSION, int MAX_COMPRESSED_DATA_LENGTH, boolean shortLength, String compressordescriptor, boolean pre1254) throws KeyEncodeException, IOException, InvalidCompressionCodecException {
    	byte[] finalData = null;
        short compressionAlgorithm = -1;
        int maxCompressedDataLength = MAX_COMPRESSED_DATA_LENGTH;
        if(shortLength)
        	maxCompressedDataLength -= 2;
        else
        	maxCompressedDataLength -= 4;
        if(sourceData.size() > MAX_LENGTH_BEFORE_COMPRESSION)
            throw new KeyEncodeException("Too big");
        if((!dontCompress) || (alreadyCompressedCodec >= 0)) {
        	byte[] cbuf = null;
        	if(alreadyCompressedCodec >= 0) {
           		if(sourceData.size() > maxCompressedDataLength)
        			throw new TooBigException("Too big (precompressed)");
        		compressionAlgorithm = alreadyCompressedCodec;
        		cbuf = BucketTools.toByteArray(sourceData);
        		if(sourceLength > MAX_LENGTH_BEFORE_COMPRESSION)
        			throw new TooBigException("Too big");
        	} else {
        		if (sourceData.size() > maxCompressedDataLength) {
					// Determine the best algorithm
        			COMPRESSOR_TYPE[] comps = COMPRESSOR_TYPE.getCompressorsArray(compressordescriptor, pre1254);
					for (COMPRESSOR_TYPE comp : comps) {
						ArrayBucket compressedData;
						try {
							compressedData = (ArrayBucket) comp.compress(
									sourceData, new ArrayBucketFactory(), Long.MAX_VALUE, maxCompressedDataLength);
						} catch (CompressionOutputSizeException e) {
							continue;
						}
						if (compressedData.size() <= maxCompressedDataLength) {
							compressionAlgorithm = comp.metadataID;
							sourceLength = sourceData.size();
							try {
								cbuf = BucketTools.toByteArray(compressedData);
								// FIXME provide a method in ArrayBucket
							} catch (IOException e) {
								throw new Error(e);
							}
							break;
						}
					}
				}

        	}
        	if(cbuf != null) {
    			// Use it
    			int compressedLength = cbuf.length;
                finalData = new byte[compressedLength+(shortLength?2:4)];
                System.arraycopy(cbuf, 0, finalData, shortLength?2:4, compressedLength);
                if(!shortLength) {
                	finalData[0] = (byte) ((sourceLength >> 24) & 0xff);
                	finalData[1] = (byte) ((sourceLength >> 16) & 0xff);
                	finalData[2] = (byte) ((sourceLength >> 8) & 0xff);
                	finalData[3] = (byte) ((sourceLength) & 0xff);
                } else {
                	finalData[0] = (byte) ((sourceLength >> 8) & 0xff);
                	finalData[1] = (byte) ((sourceLength) & 0xff);
                }
        	}
        }
        if(finalData == null) {
        	// Not compressed or not compressible; no size bytes to be added.
            if(sourceData.size() > MAX_COMPRESSED_DATA_LENGTH) {
                throw new CHKEncodeException("Too big: "+sourceData.size()+" should be "+MAX_COMPRESSED_DATA_LENGTH);
            }
        	finalData = BucketTools.toByteArray(sourceData);
        }

        return new Compressed(finalData, compressionAlgorithm);
    }

    public byte[] getRoutingKey() {
    	return routingKey;
    }

    // Not just the routing key, enough data to reconstruct the key (excluding any pubkey needed)
    public byte[] getKeyBytes() {
    	return routingKey;
    }

	public static ClientKeyBlock createKeyBlock(ClientKey key, KeyBlock block) throws KeyVerifyException {
		if(key instanceof ClientSSK)
			return ClientSSKBlock.construct((SSKBlock)block, (ClientSSK)key);
		else //if(key instanceof ClientCHK
			return new ClientCHKBlock((CHKBlock)block, (ClientCHK)key);
	}

	/** Get the full key, including any crypto type bytes, everything needed to construct a Key object */
	public abstract byte[] getFullKey();

	/** Get a copy of the key with any unnecessary information stripped, for long-term
	 * in-memory storage. E.g. for SSKs, strips the DSAPublicKey. Copies it whether or
	 * not we need to copy it because the original might pick up a pubkey after this
	 * call. And the returned key will not accidentally pick up extra data. */
	public abstract Key archivalCopy();

    public static boolean isValidCryptoAlgorithm(byte cryptoAlgorithm) {
        return cryptoAlgorithm == ALGO_AES_PCFB_256_SHA256 ||
            cryptoAlgorithm == ALGO_AES_CTR_256_SHA256;
    }
}
