/* 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.Serializable;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Pattern;

import freenet.support.Fields;
import freenet.support.Logger;

/**
 * Updatable Subspace Key.
 * Not really a ClientKey as it cannot be directly requested.
 * 
 * Contains:
 * - Enough information to produce a real SSK.
 * - Site name.
 * - Site edition number.
 * 
 * WARNING: Changing non-transient members on classes that are Serializable can result in 
 * restarting downloads or losing uploads.
 */
public class USK extends BaseClientKey implements Comparable<USK>, Serializable {

    private static final long serialVersionUID = 1L;
    /* The character to separate the site name from the edition number in its SSK form.
	 * I chose "-", because it makes it ludicrously easy to go from the USK form to the
	 * SSK form, and we don't need to go vice versa.
	 */
	static protected final String SEPARATOR = "-";
	/** Encryption type */
	public final byte cryptoAlgorithm;
	/** Public key hash */
	protected final byte[] pubKeyHash;
	/** Encryption key */
	protected final byte[] cryptoKey;
	// Extra must be verified on creation, and is fixed for now. FIXME if it becomes changeable, need to keep values here.
	
	public final String siteName;
	public final long suggestedEdition;
	
	private final int hashCode;

	public USK(byte[] pubKeyHash, byte[] cryptoKey, byte[] extra, String siteName, long suggestedEdition) throws MalformedURLException {
		this.pubKeyHash = pubKeyHash;
		this.cryptoKey = cryptoKey;
		this.siteName = siteName;
		this.suggestedEdition = suggestedEdition;
		if(extra == null)
			throw new MalformedURLException("No extra bytes (third bit) in USK");
		if(pubKeyHash == null)
			throw new MalformedURLException("No pubkey hash (first bit) in USK");
		if(cryptoKey == null)
			throw new MalformedURLException("No crypto key (second bit) in USK");
		// Verify extra bytes, get cryptoAlgorithm - FIXME this should be a static method or something?
		ClientSSK tmp = new ClientSSK(siteName, pubKeyHash, extra, null, cryptoKey);
		cryptoAlgorithm = tmp.cryptoAlgorithm;
		if(pubKeyHash.length != NodeSSK.PUBKEY_HASH_SIZE)
			throw new MalformedURLException("Pubkey hash wrong length: "+pubKeyHash.length+" should be "+NodeSSK.PUBKEY_HASH_SIZE);
		if(cryptoKey.length != ClientSSK.CRYPTO_KEY_LENGTH)
			throw new MalformedURLException("Decryption key wrong length: "+cryptoKey.length+" should be "+ClientSSK.CRYPTO_KEY_LENGTH);
		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^
			siteName.hashCode() ^ (int)suggestedEdition ^ (int)(suggestedEdition >> 32);
	}

	public static USK create(FreenetURI uri) throws MalformedURLException {
		if(!uri.isUSK()) throw new MalformedURLException("Not a USK");
		return new USK(uri.getRoutingKey(), uri.getCryptoKey(), uri.getExtra(), uri.getDocName(), uri.getSuggestedEdition());
	}
	
	protected USK(byte[] pubKeyHash2, byte[] cryptoKey2, String siteName2, long suggestedEdition2, byte cryptoAlgorithm) {
		this.pubKeyHash = pubKeyHash2;
		this.cryptoKey = cryptoKey2;
		this.siteName = siteName2;
		this.suggestedEdition = suggestedEdition2;
		this.cryptoAlgorithm = cryptoAlgorithm;
		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^
			siteName.hashCode() ^ (int)suggestedEdition ^ (int)(suggestedEdition >> 32);
	}
	
	protected USK() {
	    // For serialization.
        pubKeyHash = null;
        cryptoKey = null;
        siteName = null;
	    suggestedEdition = 0;
	    cryptoAlgorithm = 0;
	    hashCode = 0;
	}

	private static final Pattern badDocNamePattern;
	static {
		badDocNamePattern = Pattern.compile(".*\\-[0-9]+(\\/.*)?$");
	}

	// FIXME: Be careful with this constructor! There must not be an edition in the ClientSSK!
	public USK(ClientSSK ssk, long myARKNumber) {
		this.pubKeyHash = ssk.pubKeyHash;
		this.cryptoKey = ssk.cryptoKey;
		this.siteName = ssk.docName;
		this.suggestedEdition = myARKNumber;
		this.cryptoAlgorithm = ssk.cryptoAlgorithm;

		if (badDocNamePattern.matcher(siteName).matches())	// not error -- just "possible" bug
			Logger.normal(this, "POSSIBLE BUG: edition in ClientSSK " + ssk, new Exception("debug"));

		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^
			siteName.hashCode() ^ (int)suggestedEdition ^ (int)(suggestedEdition >> 32);
	}

	public USK(USK usk) {
		// FIXME can we not copy pubKeyHash?
		// If we can guarantee that neither USK nor anything getting it without copying will change it?
		// db4o treats byte[] as individual byte members, so there are no issues with deactivation.
		this.pubKeyHash = usk.pubKeyHash.clone();
		this.cryptoAlgorithm = usk.cryptoAlgorithm;
		// FIXME should we copy cryptoKey?
		this.cryptoKey = usk.cryptoKey;
		this.siteName = usk.siteName;
		this.suggestedEdition = usk.suggestedEdition;
		hashCode = Fields.hashCode(pubKeyHash) ^ Fields.hashCode(cryptoKey) ^
			siteName.hashCode() ^ (int)suggestedEdition ^ (int)(suggestedEdition >> 32);
	}

