package freenet.client.async;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;

import freenet.keys.ClientCHK;
import freenet.keys.NodeCHK;
import freenet.support.Logger;
import freenet.support.Fields;

/** Contains the keys for a splitfile segment, in an efficient compressed form. These are 
 * not changed, so the object never needs to be stored once created; this is good as it is
 * fairly large. SplitFileFetcherSegment keeps the data on which keys have been used 
 * separately and passes it in.
 * 
 * We will auto-upgrade SplitFileFetcherSegment's to use SplitFileSegmentKeys in the caller.
 * 
 * @author toad
 */
public class SplitFileSegmentKeys implements Cloneable, Serializable {
	
    private static final long serialVersionUID = 1L;
    public final int dataBlocks;
	public final int checkBlocks;
	/** Modern splitfiles have a common decrypt key */
	public final byte[] commonDecryptKey;
	/** Modern splitfiles have common extra bytes */
	public final byte[] commonExtraBytes;
	/** Routing keys. */
	public final byte[] routingKeys;
	/** Individual per-block decryption keys. Only on older splitfiles. */
	public final byte[] decryptKeys;
	/** Individual per-block extra bytes. */
	public final byte[] extraBytesForKeys;
	
	static final int EXTRA_BYTES_LENGTH = ClientCHK.EXTRA_LENGTH;

	// Bare constructor for Metadata. It will read the actual keys later.
	public SplitFileSegmentKeys(int blocksPerSegment, int checkBlocksPerSegment, byte[] splitfileSingleCryptoKey, byte splitfileSingleCryptoAlgorithm) {
		this.dataBlocks = blocksPerSegment;
		this.checkBlocks = checkBlocksPerSegment;
		routingKeys = new byte[NodeCHK.KEY_LENGTH * (dataBlocks + checkBlocks)];
		if(splitfileSingleCryptoKey != null) {
			commonDecryptKey = splitfileSingleCryptoKey;
			commonExtraBytes = ClientCHK.getExtra(splitfileSingleCryptoAlgorithm, (short)-1, false);
			decryptKeys = null;
			extraBytesForKeys = null;
		} else {
			commonDecryptKey = null;
			commonExtraBytes = null;
			decryptKeys = new byte[ClientCHK.CRYPTO_KEY_LENGTH * (dataBlocks + checkBlocks)];
			extraBytesForKeys = new byte[EXTRA_BYTES_LENGTH * (dataBlocks + checkBlocks)];
		}
	}
	
	protected SplitFileSegmentKeys() {
        // For serialization.
	    dataBlocks = 0;
	    checkBlocks = 0;
	    commonDecryptKey = null;
	    commonExtraBytes = null;
	    routingKeys = null;
	    decryptKeys = null;
	    extraBytesForKeys = null;
	}

	public int getBlockNumber(ClientCHK key, boolean[] ignoreSlots) {
		byte[] rkey = key.getRoutingKey();
		byte[] ckey = null;
		byte[] extra = null;
		int x = 0;
		for(int i=0;i<(dataBlocks + checkBlocks);i++) {
			int oldX = x;
			x += NodeCHK.KEY_LENGTH;
			if(ignoreSlots != null && ignoreSlots[i]) {
				continue;
			}
			if(!Fields.byteArrayEqual(routingKeys, rkey, oldX, 0, NodeCHK.KEY_LENGTH))
				continue;
			if(ckey == null) ckey = key.getCryptoKey();
			assert(ClientCHK.CRYPTO_KEY_LENGTH == NodeCHK.KEY_LENGTH);
			// FIXME USE THE RIGHT CONSTANT, DONT ASSUME THE TWO LENGTHS ARE THE SAME
			if(commonDecryptKey != null) {
				if(!Arrays.equals(commonDecryptKey, ckey)) continue;
			} else {
				if(!Fields.byteArrayEqual(decryptKeys,ckey,oldX,0,NodeCHK.KEY_LENGTH))
					continue;
			}
			if(extra == null) extra = key.getExtra();
			if(commonExtraBytes != null) {
				if(!Arrays.equals(commonExtraBytes, extra)) continue;
			} else {
				if(!Fields.byteArrayEqual(extraBytesForKeys, extra, i * EXTRA_BYTES_LENGTH, 0, EXTRA_BYTES_LENGTH))
					continue;
			}
			return i;
		}
		return -1;
	}
	
	public int getBlockNumber(NodeCHK key, boolean[] ignoreSlots) {
		byte[] rkey = key.getRoutingKey();
		int x = 0;
		for(int i=0;i<(dataBlocks + checkBlocks);i++) {
			int oldX = x;
			x += NodeCHK.KEY_LENGTH;
			if(ignoreSlots != null && ignoreSlots[i]) {
				continue;
			}
			if(!Fields.byteArrayEqual(routingKeys, rkey, oldX, 0, NodeCHK.KEY_LENGTH))
				continue;
			return i;
		}
		return -1;
	}
	
