package org.bouncycastle.jce.provider;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.bouncycastle.asn1.x509.X509Name;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.cert.*;

/**
 * Implements the PKIX CertPathBuilding algorithem for BouncyCastle.<br />
 * <br />
 * <b>MAYBE: implement more CertPath validation whil build path to omit invalid pathes</b>
 *
 * @see CertPathBuilderSpi
 **/
public class PKIXCertPathBuilderSpi extends CertPathBuilderSpi
{
	/**
	 * Build and validate a CertPath using the given parameter.
	 *
	 * @param params PKIXBuilderParameters object containing all
	 * information to build the CertPath
	 **/
    public  CertPathBuilderResult engineBuild( CertPathParameters params )
	throws CertPathBuilderException,
	InvalidAlgorithmParameterException 
    {
	if ( ! ( params instanceof PKIXBuilderParameters ) )
	    throw new InvalidAlgorithmParameterException("params must be a PKIXBuilderParameters instance");
	PKIXBuilderParameters pkixParams = (PKIXBuilderParameters)params;

	Iterator iter;
	Collection targets;
	Iterator targetIter;
	List certPathList = new ArrayList();
	TrustAnchor trust = null;
	X509Certificate cert;
	X509CertSelector certSelectX509 = null;
	Collection certs;
	CertPath certPath = null;
	CertPathValidatorException certPathException = null;

	try {
		// search target certificates
	    CertSelector certSelect = pkixParams.getTargetCertConstraints();
	    if ( certSelect == null )
		throw new CertPathBuilderException("targetCertConstraints must be non-null for CertPath building");
	    targets = findCertificates(certSelect, pkixParams.getCertStores() );
	    if ( targets.isEmpty() )
		throw new CertPathBuilderException("no certificate found matching targetCertContraints");

		// check all potential target certificates
	    targetIter = targets.iterator();
	    while ( targetIter.hasNext() )
        {
            cert = (X509Certificate)targetIter.next();
            certPathList.clear();
            while ( cert != null )
            {
                // add cert to the certpath
                certPathList.add( cert );

                // check wether the issuer of <cert> is a TrustAnchor 
                if ( findTrustAnchor(cert, pkixParams.getTrustAnchors()) != null )
                {
                try {
                    certPath = CertificateFactory.getInstance("X.509", "BC").generateCertPath( certPathList );
                    CertPathValidator validator = CertPathValidator.getInstance("PKIX", "BC");
                    PKIXCertPathValidatorResult result =
                    (PKIXCertPathValidatorResult) validator.validate(certPath, pkixParams);
                    return new PKIXCertPathBuilderResult(certPath,
                                     result.getTrustAnchor(),
                                     result.getPolicyTree(),
                                     result.getPublicKey() );
                } catch ( CertPathValidatorException ex ) {
                    certPathException = ex;
                }
                    // if validation failed go to next certificate
                cert = null;
                }
                else
                {
                    // try to get the issuer certificate from one
                    // of the CertStores
                try {
                    cert = findIssuer(cert, pkixParams.getCertStores());
                } catch ( CertPathValidatorException ex ) {
                    certPathException = ex;
                    cert = null;
                }
                }
            }
        }
	} catch ( Exception ex ) {
	    throw new CertPathBuilderException( 
		"Exception thrown while doing CertPath building\n", ex );
	}

	if ( certPath != null )
	    throw new CertPathBuilderException("found certificate chain, but could not be validated", certPathException);

	throw new CertPathBuilderException("unable to find certificate chain");
    }

