package org.bouncycastle.asn1.x509;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.util.Properties;

/**
 * <pre>
 *     Extensions        ::=   SEQUENCE SIZE (1..MAX) OF Extension
 *
 *     Extension         ::=   SEQUENCE {
 *        extnId            EXTENSION.&amp;id ({ExtensionSet}),
 *        critical          BOOLEAN DEFAULT FALSE,
 *        extnValue         OCTET STRING }
 * </pre>
 */
public class Extensions
    extends ASN1Object
{
    private Hashtable extensions = new Hashtable();
    private Vector ordering = new Vector();

    public static Extension getExtension(Extensions extensions, ASN1ObjectIdentifier oid)
    {
        return null == extensions ? null : extensions.getExtension(oid);
    }

    public static ASN1Encodable getExtensionParsedValue(Extensions extensions, ASN1ObjectIdentifier oid)
    {
        return null == extensions ? null : extensions.getExtensionParsedValue(oid);
    }

    public static ASN1OctetString getExtensionValue(Extensions extensions, ASN1ObjectIdentifier oid)
    {
        return null == extensions ? null : extensions.getExtensionValue(oid);
    }

    public static Extensions getInstance(
        ASN1TaggedObject obj,
        boolean explicit)
    {
        return getInstance(ASN1Sequence.getInstance(obj, explicit));
    }

    public static Extensions getInstance(
        Object obj)
    {
        if (obj instanceof Extensions)
        {
            return (Extensions)obj;
        }
        else if (obj != null)
        {
            return new Extensions(ASN1Sequence.getInstance(obj));
        }

        return null;
    }

    /**
     * Constructor from ASN1Sequence.
     * <p>
     * The extensions are a list of constructed sequences, either with (OID, OctetString) or (OID, Boolean, OctetString)
     * </p>
     */
    private Extensions()
    {
        // used by reviewStructure() to drive parse() in collect-all mode;
        // the resulting instance is never published.
    }

    private Extensions(
        ASN1Sequence seq)
    {
        parse(seq, null);
    }

    /**
     * Re-run the parse of a candidate Extensions SEQUENCE collecting every problem the strict
     * {@code getInstance(...)} path would have thrown (e.g. repeated extensions), instead of
     * stopping at the first. The strict path and this share one parse method (see
     * {@link #parse}); only the error sink differs. Intended for diagnostic/reporting tooling
     * (github #1508); it does not relax the repeated-extension rule (the
     * "org.bouncycastle.x509.ignore_repeated_extensions" property still governs whether a
     * repeat is a problem at all).
     *
     * @param seq the candidate Extensions SEQUENCE.
     * @return the exceptions the strict path would have thrown, one per problem, in parse
     *         order (empty if none).
     */
    public static List reviewStructure(ASN1Sequence seq)
    {
        List errors = new ArrayList();
        try
        {
            new Extensions().parse(seq, errors);
        }
        catch (RuntimeException e)
        {
            errors.add(e);
        }
        return errors;
    }

    private void parse(ASN1Sequence seq, List errors)
    {
        // it's tempting to check there's at least one entry in the sequence. Don't!
        // It turns out there's quite a few empty extension blocks out there...

        Enumeration e = seq.getObjects();

        while (e.hasMoreElements())
        {
            Extension ext = Extension.getInstance(e.nextElement());

            if (extensions.containsKey(ext.getExtnId()))
            {
                if (!Properties.isOverrideSet("org.bouncycastle.x509.ignore_repeated_extensions"))
                {
                    reportProblem(errors, "repeated extension found: " + ext.getExtnId());
                    // collecting only: skip the duplicate, keeping the first occurrence.
                    continue;
                }
            }

            extensions.put(ext.getExtnId(), ext);
            ordering.addElement(ext.getExtnId());
        }
    }

    private static void reportProblem(List errors, String message)
    {
        IllegalArgumentException problem = new IllegalArgumentException(message);
        if (errors == null)
        {
            throw problem;
        }
        errors.add(problem);
    }

    /**
     * Base Constructor
     *
     * @param extension a single extension.
     */
    public Extensions(
        Extension extension)
    {
        this.ordering.addElement(extension.getExtnId());
        this.extensions.put(extension.getExtnId(), extension);
    }

    /**
     * Base Constructor
     *
     * @param extensions an array of extensions.
     */
    public Extensions(
        Extension[] extensions)
    {
        if (extensions == null || extensions.length == 0)
        {
            throw new IllegalArgumentException("extension array cannot be null or empty");
        }

        for (int i = 0; i != extensions.length; i++)
        {
            Extension ext = extensions[i];

            this.ordering.addElement(ext.getExtnId());
            this.extensions.put(ext.getExtnId(), ext);
        }
    }

    /**
     * return an Enumeration of the extension field's object ids.
     */
    public Enumeration oids()
    {
        return ordering.elements();
    }

    /**
     * return the extension represented by the object identifier
     * passed in.
     *
     * @return the extension if it's present, null otherwise.
     */
    public Extension getExtension(ASN1ObjectIdentifier oid)
    {
        return (Extension)extensions.get(oid);
    }

    /**
     * return the parsed value of the extension represented by the object identifier
     * passed in.
     *
     * @return the parsed value of the extension if it's present, null otherwise.
     */
    public ASN1Encodable getExtensionParsedValue(ASN1ObjectIdentifier oid)
    {
        Extension ext = getExtension(oid);
        return ext == null ? null : ext.getParsedValue();
    }

    /**
     * return the value of the extension represented by the object identifier passed in.
     *
     * @return the value of the extension if it's present, null otherwise.
     */
    public ASN1OctetString getExtensionValue(ASN1ObjectIdentifier oid)
    {
        Extension ext = getExtension(oid);
        return ext == null ? null : ext.getExtnValue();
    }

    /**
     * <pre>
     *     Extensions        ::=   SEQUENCE SIZE (1..MAX) OF Extension
     *
     *     Extension         ::=   SEQUENCE {
     *        extnId            EXTENSION.&amp;id ({ExtensionSet}),
     *        critical          BOOLEAN DEFAULT FALSE,
     *        extnValue         OCTET STRING }
     * </pre>
     */
    public ASN1Primitive toASN1Primitive()
    {
        ASN1EncodableVector vec = new ASN1EncodableVector(ordering.size());

        Enumeration e = ordering.elements();
        while (e.hasMoreElements())
        {
            ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier)e.nextElement();
            Extension ext = (Extension)extensions.get(oid);

            vec.add(ext);
        }

        return new DERSequence(vec);
    }

    public boolean equivalent(
        Extensions other)
    {
        if (extensions.size() != other.extensions.size())
        {
            return false;
        }

        Enumeration e1 = extensions.keys();

        while (e1.hasMoreElements())
        {
            Object key = e1.nextElement();

            if (!extensions.get(key).equals(other.extensions.get(key)))
            {
                return false;
            }
        }

        return true;
    }

    public ASN1ObjectIdentifier[] getExtensionOIDs()
    {
        return toOidArray(ordering);
    }

    public ASN1ObjectIdentifier[] getNonCriticalExtensionOIDs()
    {
        return getExtensionOIDs(false);
    }

    public ASN1ObjectIdentifier[] getCriticalExtensionOIDs()
    {
        return getExtensionOIDs(true);
    }

    public boolean hasAnyCriticalExtensions()
    {
        for (int i = 0; i != ordering.size(); i++)
        {
            Object oid = ordering.elementAt(i);

            if (((Extension)extensions.get(oid)).isCritical())
            {
                return true;
            }
        }

        return false;
    }

    private ASN1ObjectIdentifier[] getExtensionOIDs(boolean isCritical)
    {
        Vector oidVec = new Vector();

        for (int i = 0; i != ordering.size(); i++)
        {
            Object oid = ordering.elementAt(i);

            if (((Extension)extensions.get(oid)).isCritical() == isCritical)
            {
                oidVec.addElement(oid);
            }
        }

        return toOidArray(oidVec);
    }

    private ASN1ObjectIdentifier[] toOidArray(Vector oidVec)
    {
        ASN1ObjectIdentifier[] oids = new ASN1ObjectIdentifier[oidVec.size()];

        for (int i = 0; i != oids.length; i++)
        {
            oids[i] = (ASN1ObjectIdentifier)oidVec.elementAt(i);
        }
        return oids;
    }
}
