package org.bouncycastle.jce.cert;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Security;

import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.OIDTokenizer;
import org.bouncycastle.asn1.x509.X509Name;

class CertUtil
{
    static class Implementation
    {
        Object      engine;
        Provider    provider;

        Implementation(
            Object      engine,
            Provider    provider)
        {
            this.engine = engine;
            this.provider = provider;
        }

        Object getEngine()
        {
            return engine;
        }

        Provider getProvider()
        {
            return provider;
        }
    }

    /**
     * see if we can find an algorithm (or its alias and what it represents) in
     * the property table for the given provider.
     *
     * @return null if no algorithm found, an Implementation if it is.
     */
    static Implementation getImplementation(
        String      baseName,
        String      algorithm,
        Provider    prov)
    {
        if (prov == null)
        {
            Provider[] provider = Security.getProviders();

            //
            // search every provider looking for the algorithm we want.
            //
            for (int i = 0; i != provider.length; i++)
            {
                Implementation imp = getImplementation(baseName, algorithm, provider[i]);
                if (imp != null)
                {
                    return imp;
                }
            }

	    return null;
        }

        String      alias;

        while ((alias = prov.getProperty("Alg.Alias." + baseName + "." + algorithm)) != null)
        {
            algorithm = alias;
        }

        String      className = prov.getProperty(baseName + "." + algorithm);

        if (className != null)
        {
            try
            {
                return new Implementation(Class.forName(className).newInstance(), prov);
            }
            catch (ClassNotFoundException e)
            {
                throw new IllegalStateException(
                    "algorithm " + algorithm + " in provider " + prov.getName() + " but no class found!");
            }
            catch (Exception e)
            {
                throw new IllegalStateException(
                    "algorithm " + algorithm + " in provider " + prov.getName() + " but class inaccessible: " + e.toString());
            }
        }

        return null;
    }

    /**
     * return an implementation for a given algorithm/provider.
     * If the provider is null, we grab the first avalaible who has the required algorithm.
     *
     * @return null if no algorithm found, an Implementation if it is.
     * @exception NoSuchProviderException if a provider is specified and not found.
     */
    static Implementation getImplementation(
        String      baseName,
        String      algorithm,
        String      provider)
        throws NoSuchProviderException
    {
        if (provider == null)
        {
            Provider[] prov = Security.getProviders();

            //
            // search every provider looking for the algorithm we want.
            //
            for (int i = 0; i != prov.length; i++)
            {
                Implementation imp = getImplementation(baseName, algorithm, prov[i]);
                if (imp != null)
                {
                    return imp;
                }
            }
        }
        else
        {
            Provider prov = Security.getProvider(provider);

            if (prov == null)
            {
                throw new NoSuchProviderException("Provider " + provider + " not found");
            }

            return getImplementation(baseName, algorithm, prov);
        }

        return null;
    }

    /**
     * see if we can find an algorithm (or its alias and what it represents) in
     * the property table for the given provider.
     *
     * @return null if no algorithm found, an Implementation if it is.
     */
    static Implementation getImplementation(
        String      baseName,
        String      algorithm,
        Provider    prov,
        Class[]     ctorparamtype,
        Object[]    ctorparam)
	throws InvalidAlgorithmParameterException
    {
        String      alias;

        while ((alias = prov.getProperty("Alg.Alias." + baseName + "." + algorithm)) != null)
        {
            algorithm = alias;
        }

        String      className = prov.getProperty(baseName + "." + algorithm);

        if (className != null)
        {
            try
            {
                return new Implementation(Class.forName(className).getConstructor( ctorparamtype ).newInstance( ctorparam ), prov);
            }
            catch (ClassNotFoundException e)
            {
                throw new IllegalStateException(
                    "algorithm " + algorithm + " in provider " + prov.getName() + " but no class found!");
            }
            catch ( Exception e )
            {
		if ( e instanceof InvalidAlgorithmParameterException )
		    throw (InvalidAlgorithmParameterException)e;

                throw new IllegalStateException(
                    "algorithm " + algorithm + " in provider " + prov.getName() + " but class inaccessible!");
            }
        }

        return null;
    }

