using System;
using System.Collections.Generic;
#if !TUNING
using System.Linq;
#endif
using System.Text;
using System.Globalization;
namespace Renci.SshNet.Common
{
    /// 
    /// Base ssh data serialization type
    /// 
    public abstract class SshData
    {
        internal const int DefaultCapacity = 64;
        internal static readonly Encoding Ascii = new ASCIIEncoding();
#if SILVERLIGHT
        internal static readonly Encoding Utf8 = Encoding.UTF8;
#else
        internal static readonly Encoding Utf8 = Encoding.Default;
#endif
#if TUNING
        private SshDataStream _stream;
        protected SshDataStream DataStream
        {
            get { return _stream; }
        }
#else
        /// 
        /// Data byte array that hold message unencrypted data
        /// 
        private List _data;
        private int _readerIndex;
#endif
        /// 
        /// Gets a value indicating whether all data from the buffer has been read.
        /// 
        /// 
        /// 	true if this instance is end of data; otherwise, false.
        /// 
        protected bool IsEndOfData
        {
            get
            {
#if TUNING
                return _stream.Position >= _stream.Length;
#else
                return _readerIndex >= _data.Count();
#endif
            }
        }
        private byte[] _loadedData;
#if TUNING
        private int _offset;
#endif
        /// 
        /// Gets the index that represents zero in current data type.
        /// 
        /// 
        /// The index of the zero reader.
        /// 
        protected virtual int ZeroReaderIndex
        {
            get
            {
                return 0;
            }
        }
#if TUNING
        /// 
        /// Gets the size of the message in bytes.
        /// 
        /// 
        /// The size of the messages in bytes.
        /// 
        protected virtual int BufferCapacity
        {
            get { return 0; }
        }
#endif
        /// 
        /// Gets data bytes array
        /// 
        /// Byte array representation of data structure.
        public
#if !TUNING
        virtual
#endif
        byte[] GetBytes()
        {
#if TUNING
            var messageLength = BufferCapacity;
            var capacity = messageLength != -1 ? messageLength : DefaultCapacity;
            var dataStream = new SshDataStream(capacity);
            WriteBytes(dataStream);
            return dataStream.ToArray();
#else
            _data = new List();
            SaveData();
            return _data.ToArray();
#endif
        }
#if TUNING
        /// 
        /// Writes the current message to the specified .
        /// 
        /// The  to write the message to.
        protected virtual void WriteBytes(SshDataStream stream)
        {
            _stream = stream;
            SaveData();
        }
#endif
        internal T OfType() where T : SshData, new()
        {
            var result = new T();
#if TUNING
            result.LoadBytes(_loadedData, _offset);
#else
            result.LoadBytes(_loadedData);
#endif
            result.LoadData();
            return result;
        }
        /// 
        /// Loads data from specified bytes.
        /// 
        /// Bytes array.
        ///  is null.
        public void Load(byte[] value)
        {
#if TUNING
            Load(value, 0);
#else
            if (value == null)
                throw new ArgumentNullException("value");
            LoadBytes(value);
            LoadData();
#endif
        }
#if TUNING
        /// 
        /// Loads data from the specified buffer.
        /// 
        /// Bytes array.
        /// The zero-based offset in  at which to begin reading SSH data.
        ///  is null.
        public void Load(byte[] value, int offset)
        {
            LoadBytes(value, offset);
            LoadData();
        }
#endif
        /// 
        /// 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)
        {
#if TUNING
            LoadBytes(bytes, 0);
#else
            // 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");
            ResetReader();
            _loadedData = bytes;
            _data = new List(bytes);
#endif
        }
#if TUNING
        /// 
        /// Loads data bytes into internal buffer.
        /// 
        /// The bytes.
        /// The zero-based offset in  at which to begin reading SSH data.
        ///  is null.
        protected void LoadBytes(byte[] bytes, int offset)
        {
            if (bytes == null)
                throw new ArgumentNullException("bytes");
            _loadedData = bytes;
            _offset = offset;
            _stream = new SshDataStream(bytes);
            ResetReader();
        }
#endif
        /// 
        /// Resets internal data reader index.
        /// 
        protected void ResetReader()
        {
#if TUNING
            _stream.Position = ZeroReaderIndex + _offset;
#else
            _readerIndex = ZeroReaderIndex;  //  Set to 1 to skip first byte which specifies message type
#endif
        }
        /// 
        /// 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()
        {
#if TUNING
            var bytesLength = (int) (_stream.Length - _stream.Position);
            var data = new byte[bytesLength];
            _stream.Read(data, 0, bytesLength);
            return data;
#else
            var data = new byte[_data.Count - _readerIndex];
            _data.CopyTo(_readerIndex, data, 0, data.Length);
            return data;
#endif
        }
        /// 
        /// 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 TUNING
            var data = new byte[length];
            var bytesRead = _stream.Read(data, 0, length);
            if (bytesRead < length)
                throw new ArgumentOutOfRangeException("length");
            return data;
#else
            if (length > _data.Count)
                throw new ArgumentOutOfRangeException("length");
            var result = new byte[length];
            _data.CopyTo(_readerIndex, result, 0, length);
            _readerIndex += length;
            return result;
#endif
        }
        /// 
        /// Reads next byte data type from internal buffer.
        /// 
        /// Byte read.
        protected byte ReadByte()
        {
#if TUNING
            var byteRead = _stream.ReadByte();
            if (byteRead == -1)
                throw new InvalidOperationException("Attempt to read past the end of the SSH data stream.");
            return (byte) byteRead;
#else
            return ReadBytes(1).FirstOrDefault();
#endif
        }
        /// 
        /// Reads next boolean data type from internal buffer.
        /// 
        /// Boolean read.
        protected bool ReadBoolean()
        {
            return ReadByte() != 0;
        }
        /// 
        /// Reads next uint16 data type from internal buffer.
        /// 
        /// uint16 read
        protected ushort ReadUInt16()
        {
            var data = ReadBytes(2);
            return (ushort)(data[0] << 8 | data[1]);
        }
        /// 
        /// Reads next uint32 data type from internal buffer.
        /// 
        /// uint32 read
        protected uint ReadUInt32()
        {
            var data = 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 ulong ReadUInt64()
        {
            var data = ReadBytes(8);
            return ((ulong)data[0] << 56 | (ulong)data[1] << 48 | (ulong)data[2] << 40 | (ulong)data[3] << 32 | (ulong)data[4] << 24 | (ulong)data[5] << 16 | (ulong)data[6] << 8 | data[7]);
        }
        /// 
        /// Reads next int64 data type from internal buffer.
        /// 
        /// int64 read
        protected long ReadInt64()
        {
            var data = 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]);
        }
