/* -*- Mode: java; c-basic-indent: 4; tab-width: 4 -*- */
package freenet.crypt;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.util.Arrays;

import freenet.node.FSParseException;
import freenet.store.StorableBlock;
import freenet.support.Base64;
import freenet.support.HexUtil;
import freenet.support.IllegalBase64Exception;
import freenet.support.SimpleFieldSet;

public class DSAPublicKey extends CryptoKey implements StorableBlock {

	private static final long serialVersionUID = -1;
	private final BigInteger y;
	public static final int PADDED_SIZE = 1024;
	public static final int HASH_LENGTH = 32;
	/** Null means use Global.DSAgroupBigA. This makes persistence simpler. */
	private final DSAGroup group;
	private byte[] fingerprint = null;
	
	public DSAPublicKey(DSAGroup g, BigInteger y) {
		if(y.signum() != 1)
			throw new IllegalArgumentException();
		this.y = y;
		if(g == Global.DSAgroupBigA) g = null;
		this.group = g;
		if(y.compareTo(getGroup().getP()) > 0)
			throw new IllegalArgumentException("y must be < p but y=" + y + " p=" + g.getP());
	}

	/**
	 * Use this constructor if you have a Hex:ed version of y already
	 * available, will save some conversions and string allocations.
	 */
	public DSAPublicKey(DSAGroup g, String yAsHexString) throws NumberFormatException {
		this.y = new BigInteger(yAsHexString, 16);
		if(y.signum() != 1)
			throw new IllegalArgumentException();
		if(g == Global.DSAgroupBigA) g = null;
		this.group = g;
	}

	public DSAPublicKey(DSAGroup g, DSAPrivateKey p) {
		this(g, g.getG().modPow(p.getX(), g.getP()));
	}

	public DSAPublicKey(InputStream is) throws IOException, CryptFormatException {
		DSAGroup g = (DSAGroup) DSAGroup.read(is);
		if(g == Global.DSAgroupBigA) g = null;
		group = g;
		y = Util.readMPI(is);
		if(y.compareTo(getGroup().getP()) > 0)
			throw new IllegalArgumentException("y must be < p but y=" + y + " p=" + getGroup().getP());
	}

	public DSAPublicKey(byte[] pubkeyBytes) throws IOException, CryptFormatException {
		this(new ByteArrayInputStream(pubkeyBytes));
	}

	private DSAPublicKey(DSAPublicKey key) {
		fingerprint = null; // regen when needed
		this.y = new BigInteger(1, key.y.toByteArray());
		DSAGroup g = key.group;
		if(g != null) g = g.cloneKey();
		this.group = g;
	}

	public static DSAPublicKey create(byte[] pubkeyAsBytes) throws CryptFormatException {
		try {
			return new DSAPublicKey(new ByteArrayInputStream(pubkeyAsBytes));
		} catch(IOException e) {
			throw new CryptFormatException(e);
		}
	}
	
	protected DSAPublicKey() {
	    // For serialization.
	    y = null;
	    group = null;
	}

	public BigInteger getY() {
		return y;
	}

	public BigInteger getP() {
		return getGroup().getP();
	}

	public BigInteger getQ() {
		return getGroup().getQ();
	}

	public BigInteger getG() {
		return getGroup().getG();
	}

	@Override
	public String keyType() {
		return "DSA.p";
	}

	// Nope, this is fine
	public final DSAGroup getGroup() {
		if(group == null) return Global.DSAgroupBigA;
		else return group;
	}

	public static CryptoKey read(InputStream i) throws IOException, CryptFormatException {
		return new DSAPublicKey(i);
	}

	public int keyId() {
		return y.intValue();
	}

	@Override
	public String toLongString() {
		return "y=" + HexUtil.biToHex(y);
	}

	@Override
	public byte[] asBytes() {
		byte[] groupBytes = getGroup().asBytes();
		byte[] ybytes = Util.MPIbytes(y);
		byte[] bytes = new byte[groupBytes.length + ybytes.length];
		System.arraycopy(groupBytes, 0, bytes, 0, groupBytes.length);
		System.arraycopy(ybytes, 0, bytes, groupBytes.length, ybytes.length);
		return bytes;
	}

	public byte[] asBytesHash() {
		byte[] hash = SHA256.digest(asBytes());
		return hash;
	}

	public byte[] asPaddedBytes() {
		byte[] asBytes = asBytes();
		if(asBytes.length == PADDED_SIZE)
			return asBytes;
		if(asBytes.length > PADDED_SIZE)
			throw new Error("Cannot fit key in " + PADDED_SIZE + " - real size is " + asBytes.length);
		return Arrays.copyOf(asBytes, PADDED_SIZE);
	}

	@Override
	public byte[] fingerprint() {
		synchronized(this) {
			if(fingerprint == null)
				fingerprint = fingerprint(new BigInteger[]{y});
			return fingerprint;
		}
	}

	public boolean equals(DSAPublicKey o) {
		if(this == o) // Not necessary, but a very cheap optimization
			return true;
		return y.equals(o.y) && getGroup().equals(o.getGroup());
	}

	@Override
	public int hashCode() {
		return y.hashCode() ^ getGroup().hashCode();
	}

	@Override
	public boolean equals(Object o) {
		if(this == o) // Not necessary, but a very cheap optimization
			return true;
		else if((o == null) || (o.getClass() != this.getClass()))
			return false;
		return y.equals(((DSAPublicKey) o).y) && getGroup().equals(((DSAPublicKey) o).getGroup());
	}

	public int compareTo(Object other) {
		if(other instanceof DSAPublicKey)
			return getY().compareTo(((DSAPublicKey) other).getY());
		else
			return -1;
	}

	public SimpleFieldSet asFieldSet() {
		SimpleFieldSet fs = new SimpleFieldSet(true);
		fs.putSingle("y", Base64.encode(y.toByteArray()));
		return fs;
	}

	public static DSAPublicKey create(SimpleFieldSet set, DSAGroup group) throws FSParseException {
		BigInteger x;
		try {
			x = new BigInteger(1, Base64.decode(set.get("y")));
		} catch (IllegalBase64Exception e) {
			throw new FSParseException(e);
		}
		try {
			return new DSAPublicKey(group, x);
		} catch (IllegalArgumentException e) {
			throw new FSParseException(e);
		}
	}

	@Override
	public byte[] getFullKey() {
		return asBytesHash();
	}

	@Override
	public byte[] getRoutingKey() {
		return asBytesHash();
	}

	public DSAPublicKey cloneKey() {
		return new DSAPublicKey(this);
	}

}