    /**
     * return an implementation for a given algorithm/provider.
     * If the provider is null, we grab the first avalaible who has the required algorithm.
     *
     * @return null if no algorithm found, an Implementation if it is.
     *
     * @exception NoSuchProviderException if a provider is specified and not found.
     */
    static Implementation getImplementation(
        String      baseName,
        String      algorithm,
        String      provider,
        Class[]     ctorparamtype,
        Object[]    ctorparam)
        throws NoSuchProviderException,
	InvalidAlgorithmParameterException
    {
        if (provider == null)
        {
            Provider[] prov = Security.getProviders();

            //
            // search every provider looking for the algorithm we want.
            //
            for (int i = 0; i != prov.length; i++)
            {
                Implementation imp = getImplementation(baseName, algorithm, prov[i], ctorparamtype, ctorparam);
                if (imp != null)
                {
                    return imp;
                }
            }
        }
        else
        {
            Provider prov = Security.getProvider(provider);

            if (prov == null)
            {
                throw new NoSuchProviderException("Provider " + provider + " not found");
            }

            return getImplementation(baseName, algorithm, prov, ctorparamtype, ctorparam);
        }

        return null;
    }

    static byte[] parseGeneralName(int type,String data)
	throws IOException
    {
	byte[] encoded = null;

	switch(type) {
	    case 0:
		throw new IOException("unable to parse OtherName String representation");
	    case 1:
		encoded = parseRfc822(data.trim());
		break;
	    case 2:
		encoded = parseDNSName(data.trim());
		break;
	    case 3:
		throw new IOException("unable to parse ORAddress String representation");
	    case 4:
		encoded = parseX509Name(data.trim());
		break;
	    case 5:
		throw new IOException("unable to parse EDIPartyName String representation");
	    case 6:
		encoded = parseURI(data.trim());
		break;
	    case 7:
		encoded = parseIP(data.trim());
		break;
	    case 8:
		encoded = parseOID(data.trim());
		break;
	    default:
		throw new IOException("unable to parse unkown type String representation");
	}
	return encoded;
    }

	/**
	 * Check the format of an OID.<br />
	 * Throw an IOException if  the first component is not 0, 1 or 2
	 * or the second component is greater than 39.<br />
	 * <br /> 
	 * User {@link org.bouncycastle.asn1.OIDTokenizer OIDTokenizer}
	 *
	 * @param the OID to be checked.
	 *
	 * @exception IOException if the first component is not 0, 1 or 2
	 * or the second component is greater than 39.
	 **/
    static byte[] parseOID(String oid)
	throws IOException
    {
	OIDTokenizer tokenizer = new OIDTokenizer(oid);
	String token;
	if ( ! tokenizer.hasMoreTokens() )
	    throw new IOException("OID contains no tokens");
	token = tokenizer.nextToken();
	if ( token == null )
	    throw new IOException("OID contains no tokens");
	try {
	    int test = (Integer.valueOf(token)).intValue();
	    if ( test < 0 || test > 2 )
		throw new IOException("first token is not >= 0 and <=2" );
	    if ( ! tokenizer.hasMoreTokens() )
		throw new IOException("OID contains only one token");
	    token = tokenizer.nextToken();
	    if ( token == null )
		throw new IOException("OID contains only one token");
	    test = (Integer.valueOf(token)).intValue();
	    if ( test < 0 || test > 39 )
		throw new IOException("secon token is not >= 0 and <=39" );
	} catch ( NumberFormatException ex ) {
	    throw new IOException("token: " + token + ": " + ex.toString() );
	}
	DERObject derData = new DERObjectIdentifier(oid);
	ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	DEROutputStream derOutStream = new DEROutputStream(outStream);
	derOutStream.writeObject( derData );
	derOutStream.close();
	return outStream.toByteArray();
    }

	/**
	 * Parse the given IPv4 or IPv6 into DER encoded byte array representation.
	 *
	 * @param the IP in well known String format
	 *
	 * @return the IP as byte array
	 *
	 * @exception IOException if the String could not be parsed
	 **/
    private static byte[] parseIP(String data)
	throws IOException
    {
	byte[] encoded = parseIPv4(data);

	if ( encoded == null )
	    encoded = parseIPv6(data);

	if ( encoded == null )
	    throw new IOException("unable to parse IP to DER encoded byte array");

	return encoded;
    }

