package org.bouncycastle.asn1;

import java.io.IOException;

import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Exceptions;
import org.bouncycastle.util.Objects;

/**
 * ASN.1 TaggedObject - in ASN.1 notation this is any object preceded by
 * a [n] where n is some number - these are assumed to follow the construction
 * rules (as with sequences).
 */
public abstract class ASN1TaggedObject
    extends ASN1Primitive
    implements ASN1TaggedObjectParser
{
    private static final int DECLARED_EXPLICIT = 1;
    private static final int DECLARED_IMPLICIT = 2;
    // TODO It will probably be better to track parsing constructed vs primitive instead
    private static final int PARSED_EXPLICIT = 3;
    private static final int PARSED_IMPLICIT = 4;

    public static ASN1TaggedObject getContextInstance(Object obj)
    {
        return ASN1Util.checkContextTagClass(checkInstance(obj));
    }

    public static ASN1TaggedObject getContextInstance(Object obj, int tagNo)
    {
        return ASN1Util.checkContextTag(checkInstance(obj), tagNo);
    }

    public static ASN1TaggedObject getContextOptional(ASN1Encodable element)
    {
        ASN1TaggedObject taggedObject = getOptional(element);
        if (taggedObject != null && taggedObject.hasContextTag())
        {
            return taggedObject;
        }
        return null;
    }

    public static ASN1TaggedObject getContextOptional(ASN1Encodable element, int tagNo)
    {
        ASN1TaggedObject taggedObject = getOptional(element);
        if (taggedObject != null && taggedObject.hasContextTag(tagNo))
        {
            return taggedObject;
        }
        return null;
    }

    public static ASN1TaggedObject getInstance(Object obj)
    {
        if (obj == null || obj instanceof ASN1TaggedObject) 
        {
            return (ASN1TaggedObject)obj;
        }
//      else if (obj instanceof ASN1TaggedObjectParser)
        else if (obj instanceof ASN1Encodable)
        {
            ASN1Primitive primitive = ((ASN1Encodable)obj).toASN1Primitive();
            if (primitive instanceof ASN1TaggedObject)
            {
                return (ASN1TaggedObject)primitive;
            }
        }
        else if (obj instanceof byte[])
        {
            try
            {
                return checkedCast(fromByteArray((byte[])obj));
            }
            catch (IOException e)
            {
                throw Exceptions.illegalArgumentException("failed to construct tagged object from byte[]", e);
            }
        }

        throw new IllegalArgumentException("unknown object in getInstance: " + obj.getClass().getName());
    }

    public static ASN1TaggedObject getInstance(Object obj, int tagClass)
    {
        return ASN1Util.checkTagClass(checkInstance(obj), tagClass);
    }

    public static ASN1TaggedObject getInstance(Object obj, int tagClass, int tagNo)
    {
        return ASN1Util.checkTag(checkInstance(obj), tagClass, tagNo);
    }

    public static ASN1TaggedObject getInstance(ASN1TaggedObject taggedObject, boolean declaredExplicit)
    {
        return ASN1Util.getExplicitContextBaseTagged(checkInstance(taggedObject, declaredExplicit));
    }

    public static ASN1TaggedObject getInstance(ASN1TaggedObject taggedObject, int tagClass, boolean declaredExplicit)
    {
        return ASN1Util.getExplicitBaseTagged(checkInstance(taggedObject, declaredExplicit), tagClass);
    }

    public static ASN1TaggedObject getInstance(ASN1TaggedObject taggedObject, int tagClass, int tagNo,
        boolean declaredExplicit)
    {
        return ASN1Util.getExplicitBaseTagged(checkInstance(taggedObject, declaredExplicit), tagClass, tagNo);
    }

    public static ASN1TaggedObject getOptional(ASN1Encodable element)
    {
        if (element == null)
        {
            throw new NullPointerException("'element' cannot be null");
        }

        if (element instanceof ASN1TaggedObject)
        {
            return (ASN1TaggedObject)element;
        }

        return null;
    }

    public static ASN1TaggedObject getOptional(ASN1Encodable element, int tagClass)
    {
        ASN1TaggedObject taggedObject = getOptional(element);
        if (taggedObject != null && taggedObject.hasTagClass(tagClass))
        {
            return taggedObject;
        }
        return null;
    }

    public static ASN1TaggedObject getOptional(ASN1Encodable element, int tagClass, int tagNo)
    {
        ASN1TaggedObject taggedObject = getOptional(element);
        if (taggedObject != null && taggedObject.hasTag(tagClass, tagNo))
        {
            return taggedObject;
        }
        return null;
    }

    private static ASN1TaggedObject checkInstance(Object obj)
    {
        if (obj == null)
        {
            throw new NullPointerException("'obj' cannot be null");
        }

        return getInstance(obj);
    }

    private static ASN1TaggedObject checkInstance(ASN1TaggedObject taggedObject, boolean declaredExplicit)
    {
        if (!declaredExplicit)
        {
            throw new IllegalArgumentException("this method not valid for implicitly tagged tagged objects");
        }

        if (taggedObject == null)
        {
            throw new NullPointerException("'taggedObject' cannot be null");
        }

        return taggedObject;
    }

    final int           explicitness;
    final int           tagClass;
    final int           tagNo;
    final ASN1Encodable obj;

    /**
     * Create a tagged object with the style given by the value of explicit.
     * <p>
     * If the object implements ASN1Choice the tag style will always be changed
     * to explicit in accordance with the ASN.1 encoding rules.
     * </p>
     * @param explicit true if the object is explicitly tagged.
     * @param tagNo the tag number for this object.
     * @param obj the tagged object.
     */
    protected ASN1TaggedObject(boolean explicit, int tagNo, ASN1Encodable obj)
    {
        this(explicit, BERTags.CONTEXT_SPECIFIC, tagNo, obj);
    }

    protected ASN1TaggedObject(boolean explicit, int tagClass, int tagNo, ASN1Encodable obj)
    {
        this(explicit ? DECLARED_EXPLICIT : DECLARED_IMPLICIT, tagClass, tagNo, obj);
    }

    ASN1TaggedObject(int explicitness, int tagClass, int tagNo, ASN1Encodable obj)
    {
        if (null == obj)
        {
            throw new NullPointerException("'obj' cannot be null");
        }
        if (tagClass == BERTags.UNIVERSAL || (tagClass & BERTags.PRIVATE) != tagClass)
        {
            throw new IllegalArgumentException("invalid tag class: " + tagClass);
        }

        this.explicitness = (obj instanceof ASN1Choice) ? DECLARED_EXPLICIT : explicitness;
        this.tagClass = tagClass;
        this.tagNo = tagNo;
        this.obj = obj;
    }

    final boolean asn1Equals(ASN1Primitive other)
    {
        if (!(other instanceof ASN1TaggedObject))
        {
            return false;
        }

        ASN1TaggedObject that = (ASN1TaggedObject)other;

        if (this.tagNo != that.tagNo ||
            this.tagClass != that.tagClass)
        {
            return false;
        }

        if (this.explicitness != that.explicitness)
        {
            /*
             * TODO This seems incorrect for some cases of implicit tags e.g. if one is a
             * declared-implicit SET and the other a parsed object.
             */
            if (this.isExplicit() != that.isExplicit())
            {
                return false;
            }
        }

        ASN1Primitive p1 = this.obj.toASN1Primitive();
        ASN1Primitive p2 = that.obj.toASN1Primitive();

        if (p1 == p2)
        {
            return true;
        }

        if (!this.isExplicit())
        {
            try
            {
                byte[] d1 = this.getEncoded();
                byte[] d2 = that.getEncoded();
                
                return Arrays.areEqual(d1, d2);
            }
            catch (IOException e)
            {
                return false;
            }
        }

        return p1.asn1Equals(p2);
    }

    public int hashCode()
    {
        return (tagClass * 7919) ^ tagNo ^ (isExplicit() ? 0x0F : 0xF0) ^ obj.toASN1Primitive().hashCode();
    }

    public int getTagClass()
    {
        return tagClass;
    }

    /**
     * Return the tag number associated with this object.
     *
     * @return the tag number.
     */
    public int getTagNo()
    {
        return tagNo;
    }

    public boolean hasContextTag()
    {
        return this.tagClass == BERTags.CONTEXT_SPECIFIC;
    }

    public boolean hasContextTag(int tagNo)
    {
        return this.tagClass == BERTags.CONTEXT_SPECIFIC && this.tagNo == tagNo;
    }

    public boolean hasTag(int tagClass, int tagNo)
    {
        return this.tagClass == tagClass && this.tagNo == tagNo;
    }

    public boolean hasTagClass(int tagClass)
    {
        return this.tagClass == tagClass;
    }

    /**
     * return whether or not the object may be explicitly tagged. 
     * <p>
     * Note: if the object has been read from an input stream, the only
     * time you can be sure if isExplicit is returning the true state of
     * affairs is if it returns false. An implicitly tagged object may appear
     * to be explicitly tagged, so you need to understand the context under
     * which the reading was done as well, see getObject below.
     */
    public boolean isExplicit()
    {
        // TODO New methods like 'isKnownExplicit' etc. to distinguish uncertain cases?
        switch (explicitness)
        {
        case DECLARED_EXPLICIT:
        case PARSED_EXPLICIT:
            return true;
        default:
            return false;
        }
    }

    boolean isParsed()
    {
        switch (explicitness)
        {
        case PARSED_EXPLICIT:
        case PARSED_IMPLICIT:
            return true;
        default:
            return false;
        }
    }

    /**
     * Needed for open types, until we have better type-guided parsing support. Use sparingly for other
     * purposes, and prefer {@link #getExplicitBaseTagged()}, {@link #getImplicitBaseTagged(int, int)} or
     * {@link #getBaseUniversal(boolean, int)} where possible. Before using, check for matching tag
     * {@link #getTagClass() class} and {@link #getTagNo() number}.
     */
    public ASN1Object getBaseObject()
    {
        return obj instanceof ASN1Object ? (ASN1Object)obj : obj.toASN1Primitive();
    }

    /**
     * Needed for open types, until we have better type-guided parsing support. Use
     * sparingly for other purposes, and prefer {@link #getExplicitBaseTagged()} or
     * {@link #getBaseUniversal(boolean, int)} where possible. Before using, check
     * for matching tag {@link #getTagClass() class} and {@link #getTagNo() number}.
     */
    public ASN1Object getExplicitBaseObject()
    {
        if (!isExplicit())
        {
            throw new IllegalStateException("object implicit - explicit expected.");
        }

        return obj instanceof ASN1Object ? (ASN1Object)obj : obj.toASN1Primitive();
    }

    public ASN1TaggedObject getExplicitBaseTagged()
    {
        if (!isExplicit())
        {
            throw new IllegalStateException("object implicit - explicit expected.");
        }

        return fromExplicit(obj.toASN1Primitive());
    }

    public ASN1TaggedObject getImplicitBaseTagged(int baseTagClass, int baseTagNo)
    {
        if (baseTagClass == BERTags.UNIVERSAL || (baseTagClass & BERTags.PRIVATE) != baseTagClass)
        {
            throw new IllegalArgumentException("invalid base tag class: " + baseTagClass);
        }

        switch (explicitness)
        {
        case DECLARED_EXPLICIT:
            throw new IllegalStateException("object explicit - implicit expected.");

        case DECLARED_IMPLICIT:
        {
            ASN1TaggedObject declared = fromExplicit(obj.toASN1Primitive());
            return ASN1Util.checkTag(declared, baseTagClass, baseTagNo);
        }

        // Parsed; return a virtual tag (i.e. that couldn't have been present in the encoding)
        default:
            return replaceTag(baseTagClass, baseTagNo);
        }
    }

    /**
     * Note: tagged objects are generally context dependent. Before trying to
     * extract a tagged object this way, make sure you have checked that both the
     * {@link #getTagClass() tag class} and {@link #getTagNo() tag number} match
     * what you are looking for.
     * 
     * @param declaredExplicit Whether the tagged type for this object was declared
     *                         EXPLICIT.
     * @param tagNo            The universal {@link BERTags tag number} of the
     *                         expected base object.
     */
    public ASN1Primitive getBaseUniversal(boolean declaredExplicit, int tagNo)
    {
        ASN1UniversalType universalType = ASN1UniversalTypes.get(tagNo);
        if (null == universalType)
        {
            throw new IllegalArgumentException("unsupported UNIVERSAL tag number: " + tagNo);
        }

        return getBaseUniversal(declaredExplicit, universalType);
    }

    ASN1Primitive getBaseUniversal(boolean declaredExplicit, ASN1UniversalType universalType)
    {
        if (declaredExplicit)
        {
            if (!isExplicit())
            {
                throw new IllegalStateException("object implicit - explicit expected.");
            }

            return universalType.fromExplicit(obj.toASN1Primitive());
        }

        if (DECLARED_EXPLICIT == explicitness)
        {
            throw new IllegalStateException("object explicit - implicit expected.");
        }

        ASN1Primitive primitive = obj.toASN1Primitive();
        switch (explicitness)
        {
        case PARSED_EXPLICIT:
            return universalType.fromImplicitConstructed(rebuildConstructed(primitive));
        case PARSED_IMPLICIT:
        {
            if (primitive instanceof ASN1Sequence)
            {
                return universalType.fromImplicitConstructed((ASN1Sequence)primitive);
            }
            return universalType.fromImplicitPrimitive((DEROctetString)primitive);
        }
        default:
            return universalType.fromExplicit(primitive);
        }
    }

    public ASN1Encodable parseBaseUniversal(boolean declaredExplicit, int baseTagNo) throws IOException
    {
        ASN1Primitive primitive = getBaseUniversal(declaredExplicit, baseTagNo);

        switch (baseTagNo)
        {
        case BERTags.BIT_STRING:
            return ((ASN1BitString)primitive).parser();
        case BERTags.OCTET_STRING:
            return ((ASN1OctetString)primitive).parser();
        case BERTags.SEQUENCE:
            return ((ASN1Sequence)primitive).parser();
        case BERTags.SET:
            return ((ASN1Set)primitive).parser();
        }

        return primitive;
    }

    public ASN1Encodable parseExplicitBaseObject() throws IOException
    {
        return getExplicitBaseObject();
    }

    public ASN1TaggedObjectParser parseExplicitBaseTagged() throws IOException
    {
        return getExplicitBaseTagged();
    }

    public ASN1TaggedObjectParser parseImplicitBaseTagged(int baseTagClass, int baseTagNo) throws IOException
    {
        return getImplicitBaseTagged(baseTagClass, baseTagNo);
    }

    public final ASN1Primitive getLoadedObject()
    {
        return this;
    }

    abstract ASN1Sequence rebuildConstructed(ASN1Primitive primitive);

    abstract ASN1TaggedObject replaceTag(int tagClass, int tagNo);

    ASN1Primitive toDERObject()
    {
        return new DERTaggedObject(explicitness, tagClass, tagNo, obj);
    }

    ASN1Primitive toDLObject()
    {
        return new DLTaggedObject(explicitness, tagClass, tagNo, obj);
    }

    public String toString()
    {
        return ASN1Util.getTagText(tagClass, tagNo) + obj;
    }

    static ASN1Primitive createConstructedDL(int tagClass, int tagNo, ASN1EncodableVector contentsElements)
    {
        boolean maybeExplicit = (contentsElements.size() == 1);

        return maybeExplicit
            ?   new DLTaggedObject(PARSED_EXPLICIT, tagClass, tagNo, contentsElements.get(0))
            :   new DLTaggedObject(PARSED_IMPLICIT, tagClass, tagNo, DLFactory.createSequence(contentsElements));
    }

    static ASN1Primitive createConstructedIL(int tagClass, int tagNo, ASN1EncodableVector contentsElements)
    {
        boolean maybeExplicit = (contentsElements.size() == 1);

        return maybeExplicit
            ?   new BERTaggedObject(PARSED_EXPLICIT, tagClass, tagNo, contentsElements.get(0))
            :   new BERTaggedObject(PARSED_IMPLICIT, tagClass, tagNo, BERFactory.createSequence(contentsElements));
    }

    static ASN1Primitive createPrimitive(int tagClass, int tagNo, byte[] contentsOctets)
    {
        // Note: !CONSTRUCTED => IMPLICIT
        return new DLTaggedObject(PARSED_IMPLICIT, tagClass, tagNo, new DEROctetString(contentsOctets));
    }

    private static ASN1TaggedObject checkedCast(ASN1Primitive primitive)
    {
        if (primitive instanceof ASN1TaggedObject)
        {
            return (ASN1TaggedObject)primitive;
        }

        throw new IllegalArgumentException("unexpected object: " + Objects.getClassName(primitive));
    }

    private static ASN1TaggedObject fromExplicit(ASN1Primitive primitive)
    {
        if (primitive instanceof ASN1TaggedObject)
        {
            return (ASN1TaggedObject)primitive;
        }

        throw new IllegalStateException("unexpected explicit encoding");
    }
}
