package org.spongycastle.x509;

import org.spongycastle.asn1.ASN1InputStream;
import org.spongycastle.asn1.ASN1Object;
import org.spongycastle.asn1.ASN1Primitive;
import org.spongycastle.asn1.DEROctetString;
import org.spongycastle.asn1.x509.GeneralName;
import org.spongycastle.asn1.x509.Target;
import org.spongycastle.asn1.x509.TargetInformation;
import org.spongycastle.asn1.x509.Targets;
import org.spongycastle.asn1.x509.X509Extensions;
import org.spongycastle.util.Selector;

import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509CertSelector;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * This class is an <code>Selector</code> like implementation to select
 * attribute certificates from a given set of criteria.
 * 
 * @see org.spongycastle.x509.X509AttributeCertificate
 * @see org.spongycastle.x509.X509Store
 */
public class X509AttributeCertStoreSelector
    implements Selector
{

    // TODO: name constraints???

    private AttributeCertificateHolder holder;

    private AttributeCertificateIssuer issuer;

    private BigInteger serialNumber;

    private Date attributeCertificateValid;

    private X509AttributeCertificate attributeCert;

    private Collection targetNames = new HashSet();

    private Collection targetGroups = new HashSet();

    public X509AttributeCertStoreSelector()
    {
        super();
    }

    /**
     * Decides if the given attribute certificate should be selected.
     * 
     * @param obj The attribute certificate which should be checked.
     * @return <code>true</code> if the attribute certificate can be selected,
     *         <code>false</code> otherwise.
     */
    public boolean match(Object obj)
    {
        if (!(obj instanceof X509AttributeCertificate))
        {
            return false;
        }

        X509AttributeCertificate attrCert = (X509AttributeCertificate) obj;

        if (this.attributeCert != null)
        {
            if (!this.attributeCert.equals(attrCert))
            {
                return false;
            }
        }
        if (serialNumber != null)
        {
            if (!attrCert.getSerialNumber().equals(serialNumber))
            {
                return false;
            }
        }
        if (holder != null)
        {
            if (!attrCert.getHolder().equals(holder))
            {
                return false;
            }
        }
        if (issuer != null)
        {
            if (!attrCert.getIssuer().equals(issuer))
            {
                return false;
            }
        }

        if (attributeCertificateValid != null)
        {
            try
            {
                attrCert.checkValidity(attributeCertificateValid);
            }
            catch (CertificateExpiredException e)
            {
                return false;
            }
            catch (CertificateNotYetValidException e)
            {
                return false;
            }
        }
        if (!targetNames.isEmpty() || !targetGroups.isEmpty())
        {

            byte[] targetInfoExt = attrCert
                .getExtensionValue(X509Extensions.TargetInformation.getId());
            if (targetInfoExt != null)
            {
                TargetInformation targetinfo;
                try
                {
                    targetinfo = TargetInformation
                        .getInstance(new ASN1InputStream(
                            ((DEROctetString) DEROctetString
                                .fromByteArray(targetInfoExt)).getOctets())
                            .readObject());
                }
                catch (IOException e)
                {
                    return false;
                }
                catch (IllegalArgumentException e)
                {
                    return false;
                }
                Targets[] targetss = targetinfo.getTargetsObjects();
                if (!targetNames.isEmpty())
                {
                    boolean found = false;

                    for (int i=0; i<targetss.length; i++)
                    {
                        Targets t = targetss[i];
                        Target[] targets = t.getTargets();
                        for (int j=0; j<targets.length; j++)
                        {
                            if (targetNames.contains(targets[j]
                                                       .getTargetName()))
                            {
                                found = true;
                                break;
                            }
                        }
                    }
                    if (!found)
                    {
                        return false;
                    }
                }
                if (!targetGroups.isEmpty())
                {
                    boolean found = false;

                    for (int i=0; i<targetss.length; i++)
                    {
                        Targets t = targetss[i];
                        Target[] targets = t.getTargets();
                        for (int j=0; j<targets.length; j++)
                        {
                            if (targetGroups.contains(targets[j]
                                                        .getTargetGroup()))
                            {
                                found = true;
                                break;
                            }
                        }
                    }
                    if (!found)
                    {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Returns a clone of this object.
     * 
     * @return the clone.
     */
    public Object clone()
    {
        X509AttributeCertStoreSelector sel = new X509AttributeCertStoreSelector();
        sel.attributeCert = attributeCert;
        sel.attributeCertificateValid = getAttributeCertificateValid();
        sel.holder = holder;
        sel.issuer = issuer;
        sel.serialNumber = serialNumber;
        sel.targetGroups = getTargetGroups();
        sel.targetNames = getTargetNames();
        return sel;
    }

    /**
     * Returns the attribute certificate which must be matched.
     * 
     * @return Returns the attribute certificate.
     */
    public X509AttributeCertificate getAttributeCert()
    {
        return attributeCert;
    }

    /**
     * Set the attribute certificate to be matched. If <code>null</code> is
     * given any will do.
     * 
     * @param attributeCert The attribute certificate to set.
     */
    public void setAttributeCert(X509AttributeCertificate attributeCert)
    {
        this.attributeCert = attributeCert;
    }

    /**
     * Get the criteria for the validity.
     * 
     * @return Returns the attributeCertificateValid.
     */
    public Date getAttributeCertificateValid()
    {
        if (attributeCertificateValid != null)
        {
            return new Date(attributeCertificateValid.getTime());
        }

        return null;
    }

    /**
     * Set the time, when the certificate must be valid. If <code>null</code>
     * is given any will do.
     * 
     * @param attributeCertificateValid The attribute certificate validation
     *            time to set.
     */
    public void setAttributeCertificateValid(Date attributeCertificateValid)
    {
        if (attributeCertificateValid != null)
        {
            this.attributeCertificateValid = new Date(attributeCertificateValid
                .getTime());
        }
        else
        {
            this.attributeCertificateValid = null;
        }
    }

    /**
     * Gets the holder.
     * 
     * @return Returns the holder.
     */
    public AttributeCertificateHolder getHolder()
    {
        return holder;
    }

    /**
     * Sets the holder. If <code>null</code> is given any will do.
     * 
     * @param holder The holder to set.
     */
    public void setHolder(AttributeCertificateHolder holder)
    {
        this.holder = holder;
    }

    /**
     * Returns the issuer criterion.
     * 
     * @return Returns the issuer.
     */
    public AttributeCertificateIssuer getIssuer()
    {
        return issuer;
    }

    /**
     * Sets the issuer the attribute certificate must have. If <code>null</code>
     * is given any will do.
     * 
     * @param issuer The issuer to set.
     */
    public void setIssuer(AttributeCertificateIssuer issuer)
    {
        this.issuer = issuer;
    }

    /**
     * Gets the serial number the attribute certificate must have.
     * 
     * @return Returns the serialNumber.
     */
    public BigInteger getSerialNumber()
    {
        return serialNumber;
    }

    /**
     * Sets the serial number the attribute certificate must have. If
     * <code>null</code> is given any will do.
     * 
     * @param serialNumber The serialNumber to set.
     */
    public void setSerialNumber(BigInteger serialNumber)
    {
        this.serialNumber = serialNumber;
    }

    /**
     * Adds a target name criterion for the attribute certificate to the target
     * information extension criteria. The <code>X509AttributeCertificate</code>
     * must contain at least one of the specified target names.
     * <p>
     * Each attribute certificate may contain a target information extension
     * limiting the servers where this attribute certificate can be used. If
     * this extension is not present, the attribute certificate is not targeted
     * and may be accepted by any server.
     *
     * @param name The name as a GeneralName (not <code>null</code>)
     */
    public void addTargetName(GeneralName name)
    {
        targetNames.add(name);
    }

    /**
     * Adds a target name criterion for the attribute certificate to the target
     * information extension criteria. The <code>X509AttributeCertificate</code>
     * must contain at least one of the specified target names.
     * <p>
     * Each attribute certificate may contain a target information extension
     * limiting the servers where this attribute certificate can be used. If
     * this extension is not present, the attribute certificate is not targeted
     * and may be accepted by any server.
     *
     * @param name a byte array containing the name in ASN.1 DER encoded form of a GeneralName
     * @throws IOException if a parsing error occurs.
     */
    public void addTargetName(byte[] name) throws IOException
    {
        addTargetName(GeneralName.getInstance(ASN1Primitive.fromByteArray(name)));
    }

    /**
     * Adds a collection with target names criteria. If <code>null</code> is
     * given any will do.
     * <p>
     * The collection consists of either GeneralName objects or byte[] arrays representing
     * DER encoded GeneralName structures.
     * 
     * @param names A collection of target names.
     * @throws IOException if a parsing error occurs.
     * @see #addTargetName(byte[])
     * @see #addTargetName(GeneralName)
     */
    public void setTargetNames(Collection names) throws IOException
    {
        targetNames = extractGeneralNames(names);
    }

    /**
     * Gets the target names. The collection consists of <code>List</code>s
     * made up of an <code>Integer</code> in the first entry and a DER encoded
     * byte array or a <code>String</code> in the second entry.
     * <p>
     * The returned collection is immutable.
     * 
     * @return The collection of target names
     * @see #setTargetNames(Collection)
     */
    public Collection getTargetNames()
    {
        return Collections.unmodifiableCollection(targetNames);
    }

    /**
     * Adds a target group criterion for the attribute certificate to the target
     * information extension criteria. The <code>X509AttributeCertificate</code>
     * must contain at least one of the specified target groups.
     * <p>
     * Each attribute certificate may contain a target information extension
     * limiting the servers where this attribute certificate can be used. If
     * this extension is not present, the attribute certificate is not targeted
     * and may be accepted by any server.
     *
     * @param group The group as GeneralName form (not <code>null</code>)
     */
    public void addTargetGroup(GeneralName group)
    {
        targetGroups.add(group);
    }

    /**
     * Adds a target group criterion for the attribute certificate to the target
     * information extension criteria. The <code>X509AttributeCertificate</code>
     * must contain at least one of the specified target groups.
     * <p>
     * Each attribute certificate may contain a target information extension
     * limiting the servers where this attribute certificate can be used. If
     * this extension is not present, the attribute certificate is not targeted
     * and may be accepted by any server.
     *
     * @param name a byte array containing the group in ASN.1 DER encoded form of a GeneralName
     * @throws IOException if a parsing error occurs.
     */
    public void addTargetGroup(byte[] name) throws IOException
    {
        addTargetGroup(GeneralName.getInstance(ASN1Primitive.fromByteArray(name)));
    }

    /**
     * Adds a collection with target groups criteria. If <code>null</code> is
     * given any will do.
     * <p>
     * The collection consists of <code>GeneralName</code> objects or <code>byte[]</code representing DER
     * encoded GeneralNames.
     * 
     * @param names A collection of target groups.
     * @throws IOException if a parsing error occurs.
     * @see #addTargetGroup(byte[])
     * @see #addTargetGroup(GeneralName)
     */
    public void setTargetGroups(Collection names) throws IOException
    {
        targetGroups = extractGeneralNames(names);
    }



    /**
     * Gets the target groups. The collection consists of <code>List</code>s
     * made up of an <code>Integer</code> in the first entry and a DER encoded
     * byte array or a <code>String</code> in the second entry.
     * <p>
     * The returned collection is immutable.
     *
     * @return The collection of target groups.
     * @see #setTargetGroups(Collection)
     */
    public Collection getTargetGroups()
    {
        return Collections.unmodifiableCollection(targetGroups);
    }

    private Set extractGeneralNames(Collection names)
        throws IOException
    {
        if (names == null || names.isEmpty())
        {
            return new HashSet();
        }
        Set temp = new HashSet();
        for (Iterator it = names.iterator(); it.hasNext();)
        {
            Object o = it.next();
            if (o instanceof GeneralName)
            {
                temp.add(o);
            }
            else
            {
                temp.add(GeneralName.getInstance(ASN1Primitive.fromByteArray((byte[])o)));
            }
        }
        return temp;
    }
}
