using System;
using System.Collections;

using Org.BouncyCastle.Utilities;

namespace Org.BouncyCastle.Asn1.Cms
{
    /**
     * a signed data object.
     */
    public class SignedData
        : Asn1Encodable
    {
        private static readonly DerInteger Version1 = new DerInteger(1);
        private static readonly DerInteger Version3 = new DerInteger(3);
        private static readonly DerInteger Version4 = new DerInteger(4);
        private static readonly DerInteger Version5 = new DerInteger(5);

        private readonly DerInteger		version;
        private readonly Asn1Set		digestAlgorithms;
        private readonly ContentInfo	contentInfo;
        private readonly Asn1Set		certificates;
        private readonly Asn1Set		crls;
        private readonly Asn1Set		signerInfos;
        private readonly bool			certsBer;
        private readonly bool		    crlsBer;

        public static SignedData GetInstance(object obj)
        {
            if (obj is SignedData)
                return (SignedData)obj;
            if (obj == null)
                return null;
            return new SignedData(Asn1Sequence.GetInstance(obj));
        }

        public SignedData(
            Asn1Set     digestAlgorithms,
            ContentInfo contentInfo,
            Asn1Set     certificates,
            Asn1Set     crls,
            Asn1Set     signerInfos)
        {
            this.version = CalculateVersion(contentInfo.ContentType, certificates, crls, signerInfos);
            this.digestAlgorithms = digestAlgorithms;
            this.contentInfo = contentInfo;
            this.certificates = certificates;
            this.crls = crls;
            this.signerInfos = signerInfos;
            this.crlsBer = crls is BerSet;
            this.certsBer = certificates is BerSet;
        }

        // RFC3852, section 5.1:
        // IF ((certificates is present) AND
        //    (any certificates with a type of other are present)) OR
        //    ((crls is present) AND
        //    (any crls with a type of other are present))
        // THEN version MUST be 5
        // ELSE
        //    IF (certificates is present) AND
        //       (any version 2 attribute certificates are present)
        //    THEN version MUST be 4
        //    ELSE
        //       IF ((certificates is present) AND
        //          (any version 1 attribute certificates are present)) OR
        //          (any SignerInfo structures are version 3) OR
        //          (encapContentInfo eContentType is other than id-data)
        //       THEN version MUST be 3
        //       ELSE version MUST be 1
        //
        private DerInteger CalculateVersion(
            DerObjectIdentifier	contentOid,
            Asn1Set				certs,
            Asn1Set				crls,
            Asn1Set				signerInfs)
        {
            bool otherCert = false;
            bool otherCrl = false;
            bool attrCertV1Found = false;
            bool attrCertV2Found = false;

            if (certs != null)
            {
                foreach (object obj in certs)
                {
                    if (obj is Asn1TaggedObject)
                    {
                        Asn1TaggedObject tagged = (Asn1TaggedObject)obj;

                        if (tagged.TagNo == 1)
                        {
                            attrCertV1Found = true;
                        }
                        else if (tagged.TagNo == 2)
                        {
                            attrCertV2Found = true;
                        }
                        else if (tagged.TagNo == 3)
                        {
                            otherCert = true;
                            break;
                        }
                    }
                }
            }

            if (otherCert)
            {
                return Version5;
            }

            if (crls != null)
            {
                foreach (object obj in crls)
                {
                    if (obj is Asn1TaggedObject)
                    {
                        otherCrl = true;
                        break;
                    }
                }
            }

            if (otherCrl)
            {
                return Version5;
            }

            if (attrCertV2Found)
            {
                return Version4;
            }

            if (attrCertV1Found || !CmsObjectIdentifiers.Data.Equals(contentOid) || CheckForVersion3(signerInfs))
            {
                return Version3;
            }

            return Version1;
        }

        private bool CheckForVersion3(
            Asn1Set signerInfs)
        {
            foreach (object obj in signerInfs)
            {
                SignerInfo s = SignerInfo.GetInstance(obj);

                if (s.Version.HasValue(3))
                {
                    return true;
                }
            }

            return false;
        }

        private SignedData(
            Asn1Sequence seq)
        {
            IEnumerator e = seq.GetEnumerator();

            e.MoveNext();
            version = (DerInteger)e.Current;

            e.MoveNext();
            digestAlgorithms = ((Asn1Set)e.Current);

            e.MoveNext();
            contentInfo = ContentInfo.GetInstance(e.Current);

            while (e.MoveNext())
            {
                Asn1Object o = (Asn1Object)e.Current;

                //
                // an interesting feature of SignedData is that there appear
                // to be varying implementations...
                // for the moment we ignore anything which doesn't fit.
                //
                if (o is Asn1TaggedObject)
                {
                    Asn1TaggedObject tagged = (Asn1TaggedObject)o;

                    switch (tagged.TagNo)
                    {
                        case 0:
                            certsBer = tagged is BerTaggedObject;
                            certificates = Asn1Set.GetInstance(tagged, false);
                            break;
                        case 1:
                            crlsBer = tagged is BerTaggedObject;
                            crls = Asn1Set.GetInstance(tagged, false);
                            break;
                        default:
                            throw new ArgumentException("unknown tag value " + tagged.TagNo);
                    }
                }
                else
                {
                    signerInfos = (Asn1Set) o;
                }
            }
        }

        public DerInteger Version
        {
            get { return version; }
        }

        public Asn1Set DigestAlgorithms
        {
            get { return digestAlgorithms; }
        }

        public ContentInfo EncapContentInfo
        {
            get { return contentInfo; }
        }

        public Asn1Set Certificates
        {
            get { return certificates; }
        }

        public Asn1Set CRLs
        {
            get { return crls; }
        }

        public Asn1Set SignerInfos
        {
            get { return signerInfos; }
        }

        /**
         * Produce an object suitable for an Asn1OutputStream.
         * <pre>
         * SignedData ::= Sequence {
         *     version CMSVersion,
         *     digestAlgorithms DigestAlgorithmIdentifiers,
         *     encapContentInfo EncapsulatedContentInfo,
         *     certificates [0] IMPLICIT CertificateSet OPTIONAL,
         *     crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
         *     signerInfos SignerInfos
         *   }
         * </pre>
         */
        public override Asn1Object ToAsn1Object()
        {
            Asn1EncodableVector v = new Asn1EncodableVector(
                version, digestAlgorithms, contentInfo);

            if (certificates != null)
            {
                if (certsBer)
                {
                    v.Add(new BerTaggedObject(false, 0, certificates));
                }
                else
                {
                    v.Add(new DerTaggedObject(false, 0, certificates));
                }
            }

            if (crls != null)
            {
                if (crlsBer)
                {
                    v.Add(new BerTaggedObject(false, 1, crls));
                }
                else
                {
                    v.Add(new DerTaggedObject(false, 1, crls));
                }
            }

            v.Add(signerInfos);

            return new BerSequence(v);
        }
    }
}