#if !TUNING
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected string ReadAsciiString()
        {
            var length = ReadUInt32();
            if (length > int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return Ascii.GetString(ReadBytes((int)length), 0, (int)length);
        }
#endif
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected string ReadString()
        {
            return ReadString(Utf8);
        }
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected string ReadString(Encoding encoding)
        {
            var length = ReadUInt32();
            if (length > int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return encoding.GetString(ReadBytes((int)length), 0, (int)length);
        }
#if TUNING
        /// 
        /// Reads next data type as byte array from internal buffer.
        /// 
        /// 
        /// The bytes read.
        /// 
        protected byte[] ReadBinary()
        {
            var length = ReadUInt32();
            if (length > int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Data longer than {0} is not supported.", int.MaxValue));
            }
            return ReadBytes((int) length);
        }
#else
        /// 
        /// Reads next string data type from internal buffer.
        /// 
        /// string read
        protected byte[] ReadBinaryString()
        {
            var length = ReadUInt32();
            if (length > int.MaxValue)
            {
                throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Strings longer than {0} is not supported.", int.MaxValue));
            }
            return ReadBytes((int)length);
        }
#endif
        /// 
        /// Reads next mpint data type from internal buffer.
        /// 
        /// mpint read.
        protected BigInteger ReadBigInt()
        {
            var length = ReadUInt32();
            var data = ReadBytes((int)length);
#if TUNING
            return new BigInteger(data.Reverse());
#else
            return new BigInteger(data.Reverse().ToArray());
#endif
        }
        /// 
        /// Reads next name-list data type from internal buffer.
        /// 
        /// String array or read data..
        protected string[] ReadNamesList()
        {
            var namesList = ReadString();
            return namesList.Split(',');
        }
        /// 
        /// Reads next extension-pair data type from internal buffer.
        /// 
        /// Extensions pair dictionary.
        protected IDictionary ReadExtensionPair()
        {
            var result = new Dictionary();
            while (!IsEndOfData)
            {
                var extensionName = ReadString();
                var extensionData = ReadString();
                result.Add(extensionName, extensionData);
            }
            return result;
        }
#if TUNING
        /// 
        /// Writes bytes array data into internal buffer.
        /// 
        /// Byte array data to write.
        ///  is null.
        protected void Write(byte[] data)
        {
            _stream.Write(data, 0, data.Length);
        }
#else
        /// 
        /// Writes bytes array data into internal buffer.
        /// 
        /// Byte array data to write.
        ///  is null.
        protected void Write(IEnumerable data)
        {
            _data.AddRange(data);
        }
#endif
#if TUNING
        /// 
        /// Writes a sequence of bytes to the current SSH data stream and advances the current position
        /// within this stream by the number of bytes written.
        /// 
        /// An array of bytes. This method write  bytes from buffer to the current SSH data stream.
        /// The zero-based offset in  at which to begin writing bytes to the SSH data stream.
        /// The number of bytes to be written to the current SSH data stream.
        ///  is null.
        /// The sum of  and  is greater than the buffer length.
        ///  or  is negative.
        protected void Write(byte[] buffer, int offset, int count)
        {
            _stream.Write(buffer, offset, count);
        }
#endif
        /// 
        /// Writes byte data into internal buffer.
        /// 
        /// Byte data to write.
        protected void Write(byte data)
        {
#if TUNING
            _stream.WriteByte(data);
#else
            _data.Add(data);
#endif
        }
        /// 
        /// Writes boolean data into internal buffer.
        /// 
        /// Boolean data to write.
        protected void Write(bool data)
        {
            Write(data ? (byte) 1 : (byte) 0);
        }
        /// 
        /// Writes uint16 data into internal buffer.
        /// 
        /// uint16 data to write.
        protected void Write(ushort data)
        {
            Write(data.GetBytes());
        }
        /// 
        /// Writes uint32 data into internal buffer.
        /// 
        /// uint32 data to write.
        protected void Write(uint data)
        {
            Write(data.GetBytes());
        }
        /// 
        /// Writes uint64 data into internal buffer.
        /// 
        /// uint64 data to write.
        protected void Write(ulong data)
        {
            Write(data.GetBytes());
        }
        /// 
        /// Writes int64 data into internal buffer.
        /// 
        /// int64 data to write.
        protected void Write(long data)
        {
            Write(data.GetBytes());
        }
        /// 
        /// Writes string data into internal buffer as ASCII.
        /// 
        /// string data to write.
        protected void WriteAscii(string data)
        {
            Write(data, Ascii);
        }
        /// 
        /// Writes string data into internal buffer using default encoding.
        /// 
        /// string data to write.
        ///  is null.
        protected void Write(string data)
        {
            Write(data, Utf8);
        }
        /// 
        /// Writes string data into internal buffer using the specified encoding.
        /// 
        /// string data to write.
        /// The character encoding to use.
        ///  is null.
        ///  is null.
        protected void Write(string data, Encoding encoding)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            if (encoding == null)
                throw new ArgumentNullException("encoding");
            var bytes = encoding.GetBytes(data);
#if TUNING
            var bytesLength = bytes.Length;
            Write((uint) bytesLength);
            Write(bytes, 0, bytesLength);
#else
            Write((uint)bytes.Length);
            Write(bytes);
#endif
        }
#if TUNING
        /// 
        /// Writes data into internal buffer.
        /// 
        /// The data to write.
        ///  is null.
        protected void WriteBinaryString(byte[] buffer)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            var bufferLength = buffer.Length;
            Write((uint)bufferLength);
            Write(buffer, 0, bufferLength);
        }
        /// 
        /// Writes data into internal buffer.
        /// 
        /// An array of bytes. This method write  bytes from buffer to the current SSH data stream.
        /// The zero-based byte offset in  at which to begin writing bytes to the SSH data stream.
        /// The number of bytes to be written to the current SSH data stream.
        ///  is null.
        /// The sum of  and  is greater than the buffer length.
        ///  or  is negative.
        protected void WriteBinary(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            Write((uint) count);
            Write(buffer, offset, count);
        }
#else
        /// 
        /// Writes string data into internal buffer.
        /// 
        /// string data to write.
        ///  is null.
        protected void WriteBinaryString(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException("data");
            Write((uint)data.Length);
            _data.AddRange(data);
        }
#endif
        /// 
        /// Writes mpint data into internal buffer.
        /// 
        /// mpint data to write.
        protected void Write(BigInteger data)
        {
#if TUNING
            var bytes = data.ToByteArray().Reverse();
            var bytesLength = bytes.Length;
            Write((uint) bytesLength);
            Write(bytes, 0, bytesLength);
#else
            var bytes = data.ToByteArray().Reverse().ToList();
            Write((uint)bytes.Count);
            Write(bytes);
#endif
        }
        /// 
        /// Writes name-list data into internal buffer.
        /// 
        /// name-list data to write.
        protected void Write(string[] data)
        {
            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)
            {
                WriteAscii(item.Key);
                WriteAscii(item.Value);
            }
        }
    }
}