/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Text;
using Apache.Qpid.Proton.Utilities;

namespace Apache.Qpid.Proton.Buffer
{
   /// <summary>
   /// A proton buffer implementation that wraps a single heap allocated
   /// byte array and provides read and write operations on that array
   /// along with self resizing based on capacity limits.
   /// </summary>
   public sealed class ProtonByteBuffer : IProtonBuffer, IReadableComponent, IWritableComponent
   {
      /// <summary>
      /// Default initial capacity when created without initial value.
      /// </summary>
      public static readonly int DefaultCapacity = 64;

      /// <summary>
      /// Default max capacity based on maximum array size limit as this
      /// buffer is backed by a byte array.
      /// </summary>
      public static readonly int DefaultMaximumCapacity = int.MaxValue;

      private byte[] array;

      /// <summary>
      /// How far into the array does position zero begin
      /// </summary>
      private int arrayOffset;

      /// <summary>
      /// Limits the usable region of the array if not set to the array length.
      /// This allows the window of operations to exists in a subregion of the
      /// array.
      /// </summary>
      private int arrayLimit;

      private long readOffset;
      private long writeOffset;
      private readonly long maxCapacity;

      /// <summary>
      /// Create a new proton byte buffer instance with default initial capacity
      /// and limited only by the size of a byte array in max capacity.
      /// </summary>
      public ProtonByteBuffer() : this(DefaultCapacity, DefaultMaximumCapacity)
      {
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given initial capacity
      /// and limited only by the size of a byte array in max capacity.
      /// </summary>
      /// <param name="initialCapacity">The initial capacity of this buffer</param>
      public ProtonByteBuffer(long initialCapacity) : this(initialCapacity, DefaultMaximumCapacity)
      {
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given initial capacity
      /// and limited to a max capacity of the given amount.
      /// </summary>
      /// <param name="initialCapacity">The initial capacity of this buffer</param>
      public ProtonByteBuffer(long initialCapacity, long maxCapacity) : base()
      {
         if (initialCapacity < 0)
         {
            throw new ArgumentOutOfRangeException(nameof(initialCapacity), "Initial capacity cannot be negative");
         }

         if (maxCapacity < initialCapacity)
         {
            throw new ArgumentOutOfRangeException(nameof(maxCapacity), "The maximum capacity cannot be less than the initial value");
         }

         this.array = new byte[initialCapacity];
         this.arrayOffset = 0;
         this.arrayLimit = array.Length;
         this.maxCapacity = maxCapacity;
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given backing array whose
      /// size determines that largest the buffer can ever be.
      /// </summary>
      /// <param name="backingArray">The actual byte array that backs this buffer</param>
      public ProtonByteBuffer(byte[] backingArray) : this(backingArray, 0, int.MaxValue)
      {
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given backing array as the
      /// starting backing store and uses the provided max capacity value to control
      /// how large the buffer could ever grow.
      /// </summary>
      /// <param name="backingArray">The actual byte array that backs this buffer</param>
      /// <param name="maxCapacity">The maximum capacity this buffer can grow to</param>
      public ProtonByteBuffer(byte[] backingArray, long maxCapacity) : this(backingArray, 0, maxCapacity)
      {
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given backing array as the
      /// starting backing store and uses the provided max capacity value to control
      /// how large the buffer could ever grow.
      /// </summary>
      /// <param name="backingArray">The actual byte array that backs this buffer</param>
      /// <param name="arrayOffset">The offset into the backing array where the buffer starts</param>
      /// <param name="maxCapacity">The maximum capacity this buffer can grow to</param>
      public ProtonByteBuffer(byte[] backingArray, int arrayOffset, long maxCapacity)
       : this(backingArray, arrayOffset, backingArray.Length - arrayOffset, maxCapacity)
      {
      }

      /// <summary>
      /// Create a new proton byte buffer instance with given backing array as the
      /// starting backing store and uses the provided max capacity value to control
      /// how large the buffer could ever grow.
      /// </summary>
      /// <param name="backingArray">The actual byte array that backs this buffer</param>
      /// <param name="arrayOffset">The offset index into the backing array where the buffer starts</param>
      /// <param name="capacity">The capacity limit for this view of the assigned array</param>
      /// <param name="maxCapacity">The maximum capacity this buffer can grow to</param>
      public ProtonByteBuffer(byte[] backingArray, int arrayOffset, int capacity, long maxCapacity) : base()
      {
         if (arrayOffset > backingArray.Length)
         {
            throw new ArgumentOutOfRangeException(nameof(arrayOffset), "Array offset cannot exceed the array length");
         }

         if (capacity > backingArray.Length - arrayOffset)
         {
            throw new ArgumentOutOfRangeException(nameof(capacity),
               "Array segment capacity cannot exceed the configured array length minus the offset");
         }

         this.array = backingArray;
         this.arrayOffset = arrayOffset;
         this.arrayLimit = arrayOffset + capacity;
         this.maxCapacity = maxCapacity;
      }

      #region Buffer State and Management APIs

      public long Capacity => arrayLimit - arrayOffset;

      public bool IsReadable => ReadOffset < WriteOffset;

      public long ReadableBytes => WriteOffset - ReadOffset;

      public bool IsWritable => WriteOffset < Capacity;

      public long WritableBytes => Capacity - WriteOffset;

      public long ReadOffset
      {
         get => readOffset;
         set
         {
            CheckRead(value, 0);
            readOffset = value;
         }
      }

      public long WriteOffset
      {
         get => writeOffset;
         set
         {
            CheckWrite(value, 0);
            writeOffset = value;
         }
      }

      public uint ComponentCount => 1;

      public uint ReadableComponentCount => IsReadable ? 1u : 0u;

      public uint WritableComponentCount => IsWritable ? 1u : 0u;

      public IProtonBuffer EnsureWritable(long amount)
      {
         return InternalEnsureWritable(true, 1, amount);
      }

      public IProtonBuffer Compact()
      {
         if (readOffset != 0)
         {
            // Compress the current readable section into the front of the
            // array and then update the offsets to match the new reality.
            Array.Copy(array, Offset(readOffset), array, arrayOffset, writeOffset - readOffset);
            writeOffset -= readOffset;
            readOffset = 0;
         }

         return this;
      }

      public IProtonBuffer Reclaim()
      {
         // For now we do nothing, but in the future some logic might be added
         // to compact and or reallocate to a smaller buffer in order to free
         // some memory space.
         return this;
      }

      public IProtonBuffer WriteSplit(long offset)
      {
         return Split(WriteOffset + offset);
      }

      public IProtonBuffer ReadSplit(long offset)
      {
         return Split(ReadOffset + offset);
      }

      public IProtonBuffer Split()
      {
         return Split(WriteOffset);
      }

      public IProtonBuffer Split(long offset)
      {
         if (offset > int.MaxValue)
         {
            throw new ArgumentOutOfRangeException(nameof(offset), "Proton byte buffer cannot exceed Int32.MaxValue bytes in capacity");
         }

         if (offset < 0 || offset > Capacity)
         {
            throw new ArgumentOutOfRangeException(nameof(offset), "The buffer split offset must be within the current buffer capacity");
         }

         int iOffset = (int)offset;

         // Conceptual definition of the split buffer outcome
         //-----------------------------------------------------------------
         //         This buffer:
         //         +--------------------------------+
         //         0|               |splitOffset     |cap
         //         +---------------+----------------+
         //         /               / \               \
         //        /               /   \               \
         //       /               /     \               \
         //      /               /       \               \
         //     /               /         \               \
         //    +---------------+           +---------------+
         //    |0              |cap        |0              |cap
         //    +---------------+           +---------------+
         //    Returned buffer.            This buffer.

         // The new buffer can only view the portion of the array before the split.
         ProtonByteBuffer front = new(array, arrayOffset, iOffset, maxCapacity);

         // This buffer is adjusted to view only the portion of the array after the split
         arrayOffset += iOffset;

         // TODO compute the index values

         front.readOffset = Math.Min(readOffset, front.Capacity);
         front.writeOffset = Math.Min(writeOffset, front.Capacity);

         this.readOffset = Math.Max(0, readOffset - iOffset);
         this.writeOffset = Math.Max(0, writeOffset - iOffset);

         return front;
      }

      public IProtonBuffer Reset()
      {
         ReadOffset = 0;
         WriteOffset = 0;

         return this;
      }

      public IProtonBuffer SkipBytes(long amount)
      {
         CheckRead(readOffset, amount);
         readOffset += amount;
         return this;
      }

      public IProtonBuffer Fill(byte value)
      {
         if (Capacity > 0)
         {
            Array.Fill(array, value, arrayOffset, (int)Capacity);
         }
         return this;
      }

      #endregion

      #region Buffer Copy API implementation

      public IProtonBuffer Copy()
      {
         return Copy(readOffset, writeOffset - readOffset);
      }

      public IProtonBuffer Copy(long index, long length)
      {
         CheckGet(index, length);
         byte[] copyBytes = new byte[length];
         Array.Copy(array, Offset(index), copyBytes, 0, length);

         IProtonBuffer copy = new ProtonByteBuffer(copyBytes, maxCapacity)
         {
            WriteOffset = length
         };

         return copy;
      }

      public IProtonBuffer CopyInto(long srcPos, byte[] dest, long destPos, long length)
      {
         CheckCopyIntoArgs(srcPos, length, destPos, dest.LongLength);
         Array.Copy(array, Offset(srcPos), dest, destPos, length);
         return this;
      }

      public IProtonBuffer CopyInto(long srcPos, IProtonBuffer dest, long destPos, long length)
      {
         CheckCopyIntoArgs(srcPos, length, destPos, dest.Capacity);

         if (dest is ProtonByteBuffer destByteBuffer)
         {
            Array.Copy(array, Offset(srcPos), destByteBuffer.array, destPos, length);
         }
         else
         {
            long writePos = dest.WriteOffset;
            for (long i = 0; i < length; i++)
            {
               dest.SetUnsignedByte(writePos + i, array[srcPos + i]);
            }
         }

         return this;
      }

      #endregion

      #region Buffer iteration API implementations

      public bool HasReadableArray => true;

      public byte[] ReadableArray => array;

      public int ReadableArrayOffset => (int)Offset(readOffset); // Array backing means int casting is ok.

      public int ReadableArrayLength => (int)ReadableBytes; // Array backing means int casting is ok.

      public bool HasWritableArray => true;

      public byte[] WritableArray => array;

      public int WritableArrayOffset => (int)Offset(writeOffset); // Array backing means int casting is ok.

      public int WritableArrayLength => (int)WritableBytes; // Array backing means int casting is ok.

      public int ForEachReadableComponent(in int index, in Func<int, IReadableComponent, bool> processor)
      {
         CheckGet(readOffset, Math.Max(1, ReadableBytes));
         return processor.Invoke(index, this) ? 1 : -1;
      }

      public int ForEachWritableComponent(in int index, in Func<int, IWritableComponent, bool> processor)
      {
         CheckSet(writeOffset, Math.Max(1, WritableBytes));
         return processor.Invoke(index, this) ? 1 : -1;
      }

      #endregion

      #region Implementation of the proton buffer accessors API

      public bool GetBoolean(long index)
      {
         CheckGet(index, sizeof(byte));
         return ProtonByteUtils.ReadBoolean(array, (int)Offset(index));
      }

      public sbyte GetByte(long index)
      {
         CheckGet(index, sizeof(sbyte));
         return ProtonByteUtils.ReadByte(array, (int)Offset(index));
      }

      public char GetChar(long index)
      {
         CheckGet(index, sizeof(char));
         return ProtonByteUtils.ReadChar(array, (int)Offset(index));
      }

      public double GetDouble(long index)
      {
         CheckGet(index, sizeof(double));
         return ProtonByteUtils.ReadDouble(array, (int)Offset(index));
      }

      public float GetFloat(long index)
      {
         CheckGet(index, sizeof(float));
         return ProtonByteUtils.ReadFloat(array, (int)Offset(index));
      }

      public int GetInt(long index)
      {
         CheckGet(index, sizeof(int));
         return ProtonByteUtils.ReadInt(array, (int)Offset(index));
      }

      public long GetLong(long index)
      {
         CheckGet(index, sizeof(long));
         return ProtonByteUtils.ReadLong(array, (int)Offset(index));
      }

      public short GetShort(long index)
      {
         CheckGet(index, sizeof(short));
         return ProtonByteUtils.ReadShort(array, (int)Offset(index));
      }

      public byte GetUnsignedByte(long index)
      {
         CheckGet(index, sizeof(byte));
         return ProtonByteUtils.ReadUnsignedByte(array, (int)Offset(index));
      }

      public uint GetUnsignedInt(long index)
      {
         CheckGet(index, sizeof(uint));
         return ProtonByteUtils.ReadUnsignedInt(array, (int)Offset(index));
      }

      public ulong GetUnsignedLong(long index)
      {
         CheckGet(index, sizeof(ulong));
         return ProtonByteUtils.ReadUnsignedLong(array, (int)Offset(index));
      }

      public ushort GetUnsignedShort(long index)
      {
         CheckGet(index, sizeof(ushort));
         return ProtonByteUtils.ReadUnsignedShort(array, (int)Offset(index));
      }

      public bool ReadBoolean()
      {
         CheckRead(readOffset, sizeof(byte));
         bool result = ProtonByteUtils.ReadBoolean(array, (int)Offset(readOffset));
         readOffset += sizeof(byte);
         return result;
      }

      public sbyte ReadByte()
      {
         CheckRead(readOffset, sizeof(sbyte));
         sbyte result = ProtonByteUtils.ReadByte(array, (int)Offset(readOffset));
         readOffset += sizeof(sbyte);
         return result;
      }

      public char ReadChar()
      {
         CheckRead(readOffset, sizeof(char));
         char result = ProtonByteUtils.ReadChar(array, (int)Offset(readOffset));
         readOffset += sizeof(char);
         return result;
      }

      public double ReadDouble()
      {
         CheckRead(readOffset, sizeof(double));
         double result = ProtonByteUtils.ReadDouble(array, (int)Offset(readOffset));
         readOffset += sizeof(double);
         return result;
      }

      public float ReadFloat()
      {
         CheckRead(readOffset, sizeof(float));
         float result = ProtonByteUtils.ReadFloat(array, (int)Offset(readOffset));
         readOffset += sizeof(float);
         return result;
      }

      public int ReadInt()
      {
         CheckRead(readOffset, sizeof(int));
         int result = ProtonByteUtils.ReadInt(array, (int)Offset(readOffset));
         readOffset += sizeof(int);
         return result;
      }

      public long ReadLong()
      {
         CheckRead(readOffset, sizeof(long));
         long result = ProtonByteUtils.ReadLong(array, (int)Offset(readOffset));
         readOffset += sizeof(long);
         return result;
      }

      public short ReadShort()
      {
         CheckRead(readOffset, sizeof(short));
         short result = ProtonByteUtils.ReadShort(array, (int)Offset(readOffset));
         readOffset += sizeof(short);
         return result;
      }

      public byte ReadUnsignedByte()
      {
         CheckRead(readOffset, sizeof(byte));
         byte result = ProtonByteUtils.ReadUnsignedByte(array, (int)Offset(readOffset));
         readOffset += sizeof(byte);
         return result;
      }

      public uint ReadUnsignedInt()
      {
         CheckRead(readOffset, sizeof(uint));
         uint result = ProtonByteUtils.ReadUnsignedInt(array, (int)Offset(readOffset));
         readOffset += sizeof(uint);
         return result;
      }

      public ulong ReadUnsignedLong()
      {
         CheckRead(readOffset, sizeof(ulong));
         ulong result = ProtonByteUtils.ReadUnsignedLong(array, (int)Offset(readOffset));
         readOffset += sizeof(ulong);
         return result;
      }

      public ushort ReadUnsignedShort()
      {
         CheckRead(readOffset, sizeof(ushort));
         ushort result = ProtonByteUtils.ReadUnsignedShort(array, (int)Offset(readOffset));
         readOffset += sizeof(ushort);
         return result;
      }

      public IProtonBuffer SetBoolean(long index, bool value)
      {
         CheckSet(index, sizeof(byte));
         ProtonByteUtils.WriteBoolean(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetByte(long index, sbyte value)
      {
         CheckSet(index, sizeof(sbyte));
         ProtonByteUtils.WriteByte(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetChar(long index, char value)
      {
         CheckSet(index, sizeof(short));
         ProtonByteUtils.WriteShort((short)value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetDouble(long index, double value)
      {
         CheckSet(index, sizeof(double));
         ProtonByteUtils.WriteDouble(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetFloat(long index, float value)
      {
         CheckSet(index, sizeof(float));
         ProtonByteUtils.WriteFloat(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetInt(long index, int value)
      {
         CheckSet(index, sizeof(int));
         ProtonByteUtils.WriteInt(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetLong(long index, long value)
      {
         CheckSet(index, sizeof(long));
         ProtonByteUtils.WriteLong(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetShort(long index, short value)
      {
         CheckSet(index, sizeof(short));
         ProtonByteUtils.WriteShort(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetUnsignedByte(long index, byte value)
      {
         CheckSet(index, sizeof(byte));
         ProtonByteUtils.WriteUnsignedByte(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetUnsignedInt(long index, uint value)
      {
         CheckSet(index, sizeof(uint));
         ProtonByteUtils.WriteUnsignedInt(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetUnsignedLong(long index, ulong value)
      {
         CheckSet(index, sizeof(ulong));
         ProtonByteUtils.WriteUnsignedLong(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer SetUnsignedShort(long index, ushort value)
      {
         CheckSet(index, sizeof(ushort));
         ProtonByteUtils.WriteUnsignedShort(value, array, (int)Offset(index));
         return this;
      }

      public IProtonBuffer WriteBoolean(bool value)
      {
         CheckWrite(writeOffset, sizeof(byte));
         ProtonByteUtils.WriteBoolean(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(byte);
         return this;
      }

      public IProtonBuffer WriteByte(sbyte value)
      {
         CheckWrite(writeOffset, sizeof(byte));
         ProtonByteUtils.WriteByte(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(byte);
         return this;
      }

      public IProtonBuffer WriteDouble(double value)
      {
         CheckWrite(writeOffset, sizeof(double));
         ProtonByteUtils.WriteDouble(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(double);
         return this;
      }

      public IProtonBuffer WriteFloat(float value)
      {
         CheckWrite(writeOffset, sizeof(float));
         ProtonByteUtils.WriteFloat(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(float);
         return this;
      }

      public IProtonBuffer WriteInt(int value)
      {
         CheckWrite(writeOffset, sizeof(int));
         ProtonByteUtils.WriteInt(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(int);
         return this;
      }

      public IProtonBuffer WriteLong(long value)
      {
         CheckWrite(writeOffset, sizeof(long));
         ProtonByteUtils.WriteLong(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(long);
         return this;
      }

      public IProtonBuffer WriteShort(short value)
      {
         CheckWrite(writeOffset, sizeof(short));
         ProtonByteUtils.WriteShort(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(short);
         return this;
      }

      public IProtonBuffer WriteUnsignedByte(byte value)
      {
         CheckWrite(writeOffset, sizeof(byte));
         ProtonByteUtils.WriteUnsignedByte(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(byte);
         return this;
      }

      public IProtonBuffer WriteUnsignedInt(uint value)
      {
         CheckWrite(writeOffset, sizeof(uint));
         ProtonByteUtils.WriteUnsignedInt(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(uint);
         return this;
      }

      public IProtonBuffer WriteUnsignedLong(ulong value)
      {
         CheckWrite(writeOffset, sizeof(ulong));
         ProtonByteUtils.WriteUnsignedLong(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(ulong);
         return this;
      }

      public IProtonBuffer WriteUnsignedShort(ushort value)
      {
         CheckWrite(writeOffset, sizeof(ushort));
         ProtonByteUtils.WriteUnsignedShort(value, array, (int)Offset(writeOffset));
         writeOffset += sizeof(ushort);
         return this;
      }

      public IProtonBuffer WriteBytes(byte[] source)
      {
         if (source == null)
         {
            throw new ArgumentNullException(nameof(source), "Input source array cannot be null");
         }

         return WriteBytes(source, 0, source.Length);
      }

      public IProtonBuffer WriteBytes(byte[] source, long offset, long length)
      {
         if (source == null)
         {
            throw new ArgumentNullException(nameof(source), "Input source array cannot be null");
         }

         if (length > source.LongLength - offset)
         {
            throw new ArgumentOutOfRangeException("Number of bytes to copy from source array: " + length +
                                                  " is greater than the readable span of the array: " +
                                                  (source.LongLength - offset));
         }

         CheckWriteIntoArgs(source.LongLength, offset, length, writeOffset, Capacity);
         Array.Copy(source, offset, array, Offset(writeOffset), length);
         writeOffset += length;
         return this;
      }

      public IProtonBuffer WriteBytes(IProtonBuffer source)
      {
         long length = source.ReadableBytes;
         CheckWrite(writeOffset, length);
         source.CopyInto(source.ReadOffset, array, Offset(writeOffset), length);
         source.ReadOffset += length;
         writeOffset += length;

         return this;
      }

      #endregion

      #region Standard language API implementations

      public int CompareTo(object obj)
      {
         Statics.RequireNonNull(obj, "Cannot compare a buffer with null");

         if (obj is IProtonBuffer)
         {
            return CompareTo(obj as IProtonBuffer);
         }

         throw new InvalidCastException("Cannot compare input type to an proton buffer type");
      }

      public int CompareTo(IProtonBuffer other)
      {
         Statics.RequireNonNull(other, "Cannot compare a buffer with null");

         long stopIndex = ReadOffset + Math.Min(ReadableBytes, other.ReadableBytes);

         for (long i = ReadOffset, j = other.ReadOffset; i < stopIndex; i++, j++)
         {
            int cmp = Compare(GetUnsignedByte(i), other.GetUnsignedByte(j));
            if (cmp != 0)
            {
               return cmp;
            }
         }

         return (int)(ReadableBytes - other.ReadableBytes);
      }

      public override int GetHashCode()
      {
         return ProtonBufferSupport.GetHashCode(this);
      }

      public override bool Equals(object other)
      {
         if (other is IProtonBuffer)
         {
            return this.Equals(other as IProtonBuffer);
         }
         else
         {
            return false;
         }
      }

      public bool Equals(IProtonBuffer other)
      {
         return ProtonBufferSupport.Equals(this, other);
      }

      public string ToString(Encoding encoding)
      {
         if (!IsReadable)
         {
            return "";
         }

         Decoder decoder = encoding.GetDecoder();
         int charCount = decoder.GetCharCount(array, (int)Offset(readOffset), (int)(writeOffset - readOffset));
         char[] output = new char[charCount];

         int outputChars = decoder.GetChars(array, (int)Offset(readOffset), (int)(writeOffset - readOffset), output, 0);

         return new string(output, 0, outputChars);
      }

      public override string ToString()
      {
         return GetType().Name +
                "{ read:" + ReadOffset +
                ", write: " + WriteOffset +
                ", capacity: " + Capacity + "}";
      }

      #endregion

      #region Internal byte buffer utility methods

      private long Offset(long index)
      {
         return index + arrayOffset;
      }

      private IProtonBuffer InternalEnsureWritable(bool allowCompaction, long minimumGrowth, long requiredWritable)
      {
         if (requiredWritable < 0)
         {
            throw new ArgumentOutOfRangeException(nameof(requiredWritable), "Requested writable bytes cannot be negative");
         }

         if (minimumGrowth < 0)
         {
            throw new ArgumentOutOfRangeException(nameof(minimumGrowth), "Requested minimum growth bytes cannot be negative");
         }

         // Called when we know that we don't need to validate if the minimum writable
         // value is negative.
         if (requiredWritable <= WritableBytes)
         {
            return this;
         }

         if (requiredWritable > maxCapacity - writeOffset)
         {
            throw new ArgumentOutOfRangeException(string.Format(
                "writeOffset({0}) + requiredWritable({1}) exceeds maximum buffer capacity({2}): {3}",
                writeOffset, requiredWritable, maxCapacity, this));
         }

         // Can we solve this with compaction instead of creating more buffers
         if (allowCompaction && WritableBytes + ReadOffset >= requiredWritable)
         {
            Compact();
         }
         else
         {
            long newCapacity = Capacity + Math.Max(requiredWritable - WritableBytes, minimumGrowth);
            if (newCapacity < 1)
            {
               throw new ArgumentOutOfRangeException("Buffer size must be positive, but was " + newCapacity + '.');
            }

            int oldCapacity = arrayLimit - arrayOffset;
            if (newCapacity > oldCapacity)
            {
               byte[] newArray = new byte[newCapacity];
               Array.ConstrainedCopy(array, arrayOffset, newArray, 0, arrayLimit - arrayOffset);
               array = newArray;
               arrayOffset = 0;
               arrayLimit = newArray.Length;
            }
         }

         return this;
      }

      private void CheckWrite(long index, long size)
      {
         if (index < readOffset || (arrayLimit - arrayOffset) < index + size)
         {
            throw OutOfBounds(index);
         }
      }

      private void CheckRead(long index, long size)
      {
         if (index < 0 || writeOffset < index + size)
         {
            throw OutOfBounds(index);
         }
      }

      private void CheckGet(long index, long size)
      {
         if (index < 0 || Capacity < index + size)
         {
            throw OutOfBounds(index);
         }
      }

      private void CheckSet(long index, long size)
      {
         if (index < 0 || Capacity < index + size)
         {
            throw OutOfBounds(index);
         }
      }

      private void CheckCopyIntoArgs(long srcPos, long length, long destPos, long destLength)
      {
         if (srcPos < 0)
         {
            throw new ArgumentOutOfRangeException("The srcPos cannot be negative: " + srcPos + '.');
         }
         if (length < 0)
         {
            throw new ArgumentOutOfRangeException("The length cannot be negative: " + length + '.');
         }
         if (Capacity < srcPos + length)
         {
            throw new ArgumentOutOfRangeException("The srcPos + length is beyond the end of the buffer: " +
                                                  "srcPos = " + srcPos + ", length = " + length + '.');
         }
         if (destPos < 0)
         {
            throw new ArgumentOutOfRangeException("The destPos cannot be negative: " + destPos + '.');
         }
         if (destLength < destPos + length)
         {
            throw new ArgumentOutOfRangeException("The destPos + length is beyond the end of the destination: " +
                                                  "destPos = " + destPos + ", length = " + length + '.');
         }
      }

      private static void CheckWriteIntoArgs(long srcCapacity, long srcPos, long length, long destPos, long destLength)
      {
         if (srcPos < 0)
         {
            throw new ArgumentOutOfRangeException("The srcPos cannot be negative: " + srcPos + '.');
         }
         if (length < 0)
         {
            throw new ArgumentOutOfRangeException("The length cannot be negative: " + length + '.');
         }
         if (srcCapacity < srcPos + length)
         {
            throw new ArgumentOutOfRangeException("The srcPos + length is beyond the end of the source buffer: " +
                                                  "srcPos = " + srcPos + ", length = " + length + '.');
         }
         if (destPos < 0)
         {
            throw new ArgumentOutOfRangeException("The destPos cannot be negative: " + destPos + '.');
         }
         if (destLength < destPos + length)
         {
            throw new ArgumentOutOfRangeException("The destPos + length is beyond the end of the destination: " +
                                                  "destPos = " + destPos + ", length = " + length + '.');
         }
      }

      private Exception OutOfBounds(long index)
      {
         return new IndexOutOfRangeException(
               "Index " + index + " is out of bounds: [read 0 to " + writeOffset + ", write 0 to " + Capacity + "].");
      }

      private static int Compare(byte x, byte y)
      {
         return (x < y) ? -1 : ((x == y) ? 0 : 1);
      }

      #endregion
   }
}