using System;
using System.Collections.Generic;
using System.Linq;
namespace Renci.SshNet.Common
{
    /// 
    /// Base class for DER encoded data.
    /// 
    public class DerData
    {
        private const byte CONSTRUCTED = 0x20;
        private const byte BOOLEAN = 0x01;
        private const byte INTEGER = 0x02;
        //private const byte BITSTRING = 0x03;
        private const byte OCTETSTRING = 0x04;
        private const byte NULL = 0x05;
        private const byte OBJECTIDENTIFIER = 0x06;
        //private const byte EXTERNAL = 0x08;
        //private const byte ENUMERATED = 0x0a;
        private const byte SEQUENCE = 0x10;
        //private const byte SEQUENCEOF = 0x10; // for completeness
        //private const byte SET = 0x11;
        //private const byte SETOF = 0x11; // for completeness
        //private const byte NUMERICSTRING = 0x12;
        //private const byte PRINTABLESTRING = 0x13;
        //private const byte T61STRING = 0x14;
        //private const byte VIDEOTEXSTRING = 0x15;
        //private const byte IA5STRING = 0x16;
        //private const byte UTCTIME = 0x17;
        //private const byte GENERALIZEDTIME = 0x18;
        //private const byte GRAPHICSTRING = 0x19;
        //private const byte VISIBLESTRING = 0x1a;
        //private const byte GENERALSTRING = 0x1b;
        //private const byte UNIVERSALSTRING = 0x1c;
        //private const byte BMPSTRING = 0x1e;
        //private const byte UTF8STRING = 0x0c;
        //private const byte APPLICATION = 0x40;
        //private const byte TAGGED = 0x80;
        private readonly List _data;
        private int _readerIndex;
        private readonly int _lastIndex;
        /// 
        /// Gets a value indicating whether end of data is reached.
        /// 
        /// 
        /// 	true if end of data is reached; otherwise, false.
        /// 
        public bool IsEndOfData
        {
            get
            {
                return this._readerIndex >= this._lastIndex;
            }
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        public DerData()
        {
            this._data = new List();
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// DER encoded data.
        public DerData(byte[] data)
        {
            this._data = new List(data);
            var dataType = this.ReadByte();
            var length = this.ReadLength();
            this._lastIndex = this._readerIndex + length;
        }
        /// 
        /// Encodes written data as DER byte array.
        /// 
        /// DER Encoded array.
        public byte[] Encode()
        {
            var length = this._data.Count();
            var lengthBytes = this.GetLength(length);
            this._data.InsertRange(0, lengthBytes);
            this._data.Insert(0, CONSTRUCTED | SEQUENCE);
            return this._data.ToArray();
        }
        /// 
        /// Reads next mpint data type from internal buffer.
        /// 
        /// mpint read.
        public BigInteger ReadBigInteger()
        {
            var type = this.ReadByte();
            if (type != INTEGER)
                throw new InvalidOperationException("Invalid data type, INTEGER(02) is expected.");
            var length = this.ReadLength();
            var data = this.ReadBytes(length);
            return new BigInteger(data.Reverse().ToArray());
        }
        /// 
        /// Reads next int data type from internal buffer.
        /// 
        /// int read.
        public int ReadInteger()
        {
            var type = this.ReadByte();
            if (type != INTEGER)
                throw new InvalidOperationException("Invalid data type, INTEGER(02) is expected.");
            var length = this.ReadLength();
            var data = this.ReadBytes(length);
            if (length > 4)
                throw new InvalidOperationException("Integer type cannot occupy more then 4 bytes");
            var result = 0;
            var shift = (length - 1) * 8;
            for (int i = 0; i < length; i++)
            {
                result |= data[i] << shift;
                shift -= 8;
            }
            //return (int)(data[0] << 56 | data[1] << 48 | data[2] << 40 | data[3] << 32 | data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]);
            return result;
        }
        /// 
        /// Writes BOOLEAN data into internal buffer.
        /// 
        /// UInt32 data to write.
        public void Write(bool data)
        {
            this._data.Add(BOOLEAN);
            this._data.Add(1);
            this._data.Add((byte)(data ? 1 : 0));
        }
        /// 
        /// Writes UInt32 data into internal buffer.
        /// 
        /// UInt32 data to write.
        public void Write(UInt32 data)
        {
            var bytes = data.GetBytes();
            this._data.Add(INTEGER);
            var length = this.GetLength(bytes.Length);
            this.WriteBytes(length);
            this.WriteBytes(bytes);
        }
        /// 
        /// Writes INTEGER data into internal buffer.
        /// 
        /// BigInteger data to write.
        public void Write(BigInteger data)
        {
            var bytes = data.ToByteArray().Reverse().ToList();
            this._data.Add(INTEGER);
            var length = this.GetLength(bytes.Count);
            this.WriteBytes(length);
            this.WriteBytes(bytes);
        }
        /// 
        /// Writes OCTETSTRING data into internal buffer.
        /// 
        /// The data.
        public void Write(byte[] data)
        {
            this._data.Add(OCTETSTRING);
            var length = this.GetLength(data.Length);
            this.WriteBytes(length);
            this.WriteBytes(data);
        }
        /// 
        /// Writes OBJECTIDENTIFIER data into internal buffer.
        /// 
        /// The identifier.
        public void Write(ObjectIdentifier identifier)
        {
            var temp = new ulong[identifier.Identifiers.Length - 1];
            temp[0] = identifier.Identifiers[0] * 40 + identifier.Identifiers[1];
            Buffer.BlockCopy(identifier.Identifiers, 2 * sizeof(ulong), temp, 1 * sizeof(ulong), (identifier.Identifiers.Length - 2) * sizeof(ulong));
            //Array.Copy(identifier.Identifiers, 2, temp, 1, identifier.Identifiers.Length - 2);
            var bytes = new List();
            foreach (var subidentifier in temp)
            {
                var item = subidentifier;
                var buffer = new byte[8];
                var bufferIndex = buffer.Length - 1;
                var current = (byte)(item & 0x7F);
                do
                {
                    buffer[bufferIndex] = current;
                    if (bufferIndex < buffer.Length - 1)
                        buffer[bufferIndex] |= (byte)0x80;
                    item >>= 7;
                    current = (byte)(item & 0x7F);
                    bufferIndex--;
                }
                while (current > 0);
                for (int i = bufferIndex + 1; i < buffer.Length; i++)
                {
                    bytes.Add(buffer[i]);
                }
            }
            this._data.Add(OBJECTIDENTIFIER);
            var length = this.GetLength(bytes.Count);
            this.WriteBytes(length);
            this.WriteBytes(bytes);
        }
        /// 
        /// Writes NULL data into internal buffer.
        /// 
        public void WriteNull()
        {
            this._data.Add(NULL);
            this._data.Add(0);
        }
        /// 
        /// Writes DerData data into internal buffer.
        /// 
        /// DerData data to write.
        public void Write(DerData data)
        {
            var bytes = data.Encode();
            this._data.AddRange(bytes);
        }
        private byte[] GetLength(int length)
        {
            if (length > 127)
            {
                int size = 1;
                int val = length;
                while ((val >>= 8) != 0)
                    size++;
                var data = new byte[size];
                data[0] = (byte)(size | 0x80);
                for (int i = (size - 1) * 8, j = 1; i >= 0; i -= 8, j++)
                {
                    data[j] = (byte)(length >> i);
                }
                return data;
            }
            return new byte[] { (byte)length };
        }
        private int ReadLength()
        {
            int length = this.ReadByte();
            if (length == 0x80)
            {
                throw new NotSupportedException("Indefinite-length encoding is not supported.");
            }
            if (length > 127)
            {
                int size = length & 0x7f;
                // Note: The invalid long form "0xff" (see X.690 8.1.3.5c) will be caught here
                if (size > 4)
                    throw new InvalidOperationException(string.Format("DER length is '{0}' and cannot be more than 4 bytes.", size));
                length = 0;
                for (int i = 0; i < size; i++)
                {
                    int next = this.ReadByte();
                    length = (length << 8) + next;
                }
                if (length < 0)
                    throw new InvalidOperationException("Corrupted data - 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;
        }
        private void WriteBytes(IEnumerable data)
        {
            this._data.AddRange(data);
        }
        private byte ReadByte()
        {
            if (this._readerIndex > this._data.Count)
                throw new InvalidOperationException("Read out of boundaries.");
            return this._data[this._readerIndex++];
        }
        private byte[] ReadBytes(int length)
        {
            if (this._readerIndex + length > this._data.Count)
                throw new InvalidOperationException("Read out of boundaries.");
            var result = new byte[length];
            this._data.CopyTo(this._readerIndex, result, 0, length);
            this._readerIndex += length;
            return result;
        }
    }
}