using System;
using System.IO;

namespace Org.BouncyCastle.Asn1
{
	public class Asn1StreamParser
	{
		private readonly Stream _in;
		private readonly int _limit;

		public Asn1StreamParser(
			Stream inStream)
			: this(inStream, int.MaxValue)
		{
		}

		public Asn1StreamParser(
			Stream	inStream,
			int		limit)
		{
			if (!inStream.CanRead)
				throw new ArgumentException("Expected stream to be readable", "inStream");

			this._in = inStream;
			this._limit = limit;
		}

		public Asn1StreamParser(
			byte[] encoding)
			: this(new MemoryStream(encoding, false), encoding.Length)
		{
		}

		public IAsn1Convertible ReadObject()
		{
			int tag = _in.ReadByte();
			if (tag == -1)
				return null;

			// turn of looking for "00" while we resolve the tag
			Set00Check(false);

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

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

			//
			// calculate length
			//
			int length = Asn1InputStream.ReadLength(_in, _limit);

			if (length < 0) // indefinite length method
			{
				// TODO Verify that the tag is constructed?

				IndefiniteLengthInputStream indIn = new IndefiniteLengthInputStream(_in);

				if ((tag & Asn1Tags.Tagged) != 0)
				{
					return new BerTaggedObjectParser(tag, tagNo, indIn);
				}

				switch (baseTagNo)
				{
						// NULL should always be primitive (therefore definite length encoded)
					case Asn1Tags.Null:
						while (indIn.ReadByte() >= 0)
						{
							// make sure we skip to end of object
						}
						return BerNull.Instance;
					case Asn1Tags.OctetString:
						return new BerOctetStringParser(new Asn1StreamParser(indIn));
					case Asn1Tags.Sequence:
						return new BerSequenceParser(new Asn1StreamParser(indIn));
					case Asn1Tags.Set:
						return new BerSetParser(new Asn1StreamParser(indIn));
					default:
						throw new IOException("unknown BER object encountered");
				}
			}
			else
			{
				DefiniteLengthInputStream defIn = new DefiniteLengthInputStream(_in, length);

				if ((tag & Asn1Tags.Application) != 0)
				{
					return new DerApplicationSpecific(tagNo, defIn.ToArray());
				}

				if ((tag & Asn1Tags.Tagged) != 0)
				{
					return new BerTaggedObjectParser(tag, tagNo, defIn);
				}

				// TODO This code should be more aware of constructed vs. primitive encodings

				switch (baseTagNo)
				{
					case Asn1Tags.BitString:
					{
						byte[] bytes = defIn.ToArray();
						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.BmpString:
						return new DerBmpString(defIn.ToArray());
					case Asn1Tags.Boolean:
						return new DerBoolean(defIn.ToArray());
					case Asn1Tags.Enumerated:
						return new DerEnumerated(defIn.ToArray());
					case Asn1Tags.GeneralizedTime:
						return new DerGeneralizedTime(defIn.ToArray());
					case Asn1Tags.GeneralString:
						return new DerGeneralString(defIn.ToArray());
					case Asn1Tags.IA5String:
						return new DerIA5String(defIn.ToArray());
					case Asn1Tags.Integer:
						return new DerInteger(defIn.ToArray());
					case Asn1Tags.Null:
						defIn.ToArray(); // make sure we read to end of object bytes.
						return DerNull.Instance;
					case Asn1Tags.NumericString:
						return new DerNumericString(defIn.ToArray());
					case Asn1Tags.ObjectIdentifier:
						return new DerObjectIdentifier(defIn.ToArray());
					case Asn1Tags.OctetString:
						// TODO Is the handling of definite length constructed encodings correct?
						if (isConstructed)
						{
							return new BerOctetStringParser(new Asn1StreamParser(defIn));
						}
						else
						{
							return new DerOctetStringParser(defIn);
						}
					case Asn1Tags.PrintableString:
						return new DerPrintableString(defIn.ToArray());
					case Asn1Tags.Sequence:
						return new DerSequenceParser(new Asn1StreamParser(defIn));
					case Asn1Tags.Set:
						return new DerSetParser(new Asn1StreamParser(defIn));
					case Asn1Tags.T61String:
						return new DerT61String(defIn.ToArray());
					case Asn1Tags.UniversalString:
						return new DerUniversalString(defIn.ToArray());
					case Asn1Tags.UtcTime:
						return new DerUtcTime(defIn.ToArray());
					case Asn1Tags.Utf8String:
						return new DerUtf8String(defIn.ToArray());
					case Asn1Tags.VisibleString:
						return new DerVisibleString(defIn.ToArray());
					default:
						return new DerUnknownTag(tag, defIn.ToArray());
				}
			}
		}

		private void Set00Check(
			bool enabled)
		{
			if (_in is IndefiniteLengthInputStream)
			{
				((IndefiniteLengthInputStream) _in).SetEofOn00(enabled);
			}
		}

		internal Asn1EncodableVector ReadVector()
		{
			Asn1EncodableVector v = new Asn1EncodableVector();

			IAsn1Convertible obj;
			while ((obj = ReadObject()) != null)
			{
				v.Add(obj.ToAsn1Object());
			}

			return v;
		}
	}
}
