using System;
using System.Collections;
using System.IO;

using Org.BouncyCastle.Asn1.Utilities;
using Org.BouncyCastle.Utilities.IO;

namespace Org.BouncyCastle.Asn1
{
	/**
	 * a general purpose ASN.1 decoder - note: this class differs from the
	 * others in that it returns null after it has read the last object in
	 * the stream. If an ASN.1 Null is encountered a Der/BER Null object is
	 * returned.
	 */
	public class Asn1InputStream
		: FilterStream
	{
		private class EosAsn1Object
			: Asn1Object
		{
			internal override void Encode(
				DerOutputStream derOut)
			{
				throw new IOException("Eeek!");
			}

			protected override bool Asn1Equals(
				Asn1Object asn1Object)
			{
				return asn1Object is EosAsn1Object;
			}

			protected override int Asn1GetHashCode()
			{
				return 0;
			}
		}

		private static readonly Asn1Object EndOfStream = new EosAsn1Object();

		private readonly int limit;

		public Asn1InputStream(
			Stream inputStream)
			: this(inputStream, int.MaxValue)
		{
		}

		/**
		 * Create an ASN1InputStream where no DER object will be longer than limit.
		 *
		 * @param input stream containing ASN.1 encoded data.
		 * @param limit maximum size of a DER encoded object.
		 */
		public Asn1InputStream(
			Stream	inputStream,
			int		limit)
			: base(inputStream)
		{
			this.limit = limit;
		}

		/**
		 * Create an ASN1InputStream based on the input byte array. The length of DER objects in
		 * the stream is automatically limited to the length of the input array.
		 *
		 * @param input array containing ASN.1 encoded data.
		 */
		public Asn1InputStream(
			byte[] input)
			: this(new MemoryStream(input, false), input.Length)
		{
		}

		/**
		 * build an object given its tag and the number of bytes to construct it from.
		 */
		internal Asn1Object BuildObject(
			int	tag,
			int	tagNo,
			int	length)
		{
			if ((tag & Asn1Tags.Application) != 0)
			{
				return new DerApplicationSpecific(tagNo, ReadDefiniteLengthFully(length));
			}

			bool isConstructed = (tag & Asn1Tags.Constructed) != 0;

			if (isConstructed)
			{
				int baseTagNo = tag & ~Asn1Tags.Constructed;

				// TODO There are other tags that may be constructed (e.g. BitString)
				switch (baseTagNo)
				{
					case Asn1Tags.Sequence:
						return new DerSequence(BuildDerEncodableVector(length));
					case Asn1Tags.Set:
						return new DerSet(BuildDerEncodableVector(length), false);
					case Asn1Tags.OctetString:
						return BuildDerConstructedOctetString(length);
					default:
					{
						//
						// with tagged object tag number is bottom 5 bits
						//
						if ((tag & Asn1Tags.Tagged) != 0)
						{
							if (length == 0)     // empty tag!
							{
								return new DerTaggedObject(false, tagNo, DerSequence.Empty);
							}

							Asn1EncodableVector v = BuildDerEncodableVector(length);

							if (v.Count == 1)
							{
								//
								// explicitly tagged (probably!) - if it isn't we'd have to
								// tell from the context
								//
								return new DerTaggedObject(tagNo, v[0]);
							}

							return new DerTaggedObject(false, tagNo, new DerSequence(v));
						}

						return new DerUnknownTag(tag, ReadDefiniteLengthFully(length));
					}
				}
			}

			byte[] bytes = ReadDefiniteLengthFully(length);

			switch (tag)
			{
				case Asn1Tags.Null:
					return DerNull.Instance;
				case Asn1Tags.Boolean:
					return new DerBoolean(bytes);
				case Asn1Tags.Integer:
					return new DerInteger(bytes);
				case Asn1Tags.Enumerated:
					return new DerEnumerated(bytes);
				case Asn1Tags.ObjectIdentifier:
					return new DerObjectIdentifier(bytes);
				case Asn1Tags.BitString:
				{
					int padBits = bytes[0];
					byte[] data = new byte[bytes.Length - 1];
					Array.Copy(bytes, 1, data, 0, bytes.Length - 1);
					return new DerBitString(data, padBits);
				}
				case Asn1Tags.NumericString:
					return new DerNumericString(bytes);
				case Asn1Tags.Utf8String:
					return new DerUtf8String(bytes);
				case Asn1Tags.PrintableString:
					return new DerPrintableString(bytes);
				case Asn1Tags.IA5String:
					return new DerIA5String(bytes);
				case Asn1Tags.T61String:
					return new DerT61String(bytes);
				case Asn1Tags.VisibleString:
					return new DerVisibleString(bytes);
				case Asn1Tags.GeneralString:
					return new DerGeneralString(bytes);
				case Asn1Tags.UniversalString:
					return new DerUniversalString(bytes);
				case Asn1Tags.BmpString:
					return new DerBmpString(bytes);
				case Asn1Tags.OctetString:
					return new DerOctetString(bytes);
				case Asn1Tags.UtcTime:
					return new DerUtcTime(bytes);
				case Asn1Tags.GeneralizedTime:
					return new DerGeneralizedTime(bytes);
				default:
				{
					//
					// with tagged object tag number is bottom 5 bits
					//
					if ((tag & Asn1Tags.Tagged) != 0)
					{
						Asn1Object obj = (bytes.Length == 0)
							?	(Asn1Object) DerNull.Instance	// empty tag!
							:	new DerOctetString(bytes);		// simple type - implicit

						return new DerTaggedObject(false, tagNo, obj);
					}

					return new DerUnknownTag(tag, bytes);
				}
			}
		}

		private byte[] ReadDefiniteLengthFully(
			int length)
		{
			byte[] bytes = new byte[length];

			if (Streams.ReadFully(this, bytes, 0, length) < length)
				throw new EndOfStreamException("EOF encountered in middle of object");

			return bytes;
		}

		/**
		 * read a string of bytes representing an indefinite length object.
		 */
		private byte[] ReadIndefiniteLengthFully()
		{
			MemoryStream bOut = new MemoryStream();
			int b1 = ReadByte();

			int b;
			while ((b = ReadByte()) >= 0)
			{
				if (b1 == 0 && b == 0) break;

				bOut.WriteByte((byte) b1);
				b1 = b;
			}

			return bOut.ToArray();
		}

		private BerOctetString BuildConstructedOctetString(
			Asn1Object sentinel)
		{
			ArrayList octs = new ArrayList();
			Asn1Object o;

			while ((o = ReadObject()) != sentinel)
			{
				if (o == null)
					throw new EndOfStreamException("attempt to read past end of file.");

				octs.Add(o);
			}

			return new BerOctetString(octs);
		}

		//
		// yes, people actually do this...
		//
		private BerOctetString BuildDerConstructedOctetString(
			int length)
		{
			DefiniteLengthInputStream dIn = new DefiniteLengthInputStream(this, length);
			Asn1InputStream aIn = new Asn1InputStream(dIn, length);

			return aIn.BuildConstructedOctetString(null);
		}

		internal Asn1EncodableVector BuildEncodableVector(
			Asn1Object sentinel)
		{
			Asn1EncodableVector v = new Asn1EncodableVector();
			Asn1Object o;

			while ((o = ReadObject()) != sentinel)
			{
				if (o == null)
					throw new EndOfStreamException("attempt to read past end of file.");

				v.Add(o);
			}

			return v;
		}

		private Asn1EncodableVector BuildDerEncodableVector(
			int length)
		{
			DefiniteLengthInputStream dIn = new DefiniteLengthInputStream(this, length);
			Asn1InputStream aIn = new Asn1InputStream(dIn, length);

			return aIn.BuildEncodableVector(null);
		}

		public Asn1Object ReadObject()
		{
			int tag = ReadByte();
			if (tag == -1)
				return null;

			//
			// calculate tag number
			//
			int tagNo = 0;
			if ((tag & Asn1Tags.Tagged) != 0 || (tag & Asn1Tags.Application) != 0)
			{
				tagNo = ReadTagNumber(this, tag);
			}

			//
			// calculate length
			//
			int length = ReadLength(this, limit);

			if (length < 0) // indefinite length method
			{
				bool isConstructed = (tag & Asn1Tags.Constructed) != 0;

				if (isConstructed)
				{
					int baseTagNo = tag & ~Asn1Tags.Constructed;

					switch (baseTagNo)
					{
						case Asn1Tags.Sequence:
							return new BerSequence(BuildEncodableVector(EndOfStream));
						case Asn1Tags.Set:
							return new BerSet(BuildEncodableVector(EndOfStream), false);
						case Asn1Tags.OctetString:
							return BuildConstructedOctetString(EndOfStream);
						default:
						{
							//
							// with tagged object tag number is bottom 5 bits
							//
							if ((tag & Asn1Tags.Tagged) != 0)
							{
								//
								// either constructed or explicitly tagged
								//
								Asn1EncodableVector v = BuildEncodableVector(EndOfStream);

								if (v.Count == 0)     // empty tag!
								{
									// TODO Should be BerTaggedObject?
									return new DerTaggedObject(tagNo);
								}

								if (v.Count == 1)
								{
									//
									// explicitly tagged (probably!) - if it isn't we'd have to
									// tell from the context
									//
									return new BerTaggedObject(tagNo, v[0]);
								}

								return new BerTaggedObject(false, tagNo, new BerSequence(v));
							}

							throw new IOException("Unknown BER object encountered");
						}
					}
				}

				// TODO Do we really want to accept primitive encodings with indefinite length?

				switch (tag)
				{
					case Asn1Tags.Null:
						ReadIndefiniteLengthFully();
						return BerNull.Instance;
					default:
					{
						//
						// with tagged object tag number is bottom 5 bits
						//
						if ((tag & Asn1Tags.Tagged) != 0)
						{
							//
							// simple type - implicit... return an octet string
							//
							byte[] bytes = ReadIndefiniteLengthFully();

							return new BerTaggedObject(false, tagNo, new DerOctetString(bytes));
						}

						throw new IOException("Unknown BER object encountered");
					}
				}
			}
			else
			{
				if (tag == 0 && length == 0)    // end of contents marker.
				{
					return EndOfStream;
				}

				return BuildObject(tag, tagNo, length);
			}
		}

		internal static int ReadTagNumber(
			Stream	s,
			int		tag)
		{
			int tagNo = tag & 0x1f;

			//
			// with tagged object tag number is bottom 5 bits, or stored at the start of the content
			//
			if (tagNo == 0x1f)
			{
				tagNo = 0;

				int b = s.ReadByte();

				while ((b >= 0) && ((b & 0x80) != 0))
				{
					tagNo |= (b & 0x7f);
					tagNo <<= 7;
					b = s.ReadByte();
				}

				if (b < 0)
					throw new EndOfStreamException("EOF found inside tag value.");

				tagNo |= (b & 0x7f);
			}

			return tagNo;
		}

		internal static int ReadLength(
			Stream	s,
			int		limit)
		{
			int length = s.ReadByte();
			if (length < 0)
				throw new EndOfStreamException("EOF found when length expected");

			if (length == 0x80)
				return -1;      // indefinite-length encoding

			if (length > 127)
			{
				int size = length & 0x7f;

				if (size > 4)
					throw new IOException("DER length more than 4 bytes");

				length = 0;
				for (int i = 0; i < size; i++)
				{
					int next = s.ReadByte();

					if (next < 0)
						throw new EndOfStreamException("EOF found reading length");

					length = (length << 8) + next;
				}

				if (length < 0)
					throw new IOException("Corrupted stream - negative length found");

				if (length >= limit)   // after all we must have read at least 1 byte
					throw new IOException("Corrupted stream - out of bounds length found");
			}

			return length;
		}
	}
}