	public int[] getBlockNumbers(NodeCHK key, boolean[] ignoreSlots) {
		ArrayList<Integer> results = null;
		byte[] rkey = key.getRoutingKey();
		int x = 0;
		for(int i=0;i<(dataBlocks + checkBlocks);i++) {
			int oldX = x;
			x += NodeCHK.KEY_LENGTH;
			if(ignoreSlots != null && ignoreSlots[i]) {
				continue;
			}
			if(!Fields.byteArrayEqual(routingKeys, rkey, oldX, 0, NodeCHK.KEY_LENGTH))
				continue;
			if(results == null) results = new ArrayList<Integer>();
			results.add(i);
		}
		if(results == null) return new int[0];
		int[] ret = new int[results.size()];
		for(int i=0;i<ret.length;i++) ret[i] = results.get(i);
		return ret;
	}
	
	public NodeCHK getNodeKey(int x, boolean[] ignoreSlots, boolean copy) {
		if(ignoreSlots != null) {
			if(ignoreSlots[x]) return null;
		}
		return getNodeKey(x, copy);
	}
	
	public ClientCHK getKey(int x, boolean[] ignoreSlots, boolean copy) {
		if(ignoreSlots != null) {
			if(ignoreSlots[x]) return null;
		}
		return getKey(x, copy);
	}

	private ClientCHK getKey(int x, boolean copy) {
		byte[] routingKey = new byte[32];
		System.arraycopy(routingKeys, x * NodeCHK.KEY_LENGTH, routingKey, 0, NodeCHK.KEY_LENGTH);
		byte[] decryptKey;
		if(commonDecryptKey != null) {
			if(copy) {
				decryptKey = commonDecryptKey.clone();
			} else {
				decryptKey = commonDecryptKey;
			}
		} else {
			int offset = x * ClientCHK.CRYPTO_KEY_LENGTH;
			decryptKey = Arrays.copyOfRange(decryptKeys, offset, offset + ClientCHK.CRYPTO_KEY_LENGTH);
		}
		byte[] extra;
		if(commonExtraBytes != null) {
			if(copy) {
				extra = commonExtraBytes.clone();
			} else {
				extra = commonExtraBytes;
			}
		} else {
			int offset = x * EXTRA_BYTES_LENGTH;
			extra = Arrays.copyOfRange(extraBytesForKeys, offset, offset + EXTRA_BYTES_LENGTH);
		}
		try {
			return new ClientCHK(routingKey, decryptKey, extra);
		} catch (MalformedURLException e) {
			Logger.error(this, "Impossible: "+e);
			throw new IllegalStateException(e);
		}
	}

	private NodeCHK getNodeKey(int x, boolean copy) {
		int xr = x * NodeCHK.KEY_LENGTH;
		byte[] routingKey = Arrays.copyOfRange(routingKeys, xr, xr + NodeCHK.KEY_LENGTH);
		byte[] extra;
		if(commonExtraBytes != null) {
			extra = commonExtraBytes;
		} else {
			int xe = x * EXTRA_BYTES_LENGTH;
			extra = Arrays.copyOfRange(extraBytesForKeys, xe, xe + EXTRA_BYTES_LENGTH);
		}
		
		byte cryptoAlgorithm = ClientCHK.getCryptoAlgorithmFromExtra(extra);
		
		return new NodeCHK(routingKey, cryptoAlgorithm);
	}

	public void readKeys(DataInputStream dis, boolean check) throws IOException {
		int count = check ? checkBlocks : dataBlocks;
		int offset = check ? dataBlocks : 0;
		if(commonDecryptKey != null) {
			int rkOffset = offset * NodeCHK.KEY_LENGTH;
			for(int i=0;i<count;i++) {
				dis.readFully(routingKeys, rkOffset, NodeCHK.KEY_LENGTH);
				rkOffset += NodeCHK.KEY_LENGTH;
			}
		} else {
			int rkOffset = offset * NodeCHK.KEY_LENGTH;
			int extraOffset = offset * EXTRA_BYTES_LENGTH;
			assert(NodeCHK.KEY_LENGTH == ClientCHK.CRYPTO_KEY_LENGTH);
			for(int i=0;i<count;i++) {
				ClientCHK key = ClientCHK.readRawBinaryKey(dis);
				byte[] r = key.getRoutingKey();
				System.arraycopy(r, 0, routingKeys, rkOffset, NodeCHK.KEY_LENGTH);
				byte[] c = key.getCryptoKey();
				System.arraycopy(c, 0, decryptKeys, rkOffset, NodeCHK.KEY_LENGTH);
				rkOffset += NodeCHK.KEY_LENGTH;
				byte[] e = key.getExtra();
				System.arraycopy(e, 0, extraBytesForKeys, extraOffset, EXTRA_BYTES_LENGTH);
				extraOffset += EXTRA_BYTES_LENGTH;
			}
		}
	}

