using System;
using System.IO;

using Nequeo.Cryptography.Key.Utilities;

namespace Nequeo.Cryptography.Key.Asn1
{
    /**
     * Base class for an application specific object
     */
    public class DerApplicationSpecific
        : Asn1Object
    {
		private readonly bool	isConstructed;
        private readonly int	tag;
        private readonly byte[]	octets;

		internal DerApplicationSpecific(
			bool	isConstructed,
			int		tag,
			byte[]	octets)
		{
			this.isConstructed = isConstructed;
			this.tag = tag;
			this.octets = octets;
		}

		public DerApplicationSpecific(
            int		tag,
            byte[]	octets)
			: this(false, tag, octets)
        {
        }

		public DerApplicationSpecific(
			int				tag, 
			Asn1Encodable	obj) 
			: this(true, tag, obj)
		{
		}

		public DerApplicationSpecific(
			bool			isExplicit,
			int				tag,
			Asn1Encodable	obj)
		{
			byte[] data = obj.GetDerEncoded();

			this.isConstructed = isExplicit;
			this.tag = tag;

			if (isExplicit)
			{
				this.octets = data;
			}
			else
			{
				int lenBytes = GetLengthOfLength(data);
				byte[] tmp = new byte[data.Length - lenBytes];
				Array.Copy(data, lenBytes, tmp, 0, tmp.Length);
				this.octets = tmp;
			}
		}

		public DerApplicationSpecific(
			int					tagNo,
			Asn1EncodableVector	vec)
		{
			this.tag = tagNo;
			this.isConstructed = true;
			MemoryStream bOut = new MemoryStream();

			for (int i = 0; i != vec.Count; i++)
			{
				try
				{
					byte[] bs = vec[i].GetEncoded();
					bOut.Write(bs, 0, bs.Length);
				}
				catch (IOException e)
				{
					throw new InvalidOperationException("malformed object", e);
				}
			}
			this.octets = bOut.ToArray();
		}

		private int GetLengthOfLength(
			byte[] data)
		{
			int count = 2;	// TODO: assumes only a 1 byte tag number

			while((data[count - 1] & 0x80) != 0)
			{
				count++;
			}

			return count;
		}

		public bool IsConstructed()
        {
			return isConstructed;
        }

		public byte[] GetContents()
        {
            return octets;
        }

		public int ApplicationTag
        {
            get { return tag; }
        }

		/**
		 * Return the enclosed object assuming explicit tagging.
		 *
		 * @return  the resulting object
		 * @throws IOException if reconstruction fails.
		 */
		public Asn1Object GetObject()
        {
			return FromByteArray(GetContents());
		}

		/**
		 * Return the enclosed object assuming implicit tagging.
		 *
		 * @param derTagNo the type tag that should be applied to the object's contents.
		 * @return  the resulting object
		 * @throws IOException if reconstruction fails.
		 */
		public Asn1Object GetObject(
			int derTagNo)
		{
			if (derTagNo >= 0x1f)
				throw new IOException("unsupported tag number");

			byte[] orig = this.GetEncoded();
			byte[] tmp = ReplaceTagNumber(derTagNo, orig);

			if ((orig[0] & Asn1Tags.Constructed) != 0)
			{
				tmp[0] |= Asn1Tags.Constructed;
			}

			return FromByteArray(tmp);;
		}

		internal override void Encode(
			DerOutputStream derOut)
        {
			int classBits = Asn1Tags.Application;
			if (isConstructed)
			{
				classBits |= Asn1Tags.Constructed; 
			}

			derOut.WriteEncoded(classBits, tag, octets);
		}

		protected override bool Asn1Equals(
			Asn1Object asn1Object)
        {
			DerApplicationSpecific other = asn1Object as DerApplicationSpecific;

			if (other == null)
				return false;

			return this.isConstructed == other.isConstructed
				&& this.tag == other.tag
				&& Arrays.AreEqual(this.octets, other.octets);
        }

		protected override int Asn1GetHashCode()
		{
			return isConstructed.GetHashCode() ^ tag.GetHashCode() ^ Arrays.GetHashCode(octets);
        }

		private byte[] ReplaceTagNumber(
			int		newTag,
			byte[]	input)
		{
			int tagNo = input[0] & 0x1f;
			int index = 1;
			//
			// with tagged object tag number is bottom 5 bits, or stored at the start of the content
			//
			if (tagNo == 0x1f)
			{
				tagNo = 0;

				int b = input[index++] & 0xff;

				// X.690-0207 8.1.2.4.2
				// "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
				if ((b & 0x7f) == 0) // Note: -1 will pass
				{
					throw new InvalidOperationException("corrupted stream - invalid high tag number found");
				}

				while ((b >= 0) && ((b & 0x80) != 0))
				{
					tagNo |= (b & 0x7f);
					tagNo <<= 7;
					b = input[index++] & 0xff;
				}

				tagNo |= (b & 0x7f);
			}

			byte[] tmp = new byte[input.Length - index + 1];

			Array.Copy(input, index, tmp, 1, tmp.Length - 1);

			tmp[0] = (byte)newTag;

			return tmp;
		}
    }
}
