package net.i2p.crypto;

import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidParameterSpecException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.data.Hash;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.SystemVersion;

/**
 * Defines the properties for various signature types
 * that I2P supports or may someday support.
 *
 * All Signatures, SigningPublicKeys, and SigningPrivateKeys have a type.
 * Note that a SigType specifies both an algorithm and parameters, so that
 * we may change primes or curves for a given algorithm.
 *
 * @since 0.9.8
 */
public enum SigType {
    /**
     *  DSA_SHA1 is the default.
     *  Pubkey 128 bytes; privkey 20 bytes; hash 20 bytes; sig 40 bytes
     *  @since 0.9.8
     */
    DSA_SHA1(0, 128, 20, 20, 40, SigAlgo.DSA, "SHA-1", "SHA1withDSA", CryptoConstants.DSA_SHA1_SPEC, "1.2.840.10040.4.3", "0"),
    /**  Pubkey 64 bytes; privkey 32 bytes; hash 32 bytes; sig 64 bytes */
    ECDSA_SHA256_P256(1, 64, 32, 32, 64, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P256_SPEC, "1.2.840.10045.4.3.2", "0.9.12"),
    /**  Pubkey 96 bytes; privkey 48 bytes; hash 48 bytes; sig 96 bytes */
    ECDSA_SHA384_P384(2, 96, 48, 48, 96, SigAlgo.EC, "SHA-384", "SHA384withECDSA", ECConstants.P384_SPEC, "1.2.840.10045.4.3.3", "0.9.12"),
    /**  Pubkey 132 bytes; privkey 66 bytes; hash 64 bytes; sig 132 bytes */
    ECDSA_SHA512_P521(3, 132, 66, 64, 132, SigAlgo.EC, "SHA-512", "SHA512withECDSA", ECConstants.P521_SPEC, "1.2.840.10045.4.3.4", "0.9.12"),

    /**  Pubkey 256 bytes; privkey 512 bytes; hash 32 bytes; sig 256 bytes */
    RSA_SHA256_2048(4, 256, 512, 32, 256, SigAlgo.RSA, "SHA-256", "SHA256withRSA", RSAConstants.F4_2048_SPEC, "1.2.840.113549.1.1.11", "0.9.12"),
    /**  Pubkey 384 bytes; privkey 768 bytes; hash 48 bytes; sig 384 bytes */
    RSA_SHA384_3072(5, 384, 768, 48, 384, SigAlgo.RSA, "SHA-384", "SHA384withRSA", RSAConstants.F4_3072_SPEC, "1.2.840.113549.1.1.12", "0.9.12"),
    /**  Pubkey 512 bytes; privkey 1024 bytes; hash 64 bytes; sig 512 bytes */
    RSA_SHA512_4096(6, 512, 1024, 64, 512, SigAlgo.RSA, "SHA-512", "SHA512withRSA", RSAConstants.F4_4096_SPEC, "1.2.840.113549.1.1.13", "0.9.12"),

    /**
     *  Pubkey 32 bytes; privkey 32 bytes; hash 64 bytes; sig 64 bytes
     *
     *  Due to bugs in previous versions, minimum version is 0.9.17.
     *
     *  @since 0.9.15
     */
    EdDSA_SHA512_Ed25519(7, 32, 32, 64, 64, SigAlgo.EdDSA, "SHA-512", "SHA512withEdDSA",
                         EdDSANamedCurveTable.getByName("ed25519-sha-512"), "1.3.101.101", "0.9.17"),

    /**
     *  Prehash version (double hashing, for offline use such as su3, not for use on the network)
     *  Pubkey 32 bytes; privkey 32 bytes; hash 64 bytes; sig 64 bytes
     *  @since 0.9.25
     */
    EdDSA_SHA512_Ed25519ph(8, 32, 32, 64, 64, SigAlgo.EdDSA, "SHA-512", "NonewithEdDSA",
                           EdDSANamedCurveTable.getByName("ed25519-sha-512"), "1.3.101.101", "0.9.25"),

    // 9 and 10 reserved for GOST - see proposal 134

    /**
     *  Blinded version of EdDSA, use for encrypted LS2
     *  Pubkey 32 bytes; privkey 32 bytes; hash 64 bytes; sig 64 bytes
     *
     *  @since 0.9.39
     */
    RedDSA_SHA512_Ed25519(11, 32, 32, 64, 64, SigAlgo.EdDSA, "SHA-512", "SHA512withEdDSA",
                          EdDSANamedCurveTable.getByName("ed25519-sha-512"), "1.3.101.101", "0.9.39"),

    ;

    // TESTING....................


    // others..........

