using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
namespace Renci.SshNet.Common
{
    /// 
    /// Base ssh data serialization type
    /// 
    public abstract class SshData
    {
        private static Encoding _ascii = new ASCIIEncoding();
#if SILVERLIGHT
        private static Encoding _utf8 = Encoding.UTF8;
#else
        private static Encoding _utf8 = Encoding.Default;
#endif
        /// 
        /// Data byte array that hold message unencrypted data
        /// 
        private List _data;
        private int _readerIndex;
        /// 
        /// Gets a value indicating whether all data from the buffer has been read.
        /// 
        /// 
        /// 	true if this instance is end of data; otherwise, false.
        /// 
        public bool IsEndOfData
        {
            get
            {
                return this._readerIndex >= this._data.Count();
            }
        }
        private byte[] _loadedData;
        /// 
        /// Gets the index that represents zero in current data type.
        /// 
        /// 
        /// The index of the zero reader.
        /// 
        protected virtual int ZeroReaderIndex
        {
            get
            {
                return 0;
            }
        }
        /// 
        /// Gets data bytes array
        /// 
        /// 
        public virtual byte[] GetBytes()
        {
            this._data = new List();
            this.SaveData();
            return this._data.ToArray();
        }
        internal T OfType() where T : SshData, new()
        {
            var result = new T();
            result.LoadBytes(this._loadedData);
            result.LoadData();
            return result;
        }
        /// 
        /// Loads data from specified bytes.
        /// 
        /// Bytes array.
        ///  is null.
        public void Load(byte[] value)
        {
            if (value == null)
                throw new ArgumentNullException("value");
            this.LoadBytes(value);
            this.LoadData();
        }
        /// 
        /// Called when type specific data need to be loaded.
        /// 
        protected abstract void LoadData();
        /// 
        /// Called when type specific data need to be saved.
        /// 
        protected abstract void SaveData();
        /// 
        /// Loads data bytes into internal buffer.
        /// 
        /// The bytes.
        ///  is null.
        protected void LoadBytes(byte[] bytes)
        {
            // Note about why I check for null here, and in Load(byte[]) in this class.
            // This method is called by several other classes, such as SshNet.Messages.Message, SshNet.Sftp.SftpMessage.
            if (bytes == null)
                throw new ArgumentNullException("bytes");
            this.ResetReader();
            this._loadedData = bytes;
            this._data = new List(bytes);
        }
        /// 
        /// Resets internal data reader index.
        /// 
        protected void ResetReader()
        {
            this._readerIndex = this.ZeroReaderIndex;  //  Set to 1 to skip first byte which specifies message type
        }
        /// 
        /// Reads all data left in internal buffer at current position.
        /// 
        /// An array of bytes containing the remaining data in the internal buffer.
        protected byte[] ReadBytes()
        {
            var data = new byte[this._data.Count - this._readerIndex];
            this._data.CopyTo(this._readerIndex, data, 0, data.Length);
            return data;
        }
        /// 
        /// Reads next specified number of bytes data type from internal buffer.
        /// 
        /// Number of bytes to read.
        /// An array of bytes that was read from the internal buffer.
        ///  is greater than the internal buffer size.
        protected byte[] ReadBytes(int length)
        {
            // Note that this also prevents allocating non-relevant lengths, such as if length is greater than _data.Count but less than int.MaxValue.
            // For the nerds, the condition translates to: if (length > data.Count && length < int.MaxValue)
            // Which probably would cause all sorts of exception, most notably OutOfMemoryException.
            if (length > this._data.Count)
                throw new ArgumentOutOfRangeException("length");
            var result = new byte[length];
            this._data.CopyTo(this._readerIndex, result, 0, length);
            this._readerIndex += length;
            return result;
        }
        /// 
        /// Reads next byte data type from internal buffer.
        /// 
        /// Byte read.
        protected byte ReadByte()
        {
            return this.ReadBytes(1).FirstOrDefault();
        }
        /// 
        /// Reads next boolean data type from internal buffer.
        /// 
        /// Boolean read.
        protected bool ReadBoolean()
        {
            return this.ReadByte() == 0 ? false : true;
        }
        /// 
        /// Reads next uint16 data type from internal buffer.
        /// 
        /// uint16 read
        protected UInt16 ReadUInt16()
        {
            var data = this.ReadBytes(2);
            return (ushort)(data[0] << 8 | data[1]);
        }
        /// 
        /// Reads next uint32 data type from internal buffer.
        /// 
        /// uint32 read
        protected UInt32 ReadUInt32()
        {
            var data = this.ReadBytes(4);
            return (uint)(data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]);
        }
        /// 
        /// Reads next uint64 data type from internal buffer.
        /// 
        /// uint64 read
        protected UInt64 ReadUInt64()
        {
            var data = this.ReadBytes(8);
            return (uint)(data[0] << 56 | data[1] << 48 | data[2] << 40 | data[3] << 32 | data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]);
        }
        /// 
        /// Reads next int64 data type from internal buffer.
        /// 
        /// int64 read
        protected Int64 ReadInt64()
        {
            var data = this.ReadBytes(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]);
        }
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected string ReadAsciiString()
        {
            var length = this.ReadUInt32();
            if (length > (uint)int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return _ascii.GetString(this.ReadBytes((int)length), 0, (int)length);
        }
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected string ReadString()
        {
            var length = this.ReadUInt32();
            if (length > (uint)int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return _utf8.GetString(this.ReadBytes((int)length), 0, (int)length);
        }
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected byte[] ReadBinaryString()
        {
            var length = this.ReadUInt32();
            if (length > (uint)int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return this.ReadBytes((int)length);
        }
        /// 
        /// Reads next mpint data type from internal buffer.
        /// 
        /// mpint read.
        protected BigInteger ReadBigInt()
        {
            var length = this.ReadUInt32();
            var data = this.ReadBytes((int)length);
            return new BigInteger(data.Reverse().ToArray());
        }
        /// 
        /// Reads next name-list data type from internal buffer.
        /// 
        /// 
        protected string[] ReadNamesList()
        {
            var namesList = this.ReadString();
            return namesList.Split(',');
        }
        /// 
        /// Reads next extension-pair data type from internal buffer.
        /// 
        /// 
        protected IDictionary ReadExtensionPair()
        {
            Dictionary result = new Dictionary();
            while (this._readerIndex < this._data.Count)
            {
                var extensionName = this.ReadString();
                var extensionData = this.ReadString();
                result.Add(extensionName, extensionData);
            }
            return result;
        }
        /// 
        /// Writes bytes array data into internal buffer.
        /// 
        /// Byte array data to write.
        ///  is null.
        protected void Write(IEnumerable data)
        {
            this._data.AddRange(data);
        }
        /// 
        /// Writes byte data into internal buffer.
        /// 
        /// Byte data to write.
        protected void Write(byte data)
        {
            this._data.Add(data);
        }
        /// 
        /// Writes boolean data into internal buffer.
        /// 
        /// Boolean data to write.
        protected void Write(bool data)
        {
            if (data)
            {
                this.Write(1);
            }
            else
            {
                this.Write(0);
            }
        }
        /// 
        /// Writes uint16 data into internal buffer.
        /// 
        /// uint16 data to write.
        protected void Write(UInt16 data)
        {
            this.Write(data.GetBytes());
        }
        /// 
        /// Writes uint32 data into internal buffer.
        /// 
        /// uint32 data to write.
        protected void Write(UInt32 data)
        {
            this.Write(data.GetBytes());
        }
        /// 
        /// Writes uint64 data into internal buffer.
        /// 
        /// uint64 data to write.
        protected void Write(UInt64 data)
        {
            this.Write(data.GetBytes());
        }
        /// 
        /// Writes int64 data into internal buffer.
        /// 
        /// int64 data to write.
        protected void Write(Int64 data)
        {
            this.Write(data.GetBytes());
        }
        /// 
        /// Writes string data into internal buffer using default encoding.
        /// 
        /// string data to write.
        ///  is null.
        protected void Write(string data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            var bytes = _utf8.GetBytes(data);
            this.Write((uint)bytes.Length);
            this.Write(bytes);
        }
        /// 
        /// Writes string data into internal buffer as ASCII.
        /// 
        /// string data to write.
        protected void WriteAscii(string data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            var bytes = _ascii.GetBytes(data);
            this.Write((uint)bytes.Length);
            this.Write(bytes);
        }
        /// 
        /// Writes string data into internal buffer.
        /// 
        /// string data to write.
        ///  is null.
        protected void WriteBinaryString(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            this.Write((uint)data.Length);
            this._data.AddRange(data);
        }
        /// 
        /// Writes mpint data into internal buffer.
        /// 
        /// mpint data to write.
        protected void Write(BigInteger data)
        {
            var bytes = data.ToByteArray().Reverse().ToList();
            this.Write((uint)bytes.Count);
            this.Write(bytes);
        }
        /// 
        /// Writes name-list data into internal buffer.
        /// 
        /// name-list data to write.
        protected void Write(string[] data)
        {
            this.WriteAscii(string.Join(",", data));
        }
        /// 
        /// Writes extension-pair data into internal buffer.
        /// 
        /// extension-pair data to write.
        protected void Write(IDictionary data)
        {
            foreach (var item in data)
            {
                this.WriteAscii(item.Key);
                this.WriteAscii(item.Value);
            }
        }
    }
}