	/**
	 * Parse the given IPv4 into DER encoded byte array representation.
	 *
	 * @param the IP in well known String format
	 *
	 * @return the IP as byte array or <code>null</code> if not parseable
	 **/
    private static byte[] parseIPv4(String data)
    {
	if (data.length() == 0)
	    return null;

	int octet;
	int octets = 0;
	byte[] dst = new byte[4];

	int pos = 0;
	int start = 0;
	while ( start < data.length() &&
		( pos = data.indexOf(".",start) ) > start &&
		pos-start>3 )
	{
	    try {
		octet = (Integer.valueOf(data.substring(start,pos-start))).intValue();
	    } catch ( NumberFormatException ex ) {
		return null;
	    }
	    if ( octet < 0 || octet > 255 )
		return null;
	    dst[octets++] = (byte)(octet & 0xff);;

	    start = pos + 1;
	}

	if ( octets < 4 )
	    return null;

	return dst;
    }

	/**
	 * Parse the given IPv6 into DER encoded byte array representation.<br />
	 * <br />
	 * <b>TODO: implement this</b>
	 *
	 * @param the IP in well known String format
	 *
	 * @return the IP as byte array or <code>null</code> if not parseable
	 **/
    private static byte[] parseIPv6(String data)
    {
	return null;
    }
	/**
	 * Parse the given URI into DER encoded byte array representation.
	 *
	 * @param the URI in well known String format
	 *
	 * @return the URI as byte array
	 *
	 * @exception IOException if the String could not be parsed
	 **/
    private static byte[] parseURI(String data)
	throws IOException
    {
	    //TODO do parsing test
	DERObject derData = new DERIA5String(data);
	ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	DEROutputStream derOutStream = new DEROutputStream(outStream);
	derOutStream.writeObject(derData);
	derOutStream.close();
	return outStream.toByteArray();
    }

	/**
	 * Parse the given rfc822 addr-spec into DER encoded byte array representation.
	 *
	 * @param the rfc822 addr-spec in well known String format
	 *
	 * @return the rfc822 addr-spec as byte array
	 *
	 * @exception IOException if the String could not be parsed
	 **/
    private static byte[] parseRfc822(String data)
	throws IOException
    {
	int tmpInt = data.indexOf("@");
	if ( tmpInt < 0 || tmpInt >= data.length()-1 )
	    throw new IOException("wrong format of rfc822Name:" + data);
	    //TODO more test for illegal charateers
	DERObject derData = new DERIA5String(data);
	ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	DEROutputStream derOutStream = new DEROutputStream(outStream);
	derOutStream.writeObject(derData);
	derOutStream.close();
	return outStream.toByteArray();
    }

	/**
	 * Parse the given DNS name into DER encoded byte array representation.
	 * The String must be in den preffered name syntax as defined in RFC 1034.
	 *
	 * @param the DNS name in well known String format
	 *
	 * @return the DNS name as byte array
	 *
	 * @exception IOException if the String could not be parsed
	 **/
    private static byte[] parseDNSName(String data)
	throws IOException
    {
	    //TODO more test for illegal charateers
	DERObject derData = new DERIA5String(data);
	ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	DEROutputStream derOutStream = new DEROutputStream(outStream);
	derOutStream.writeObject(derData);
	derOutStream.close();
	return outStream.toByteArray();
    }

	/**
	 * Parse the given X.509 name into DER encoded byte array representation.
	 *
	 * @param the X.509 name in well known String format
	 *
	 * @return the X.509 name as byte array
	 *
	 * @exception IOException if the String could not be parsed
	 **/
    private static byte[] parseX509Name(String data)
	throws IOException
    {
	    //TODO more test for illegal charateers
	ByteArrayOutputStream outStream = new ByteArrayOutputStream();
	DEROutputStream derOutStream = new DEROutputStream(outStream);
	derOutStream.writeObject(new X509Name(trimX509Name(data)));
	derOutStream.close();
	return outStream.toByteArray();
    }

	/**
	 * Returns the given name converted to upper case and all multi spaces squezed
	 * to one space.
	 **/
    static String trimX509Name(String name)
    {
	String data = name.trim().toUpperCase();
	int pos;
	while ( (pos = data.indexOf("  ")) >= 0  )
	{
	    data = data.substring(0,pos) + data.substring(pos+1);
	}
	while ( (pos = data.indexOf(" =")) >= 0  )
	{
	    data = data.substring(0,pos) + data.substring(pos+1);
	}
	while ( (pos = data.indexOf("= ")) >= 0  )
	{
	    data = data.substring(0,pos+1) + data.substring(pos+2);
	}
	return data;
    }
}