	public void writeKeys(DataOutputStream dos, boolean check) throws IOException {
		int count = check ? checkBlocks : dataBlocks;
		int offset = check ? dataBlocks : 0;
		if(commonDecryptKey != null) {
			int rkOffset = offset * NodeCHK.KEY_LENGTH;
			for(int i=0;i<count;i++) {
				dos.write(routingKeys, rkOffset, NodeCHK.KEY_LENGTH);
				rkOffset += NodeCHK.KEY_LENGTH;
			}
		} else {
			int rkOffset = offset * NodeCHK.KEY_LENGTH;
			int extraOffset = offset * EXTRA_BYTES_LENGTH;
			assert(NodeCHK.KEY_LENGTH == ClientCHK.CRYPTO_KEY_LENGTH);
			for(int i=0;i<count;i++) {
				dos.write(extraBytesForKeys, extraOffset, EXTRA_BYTES_LENGTH);
				extraOffset += EXTRA_BYTES_LENGTH;
				dos.write(routingKeys, rkOffset, NodeCHK.KEY_LENGTH);
				dos.write(decryptKeys, rkOffset, NodeCHK.KEY_LENGTH);
				rkOffset += NodeCHK.KEY_LENGTH;
			}
		}
	}
	
    public static int storedKeysLength(int dataBlocks, int checkBlocks, boolean commonDecryptKey) {
        // FIXME URGENT Implement a unit test for storedKeysLength() vs writeKeys() vs readKeys().
        int blocks = dataBlocks + checkBlocks;
        if(commonDecryptKey) {
            return blocks * NodeCHK.KEY_LENGTH;
        } else {
            return blocks * (EXTRA_BYTES_LENGTH + NodeCHK.KEY_LENGTH*2);
        }
    }

	public int getDataBlocks() {
		return dataBlocks;
	}

	public int getCheckBlocks() {
		return checkBlocks;
	}

	public void setKey(int i, ClientCHK key) {
		byte[] r = key.getRoutingKey();
		System.arraycopy(r, 0, routingKeys, i * NodeCHK.KEY_LENGTH, NodeCHK.KEY_LENGTH);
		if(decryptKeys != null) {
			byte[] c = key.getCryptoKey();
			System.arraycopy(c, 0, decryptKeys, i * ClientCHK.CRYPTO_KEY_LENGTH, ClientCHK.CRYPTO_KEY_LENGTH);
		}
		if(extraBytesForKeys != null) {
			byte[] e = key.getExtra();
			System.arraycopy(e, 0, extraBytesForKeys, i * EXTRA_BYTES_LENGTH, EXTRA_BYTES_LENGTH);
		}
	}

	public NodeCHK[] listNodeKeys(boolean[] foundKeys, boolean copy) {
		ArrayList<NodeCHK> list = new ArrayList<NodeCHK>();
		for(int i=0;i<dataBlocks+checkBlocks;i++) {
			NodeCHK k = getNodeKey(i, foundKeys, copy);
			if(k == null) continue;
			list.add(k);
		}
		return list.toArray(new NodeCHK[list.size()]);
	}
	
	@Override
	public SplitFileSegmentKeys clone() {
		try {
			return (SplitFileSegmentKeys) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new Error("Yes it is!");
		}
	}

	// Not often used, not very efficient, but overriding equals() requires overriding hashCode().
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + checkBlocks;
        result = prime * result + Arrays.hashCode(commonDecryptKey);
        result = prime * result + Arrays.hashCode(commonExtraBytes);
        result = prime * result + dataBlocks;
        result = prime * result + Arrays.hashCode(decryptKeys);
        result = prime * result + Arrays.hashCode(extraBytesForKeys);
        result = prime * result + Arrays.hashCode(routingKeys);
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SplitFileSegmentKeys other = (SplitFileSegmentKeys) obj;
        if (checkBlocks != other.checkBlocks)
            return false;
        if (!Arrays.equals(commonDecryptKey, other.commonDecryptKey))
            return false;
        if (!Arrays.equals(commonExtraBytes, other.commonExtraBytes))
            return false;
        if (dataBlocks != other.dataBlocks)
            return false;
        if (!Arrays.equals(decryptKeys, other.decryptKeys))
            return false;
        if (!Arrays.equals(extraBytesForKeys, other.extraBytesForKeys))
            return false;
        if (!Arrays.equals(routingKeys, other.routingKeys))
            return false;
        return true;
    }

    public int totalKeys() {
        return checkBlocks + dataBlocks;
    }

}