	/**
	 * Search the given Set of TrustAnchor's for one that is the
	 * issuer of the fiven X509 certificate.
	 *
	 * @param cert the X509 certificate
	 * @param trustAnchors a Set of TrustAnchor's
	 *
	 * @return the <code>TrustAnchor</code> object if found or
	 * <code>null</code> if not.
	 *
	 * @exception CertPathValidatorException if a TrustAnchor  was
	 * found but the signature verificytion on the given certificate
	 * has thrown an exception. This Exception can be obtainted with
	 * <code>getCause()</code> method.
	 **/
    static final TrustAnchor findTrustAnchor(X509Certificate cert, Set trustAnchors) 
	throws CertPathValidatorException
    {
	Iterator iter = trustAnchors.iterator();
	TrustAnchor trust = null;
	PublicKey trustPublicKey = null;
	Exception invalidKeyEx = null;

	X509CertSelector certSelectX509 = new X509CertSelector();
	try {
	    certSelectX509.setSubject(((X509Principal)cert.getIssuerDN()).getEncoded());
	} catch ( IOException ex ) {
	    ex.printStackTrace();
	    return null;
	}

	while ( iter.hasNext() && trust == null )
	{
	    trust = (TrustAnchor)iter.next();
	    if ( trust.getTrustedCert() != null )
	    {
		if ( certSelectX509.match(trust.getTrustedCert()) )
		{
		    trustPublicKey = trust.getTrustedCert().getPublicKey();
		}
		else
		{
		    trust = null;
		}
	    }
	    else if  ( trust.getCAName() != null && trust.getCAPublicKey() != null )
	    {
		try {
		    X509Name certIssuer = X509Name.getInstance(cert.getIssuerDN());
		    X509Name caName = X509Name.getInstance(trust.getCAName());
		    if ( certIssuer.equals(caName) )
		    {
			trustPublicKey = trust.getCAPublicKey();
		    }
		    else
		    {
			trust = null;
		    }
		} catch ( IllegalArgumentException ex ) {
		    trust = null;
		}
	    }
	    else trust = null;
	    
	    if ( trustPublicKey != null )
	    {
		try {
		    cert.verify(trustPublicKey);
		} catch ( Exception ex ) {
		    invalidKeyEx = ex;
		    trust = null;
		}
	    }
	}
	
	if ( trust == null && invalidKeyEx != null )
	    throw new CertPathValidatorException("TrustAnchor found put certificate validation failed",invalidKeyEx,null,-1);

	return trust;
    }

	/**
	 * Return a Collection of all certificates found in the
	 * CertStore's that are matching the certSelect criteriums.
	 *
	 * @param certSelector a {@link CertSelector CertSelector}
	 * object that will be used to select the certificates
	 * @param certStores a List containing only {@link CertStore
	 * CertStore} objects. These are used to search for
	 * certificates
	 *
	 * @return a Collection of all found {@link Certificate Certificate}
	 * objects. May be emtpy but never <code>null</code>.
	 **/
    private static final Collection findCertificates(CertSelector certSelect, List certStores)
    {
	Set certs = new HashSet();
	Iterator iter = certStores.iterator();
	CertStore certStore;
	while ( iter.hasNext() ) {
	    certStore = (CertStore)iter.next();
	    try {
		certs.addAll(certStore.getCertificates(certSelect));
	    } catch ( CertStoreException ex ) {
		ex.printStackTrace();
	    }
	}
	return certs;
    }
    
	/**
	 * Find the issuer certificate of the given certificate.
	 *
	 * @param cert the certificate hows issuer certificate should
	 * be found.
	 * @param certStores a list of <code>CertStore</code> object
	 * that will be searched
	 *
	 * @return then <code>X509Certificate</code> object containing
	 * the issuer certificate or <code>null</code> if not found
	 *
	 * @exception CertPathValidatorException if a TrustAnchor  was
	 * found but the signature verificytion on the given certificate
	 * has thrown an exception. This Exception can be obtainted with
	 * <code>getCause()</code> method.
	 **/
    private static final X509Certificate findIssuer(X509Certificate cert, List certStores)
	throws CertPathValidatorException
    {
	Exception invalidKeyEx = null;
	X509CertSelector certSelect = new X509CertSelector();
	try {
	    certSelect.setSubject(((X509Principal)cert.getIssuerDN()).getEncoded());
	} catch ( IOException ex ) {
	    ex.printStackTrace();
	    return null;
	}

	Iterator iter = findCertificates(certSelect, certStores).iterator();
	X509Certificate issuer = null;
	while ( iter.hasNext() && issuer == null )
	{
	    issuer = (X509Certificate)iter.next();
	    try {
		cert.verify(issuer.getPublicKey());
	    } catch ( Exception ex ) {
		invalidKeyEx = ex;
		issuer = null;
	    }
	}
	if ( issuer == null && invalidKeyEx != null )
	    throw new CertPathValidatorException("issuer found but certificate validation failed",invalidKeyEx,null,-1);

	return issuer;
    }
}