    // EC mix and match
    //ECDSA_SHA256_P192(5, 48, 24, 32, 48, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P192_SPEC),
    //ECDSA_SHA256_P384(6, 96, 48, 32, 96, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P384_SPEC),
    //ECDSA_SHA256_P521(7, 132, 66, 32, 132, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.P521_SPEC),
    //ECDSA_SHA384_P256(8, 64, 32, 48, 64, SigAlgo.EC, "SHA-384", "SHA384withECDSA", ECConstants.P256_SPEC),
    //ECDSA_SHA384_P521(9, 132, 66, 48, 132, SigAlgo.EC, "SHA-384", "SHA384withECDSA", ECConstants.P521_SPEC),
    //ECDSA_SHA512_P256(10, 64, 32, 64, 64, SigAlgo.EC, "SHA-512", "SHA512withECDSA", ECConstants.P256_SPEC),
    //ECDSA_SHA512_P384(11, 96, 48, 64, 96, SigAlgo.EC, "SHA-512", "SHA512withECDSA", ECConstants.P384_SPEC),

    // Koblitz
    //ECDSA_SHA256_K163(12, 42, 21, 32, 42, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.K163_SPEC),
    //ECDSA_SHA256_K233(13, 60, 30, 32, 60, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.K233_SPEC),
    //ECDSA_SHA256_K283(14, 72, 36, 32, 72, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.K283_SPEC),
    //ECDSA_SHA256_K409(15, 104, 52, 32, 104, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.K409_SPEC),
    //ECDSA_SHA256_K571(16, 144, 72, 32, 144, SigAlgo.EC, "SHA-256", "SHA256withECDSA", ECConstants.K571_SPEC),

    // too short..............
    /**  Pubkey 48 bytes; privkey 24 bytes; hash 20 bytes; sig 48 bytes */
    //ECDSA_SHA1_P192(1, 48, 24, 20, 48, SigAlgo.EC, "SHA-1", "SHA1withECDSA", ECConstants.P192_SPEC),
    //RSA_SHA1(17, 128, 256, 20, 128, SigAlgo.RSA, "SHA-1", "SHA1withRSA", RSAConstants.F4_1024_SPEC),
    //MD5
    //RSA_SHA1

    //ELGAMAL_SHA256
    //DSA_2048_224(2, 256, 28, 32, 56, "SHA-256"),
    // Nonstandard, used by Syndie.
    // Pubkey 128 bytes; privkey 20 bytes; hash 32 bytes; sig 40 bytes
    //DSA_1024_160_SHA256(1, 128, 20, 32, 40, "SHA-256", "?"),
    // Pubkey 256 bytes; privkey 32 bytes; hash 32 bytes; sig 64 bytes
    //DSA_2048_256(2, 256, 32, 32, 64, "SHA-256", "?"),
    // Pubkey 384 bytes; privkey 32 bytes; hash 32 bytes; sig 64 bytes
    //DSA_3072_256(3, 384, 32, 32, 64, "SHA-256", "?"),


    private final int code, pubkeyLen, privkeyLen, hashLen, sigLen;
    private final SigAlgo base;
    private final String digestName, algoName, oid, since;
    private final AlgorithmParameterSpec params;
    private final boolean isAvail;

    SigType(int cod, int pubLen, int privLen, int hLen, int sLen, SigAlgo baseAlgo,
            String mdName, String aName, AlgorithmParameterSpec pSpec, String oid, String supportedSince) {
        code = cod;
        pubkeyLen = pubLen;
        privkeyLen = privLen;
        hashLen = hLen;
        sigLen = sLen;
        base = baseAlgo;
        digestName = mdName;
        algoName = aName;
        params = pSpec;
        this.oid = oid;
        since = supportedSince;
        isAvail = x_isAvailable();
    }

    /** the unique identifier for this type */
    public int getCode() { return code; }
    /** the length of the public key, in bytes */
    public int getPubkeyLen() { return pubkeyLen; }
    /** the length of the private key, in bytes */
    public int getPrivkeyLen() { return privkeyLen; }
    /** the length of the hash, in bytes */
    public int getHashLen() { return hashLen; }
    /** the length of the signature, in bytes */
    public int getSigLen() { return sigLen; }
    /** the standard base algorithm name used for the Java crypto factories */
    public SigAlgo getBaseAlgorithm() { return base; }
    /** the standard name used for the Java crypto factories */
    public String getAlgorithmName() { return algoName; }
    /**
     *  The elliptic curve ECParameterSpec for ECDSA; DSAParameterSpec for DSA
     *  @throws InvalidParameterSpecException if the algorithm is not available on this JVM.
     */
    public AlgorithmParameterSpec getParams() throws InvalidParameterSpecException {
        if (params == null)
            throw new InvalidParameterSpecException(toString() + " is not available in this JVM");
        return params;
    }