	@Override
	public FreenetURI getURI() {
		return new FreenetURI(pubKeyHash, cryptoKey, ClientSSK.getExtraBytes(cryptoAlgorithm), siteName, suggestedEdition);
	}

	public ClientSSK getSSK(long ver) {
		return getSSK(getName(ver));
	}
	
	public ClientSSK getSSK(String string) {
		try {
			return new ClientSSK(string, pubKeyHash, ClientSSK.getExtraBytes(cryptoAlgorithm), null, cryptoKey);
		} catch (MalformedURLException e) {
			Logger.error(this, "Caught "+e+" should not be possible in USK.getSSK", e);
			throw new Error(e);
		}
	}
	
	public String getName(long ver) {
		return siteName + SEPARATOR + ver;
	}

	public ClientKey getSSK() {
		return getSSK(suggestedEdition);
	}
	
	public USK copy(long edition) {
		if(suggestedEdition == edition) return this;
		return new USK(pubKeyHash, cryptoKey, siteName, edition, cryptoAlgorithm);
	}

	public USK clearCopy() {
		return copy(0);
	}
	
	public final USK copy() {
		// We need our own constructor to make sure we copy pubKeyHash.
		// So clone() doesn't work for this.
		// FIXME when we are sure we don't need to copy the byte[]'s we might be able to switch back to clone().
		return new USK(this);
	}
	
	@Override
	public boolean equals(Object o) {
		if(o == null || !(o instanceof USK)) return false;
		return equals(o, true);
	}
	
	public boolean equals(Object o, boolean includeVersion) {
		if(o instanceof USK) {
			USK u = (USK)o;
			if(!Arrays.equals(pubKeyHash, u.pubKeyHash)) return false;
			if(!Arrays.equals(cryptoKey, u.cryptoKey)) return false;
			if(!siteName.equals(u.siteName)) return false;
			if(includeVersion && (suggestedEdition != u.suggestedEdition)) return false;
			return true;
		}
		return false;
	}

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

	public FreenetURI getBaseSSK() {
		return new FreenetURI("SSK", siteName, pubKeyHash, cryptoKey, ClientSSK.getExtraBytes(cryptoAlgorithm));
	}
	
	@Override
	public String toString() {
		return super.toString()+ ':' +getURI();
	}

	public FreenetURI turnMySSKIntoUSK(FreenetURI uri) {
		if(uri.getKeyType().equals("SSK") &&
				Arrays.equals(uri.getRoutingKey(), pubKeyHash) &&
				Arrays.equals(uri.getCryptoKey(), cryptoKey) &&
				Arrays.equals(uri.getExtra(), ClientSSK.getExtraBytes(cryptoAlgorithm)) &&
				uri.getDocName() != null &&
				uri.getDocName().startsWith(siteName)) {
			String doc = uri.getDocName();
			doc = doc.substring(siteName.length());
			if(doc.length() < 2 || doc.charAt(0) != '-') return uri;
			doc = doc.substring(1);
			long edition;
			try {
				edition = Long.parseLong(doc);
			} catch (NumberFormatException e) {
				Logger.normal(this, "Trying to turn SSK back into USK: "+uri+" doc="+doc+" caught "+e, e);
				return uri;
			}
			if(!doc.equals(Long.toString(edition))) return uri;
			return new FreenetURI("USK", siteName, uri.getAllMetaStrings(), pubKeyHash, cryptoKey, ClientSSK.getExtraBytes(cryptoAlgorithm), edition);
		}
		return uri;
	}

	@Override
	public int compareTo(USK o) {
		if(this == o) return 0;
		if(cryptoAlgorithm < o.cryptoAlgorithm) return -1;
		if(cryptoAlgorithm > o.cryptoAlgorithm) return 1;
		int cmp = Fields.compareBytes(pubKeyHash, o.pubKeyHash);
		if(cmp != 0) return cmp;
		cmp = Fields.compareBytes(cryptoKey, o.cryptoKey);
		if(cmp != 0) return cmp;
		cmp = siteName.compareTo(o.siteName);
		if(cmp != 0) return cmp;
		if(suggestedEdition > o.suggestedEdition) return 1;
		if(suggestedEdition < o.suggestedEdition) return -1;
		return 0;
	}
	
	public static final Comparator<USK> FAST_COMPARATOR = new Comparator<USK>() {

		@Override
		public int compare(USK o1, USK o2) {
			if(o1.hashCode > o2.hashCode) return 1;
			else if(o1.hashCode < o2.hashCode) return -1;
			return o1.compareTo(o2);
		}
		
	};

	public byte[] getPubKeyHash() {
		return Arrays.copyOf(pubKeyHash, pubKeyHash.length);
	}

	public boolean samePubKeyHash(NodeSSK k) {
		return Arrays.equals(k.getPubKeyHash(), pubKeyHash);
	}
}
