package org.bouncycastle.jce.provider;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.security.NoSuchProviderException;
import org.bouncycastle.jce.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import org.bouncycastle.jce.cert.CertificateFactory;
import org.bouncycastle.openssl.PEMWriter;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.pkcs.ContentInfo;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.SignedData;
import org.bouncycastle.jce.X509Principal;
import org.bouncycastle.jce.PrincipalUtil;

/**
 * CertPath implementation for X.509 certificates.
 * <br />
 **/
public  class PKIXCertPath
    extends CertPath
{
    static final List certPathEncodings;
    
    static
    {
        List encodings = new ArrayList();
        encodings.add("PkiPath");
        encodings.add("PEM");
        encodings.add("PKCS7");
        certPathEncodings = Collections.unmodifiableList( encodings );
    }
    
    private List certificates;

    /**
     * @param certs
     */
    private List sortCerts(
        List certs)
    {
        if (certs.size() == 1)
        {
            return certs;
        }
        
        try
        {
            X509Principal   issuer = PrincipalUtil.getIssuerX509Principal(((X509Certificate)certs.get(0)));
            boolean         okay = true;
            
            for (int i = 1; i != certs.size(); i++) 
            {
                X509Certificate cert = (X509Certificate)certs.get(i);
                
                if (issuer.equals(PrincipalUtil.getSubjectX509Principal(cert)))
                {
                    issuer = PrincipalUtil.getIssuerX509Principal(((X509Certificate)certs.get(i)));
                }
                else
                {
                    okay = false;
                    break;
                }
            }
            
            if (okay)
            {
                return certs;
            }
            
            // find end-entity cert
            ArrayList       retList = new ArrayList(certs.size());
            
            for (int i = 0; i < certs.size(); i++)
            {
                X509Certificate cert = (X509Certificate)certs.get(i);
                boolean         found = false;
                
                X509Principal   subject = PrincipalUtil.getSubjectX509Principal(cert);
                
                for (int j = 0; j != certs.size(); j++)
                {
                    X509Certificate c = (X509Certificate)certs.get(j);
                    if (PrincipalUtil.getIssuerX509Principal(c).equals(subject))
                    {
                        found = true;
                        break;
                    }
                }
                
                if (!found)
                {
                    retList.add(cert);
                    certs.remove(i);
                }
            }
            
            // can only have one end entity cert - something's wrong, give up.
            if (retList.size() > 1)
            {
                for (int i = 0; i != certs.size(); i++)
                {
                    retList.add(certs.get(i));
                }
                
                return retList;
            }

            for (int i = 0; i != retList.size(); i++)
            {
                issuer = PrincipalUtil.getIssuerX509Principal((X509Certificate)retList.get(i));
                
                for (int j = 0; j < certs.size(); j++)
                {
                    X509Certificate c = (X509Certificate)certs.get(j);
                    if (issuer.equals(PrincipalUtil.getSubjectX509Principal(c)))
                    {
                        retList.add(c);
                        certs.remove(j);
                        break;
                    }
                }
            }
            
            // make sure all certificates are accounted for.
            for (int i = 0; i != certs.size(); i++)
            {
                retList.add(certs.get(i));
            }
            
            return retList;
        }
        catch (Exception e)
        {
            return certs;
        }
    }
    
    /**
     * Creates a CertPath of the specified type.
     * This constructor is protected because most users should use
     * a CertificateFactory to create CertPaths.
     * @param type the standard name of the type of Certificatesin this path
     **/
    PKIXCertPath( List certificates )
    {
        super("X.509");
        this.certificates = sortCerts(new ArrayList(certificates));
    }
    
    /**
     * Creates a CertPath of the specified type.
     * This constructor is protected because most users should use
     * a CertificateFactory to create CertPaths.
     *
     * @param type the standard name of the type of Certificatesin this path
     **/
    PKIXCertPath(
        InputStream inStream,
        String encoding)
        throws CertificateException
    {
        super("X.509");
        try {
            if (encoding.equalsIgnoreCase( "PkiPath" ))
            {
                ASN1InputStream derInStream = new ASN1InputStream(inStream);
                DERObject derObject = derInStream.readObject();
                if ( derObject == null || ! ( derObject instanceof ASN1Sequence ) )
                {
                    throw new CertificateException("input stream does not contain a ASN1 SEQUENCE while reading PkiPath encoded data to load CertPath" );
                }
                Enumeration e = ((ASN1Sequence)derObject).getObjects();
                InputStream certInStream;
                ByteArrayOutputStream outStream;
                DEROutputStream derOutStream;
                certificates = new ArrayList();
                CertificateFactory certFactory= CertificateFactory.getInstance( "X.509", "BC" );
                while ( e.hasMoreElements() ) {
                    outStream = new ByteArrayOutputStream();
                    derOutStream = new DEROutputStream(outStream);
        
                    derOutStream.writeObject(e.nextElement());
                    derOutStream.close();
    
                    certInStream = new ByteArrayInputStream(outStream.toByteArray());
                    certificates.add(0,certFactory.generateCertificate(certInStream));
                }
            }
            else if (encoding.equalsIgnoreCase("PKCS7") || encoding.equalsIgnoreCase("PEM"))
            {
                inStream = new BufferedInputStream(inStream);
                certificates = new ArrayList();
                CertificateFactory certFactory= CertificateFactory.getInstance( "X.509", "BC" );
                Certificate cert = null;
                while ((cert = certFactory.generateCertificate(inStream)) != null)
                {
                    certificates.add(cert);
                }
            }
            else
            {
                throw new CertificateException( "unsupported encoding: " + encoding);
            }
        }
        catch (IOException ex) 
        {
            throw new CertificateException( "IOException throw while decoding CertPath:\n" + ex.toString() ); 
        }
        catch (NoSuchProviderException ex ) 
        {
            throw new CertificateException( "BouncyCastle provider not found while trying to get a CertificateFactory:\n" + ex.toString() ); 
        }

        this.certificates = sortCerts(certificates);
    }
    
    /**
     * Returns an iteration of the encodings supported by this
     * certification path, with the default encoding
     * first. Attempts to modify the returned Iterator via its
     * remove method result in an UnsupportedOperationException.
     *
     * @return an Iterator over the names of the supported encodings (as Strings)
     **/
    public Iterator getEncodings()
    {
        return certPathEncodings.iterator();
    }
    
    /**
     * Returns the encoded form of this certification path, using
     * the default encoding.
     *
     * @return the encoded bytes
     * @exception CertificateEncodingException if an encoding error occurs
     **/
    public byte[] getEncoded()
        throws CertificateEncodingException
    {
        Iterator iter = getEncodings();
        if ( iter.hasNext() )
        {
            Object enc = iter.next();
            if ( enc instanceof String )
            {
            return getEncoded((String)enc);
            }
        }
        return null;
    }
    
    /**
     * Returns the encoded form of this certification path, using
     * the specified encoding.
     *
     * @param encoding the name of the encoding to use
     * @return the encoded bytes
     * @exception CertificateEncodingException if an encoding error
     * occurs or the encoding requested is not supported
     *
     **/
    public byte[] getEncoded(String encoding)
        throws CertificateEncodingException
    {
        if (encoding.equalsIgnoreCase("PkiPath"))
        {
            ASN1EncodableVector v = new ASN1EncodableVector();
    
            ListIterator iter = certificates.listIterator(certificates.size());
            while ( iter.hasPrevious() )
            {
                v.add(toASN1Object((X509Certificate)iter.previous()));
            }
    
            return toDEREncoded(new DERSequence(v));
        }
        else if (encoding.equalsIgnoreCase("PKCS7"))
        {
            ContentInfo encInfo = new ContentInfo(PKCSObjectIdentifiers.data, null);
    
            ASN1EncodableVector v = new ASN1EncodableVector();
            for (int i = 0; i != certificates.size(); i++)
            {
                v.add(toASN1Object((X509Certificate)certificates.get(i)));
            }
            
            SignedData  sd = new SignedData(
                                     new DERInteger(1),
                                     new DERSet(),
                                     encInfo, 
                                     new DERSet(v), 
                                     null, 
                                     new DERSet());
    
            return toDEREncoded(new ContentInfo(
                    PKCSObjectIdentifiers.signedData, sd));
        }
        else if (encoding.equalsIgnoreCase("PEM"))
        {
            ByteArrayOutputStream bOut = new ByteArrayOutputStream();
            PEMWriter             pWrt = new PEMWriter(new OutputStreamWriter(bOut));
    
            try
            {
                for (int i = 0; i != certificates.size(); i++)
                {
                    pWrt.writeObject(certificates.get(i));
                }
            
                pWrt.close();
            }
            catch (Exception e)
            {
                throw new CertificateEncodingException("can't encode certificate for PEM encoded path");
            }
    
            return bOut.toByteArray();
        }
        else
        {
            throw new CertificateEncodingException("unsupported encoding: " + encoding);
        }
    }
    
    /**
     * Returns the list of certificates in this certification
     * path. The List returned must be immutable and thread-safe. 
     *
     * @return an immutable List of Certificates (may be empty, but not null)
     **/
    public List getCertificates()
    {
        return Collections.unmodifiableList(new ArrayList(certificates));
    }
    
    /**
     * Return a DERObject containing the encoded certificate.
     *
     * @param cert the X509Certificate object to be encoded
     *
     * @return the DERObject
     **/
    private DERObject toASN1Object(
        X509Certificate cert )
        throws CertificateEncodingException
    {
        try
        {
            return new ASN1InputStream(cert.getEncoded()).readObject();
        }
        catch (Exception e)
        {
            throw new CertificateEncodingException("Exception while encoding certificate: " + e.toString());
        }
    }
    
    private byte[] toDEREncoded(ASN1Encodable obj) 
        throws CertificateEncodingException
    {
        try
        {
            ByteArrayOutputStream bOut = new ByteArrayOutputStream();
            DEROutputStream       dOut = new DEROutputStream(bOut);
            
            dOut.writeObject(obj);
            dOut.close();
            
            return bOut.toByteArray();
        }
        catch (IOException e)
        {
            throw new CertificateEncodingException("Exeption thrown: " + e);
        }
    }
}