    /** @throws UnsupportedOperationException if not supported */
    public MessageDigest getDigestInstance() {
        if (digestName.equals("SHA-1"))
            return SHA1.getInstance();
        if (digestName.equals("SHA-256"))
            return SHA256Generator.getDigestInstance();
        try {
            return MessageDigest.getInstance(digestName);
        } catch (NoSuchAlgorithmException e) {
            throw new UnsupportedOperationException(e);
        }
    }

    /**
     *  @since 0.9.9
     *  @throws UnsupportedOperationException if not supported
     */
    public SimpleDataStructure getHashInstance() {
        switch (getHashLen()) {
            case 20:
                return new SHA1Hash();
            case 32:
                return new Hash();
            case 48:
                return new Hash384();
            case 64:
                return new Hash512();
            default:
                throw new UnsupportedOperationException("Unsupported hash length: " + getHashLen());
        }
    }

    /**
     *  The router version in which this type was first supported.
     *
     *  @since 0.9.15
     */
    public String getSupportedSince() {
        return since;
    }

    /**
     *  The OID for the signature.
     *
     *  @since 0.9.25
     */
    public String getOID() {
        return oid;
    }

    /**
     *  @since 0.9.12
     *  @return true if supported in this JVM
     */
    public boolean isAvailable() {
        return isAvail;
    }

    private boolean x_isAvailable() {
        if (DSA_SHA1 == this)
            return true;
        try {
            getParams();
            if (getBaseAlgorithm() != SigAlgo.EdDSA) {
                Signature jsig = Signature.getInstance(getAlgorithmName());
                if (getBaseAlgorithm() == SigAlgo.EC && SystemVersion.isGentoo() ) {
                    // Do a full keygen/sign test on Gentoo, because it lies. Keygen works but sigs fail.
                    // https://bugs.gentoo.org/show_bug.cgi?id=528338
                    // http://icedtea.classpath.org/bugzilla/show_bug.cgi?id=2497
                    // http://zzz.i2p/topics/1931
                    // Be sure nothing in the code paths below calls isAvailable()
                    // get an I2P keypair
                    SimpleDataStructure[] keys = KeyGenerator.getInstance().generateSigningKeys(this);
                    SigningPrivateKey privKey = (SigningPrivateKey) keys[1];
                    // convert privkey back to Java key and sign
                    jsig.initSign(SigUtil.toJavaECKey(privKey));
                    // use the pubkey as random data
                    jsig.update(keys[0].getData());
                    jsig.sign();
                }
            }
            getDigestInstance();
            getHashInstance();
        } catch (GeneralSecurityException e) {
            return false;
        } catch (RuntimeException e) {
            return false;
        }
        return true;
    }

    /**
     *  @return true if supported in this JVM
     *  @since 0.9.15
     */
    public static boolean isAvailable(int code) {
        SigType type = getByCode(code);
        if (type == null)
            return false;
        return type.isAvailable();
    }

    /**
     *  @param stype number or name
     *  @return true if supported in this JVM
     *  @since 0.9.15
     */
    public static boolean isAvailable(String stype) {
        SigType type = parseSigType(stype);
        if (type == null)
            return false;
        return type.isAvailable();
    }

    private static final Map<Integer, SigType> BY_CODE = new HashMap<Integer, SigType>();

    static {
        for (SigType type : SigType.values()) {
            if (BY_CODE.put(Integer.valueOf(type.getCode()), type) != null)
                throw new IllegalStateException("Duplicate SigType code");
        }
    }

    /** @return null if not supported */
    public static SigType getByCode(int code) {
        return BY_CODE.get(Integer.valueOf(code));
    }

    /**
     *  Convenience for user apps
     *
     *  @param stype number or name
     *  @return null if not found
     *  @since 0.9.9 moved from SU3File in 0.9.12
     */
    public static SigType parseSigType(String stype) {
        try {
            String uc = stype.toUpperCase(Locale.US);
            // handle mixed-case enum
            if (uc.equals("EDDSA_SHA512_ED25519"))
                return EdDSA_SHA512_Ed25519;
            if (uc.equals("EDDSA_SHA512_ED25519PH"))
                return EdDSA_SHA512_Ed25519ph;
            if (uc.equals("REDDSA_SHA512_ED25519"))
                return RedDSA_SHA512_Ed25519;
            return valueOf(uc);
        } catch (IllegalArgumentException iae) {
            try {
                int code = Integer.parseInt(stype);
                return getByCode(code);
            } catch (NumberFormatException nfe) {
                return null;
            }
        }
    }
}
