//                           UNCLASSIFIED
/*-------------------------------------------------------------------------
RAYTHEON PROPRIETARY: THIS DOCUMENT CONTAINS DATA OR INFORMATION
PROPRIETARY TO RAYTHEON COMPANY AND IS RESTRICTED TO USE ONLY BY PERSONS
AUTHORIZED BY RAYTHEON COMPANY IN WRITING TO USE IT. DISCLOSURE TO
UNAUTHORIZED PERSONS WOULD LIKELY CAUSE SUBSTANTIAL COMPETITIVE HARM TO
RAYTHEON COMPANY'S BUSINESS POSITION. NEITHER SAID DOCUMENT NOR ITS
CONTENTS SHALL BE FURNISHED OR DISCLOSED TO OR COPIED OR USED BY PERSONS
OUTSIDE RAYTHEON COMPANY WITHOUT THE EXPRESS WRITTEN APPROVAL OF RAYTHEON
COMPANY.

THIS PROPRIETARY NOTICE IS NOT APPLICABLE IF DELIVERED TO THE U.S.
GOVERNMENT.

UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
-------------------------------------------------------------------------*/

using System;
using System.Runtime.InteropServices;

namespace Raytheon.Common
{
   /// <summary>
   /// this class is a container.  It acts like an array that allows bytes to be removed from its 
   /// front.  When bytes are removed, the beginning of the buffer moves to the byte following 
   /// those which were removed. A typical use of this is to AddData(), then use CheckOutStartOfData()/CheckInStartOfData to get
   /// a pointer to the data and the amount of bytes that the pointer points to.
   /// </summary>
   public class DataBuffer: IDisposable
   {
      #region PrivateClassMembers
      private unsafe byte[] _buffer;
      private uint _endOfDataIndex; // index of last used byte
      private uint _startOfDataIndex = uint.MaxValue; // index of first used byte, MaxValue means that buffer is empty
      private uint _startOfCheckedOutDataIndex = uint.MaxValue; // index of first used byte, MaxValue means that buffer is empty

      private static object _syncObj = new Object();
      private IntPtr _pStartOfBuffer;
      private IntPtr _pStartOfCheckedOutData;
      private GCHandle _pinnedArray;
      #endregion

      #region PublicFuctions

      /// <summary>
      /// The constructor which allocates a byte array and pins it so we can return byte*s in StartOfData()
      /// </summary>
      /// <param name="bufferSize">The number of bytes in the data buffer</param>
      public DataBuffer(uint bufferSize)
      {
         _buffer = new byte[bufferSize];
         _pinnedArray = GCHandle.Alloc(_buffer, GCHandleType.Pinned);
         _pStartOfBuffer = _pinnedArray.AddrOfPinnedObject();
         _pStartOfCheckedOutData = IntPtr.Zero;
      }

      /// <summary>
      /// The finalizer
      /// </summary>
      ~DataBuffer()
      {
         Dispose(false);
      }

      /// <summary>
      /// adds bytes to the buffer
      /// throws exception if requesting to add more bytes than entire buffer will hold.
      /// throws an exception if data is checked out and this call attempts to overwrite the checked out data
      /// </summary>
      /// <param name="data">array from which to add bytes to buffer</param>
      /// <param name="numBytesToAdd"># if bytes from array to add to buffer</param>
      public void Add(byte[] data, uint numBytesToAdd)
      {
         // get exclusive access to array, indexes...
         lock (_syncObj)
         {
            uint numBytesCurrentlyUsed = BytesUsed;

            // not enough room in entire buffer?
            if (numBytesToAdd > (_buffer.Length - numBytesCurrentlyUsed))
            {
               throw new Exception("DataBuffer::Add() - Not enough room in buffer for data - need to increase size of buffer");
            }

            // empty buffer?
            if (_startOfDataIndex == UInt32.MaxValue)
            {
               Array.Copy(data, _buffer, numBytesToAdd);
               _startOfDataIndex = 0;
               _endOfDataIndex = numBytesToAdd - 1;
            }
            else // not empty
            {
               // not enough room at end?
               if (numBytesToAdd > (_buffer.Length - _endOfDataIndex - 1))
               {
                  // make sure not overwritting checked out data
                  ulong afterShiftEndOfDataAddy = (ulong)_pStartOfBuffer + numBytesCurrentlyUsed - 1 + numBytesToAdd;

                  if (_pStartOfCheckedOutData != IntPtr.Zero &&
                        afterShiftEndOfDataAddy >= (ulong)_pStartOfCheckedOutData)
                  {
                     throw new Exception("DataBuffer::Add() - attempting to overwrite data that is checked out (when shifting data to beginning)");
                  }

                  // shuffle to front of buffer
                  Array.Copy(_buffer, _startOfDataIndex, _buffer, 0, BytesUsed);
                  _endOfDataIndex = BytesUsed - 1;
                  _startOfDataIndex = 0;
               }
               else if (_pStartOfCheckedOutData != IntPtr.Zero)
               {
                  // make sure not trying to overwrite data when it is checked out                
                  ulong endOfDataAddress = (ulong)_pStartOfBuffer + _endOfDataIndex;

                  if (endOfDataAddress < (ulong)_pStartOfCheckedOutData &&
                        endOfDataAddress + numBytesToAdd >= (ulong)_pStartOfCheckedOutData)
                  {
                     throw new Exception("DataBuffer::Add() - attempting to overwrite data that is checked out");
                  }
               }

               Array.Copy(data, 0, _buffer, _endOfDataIndex + 1, numBytesToAdd);
               _endOfDataIndex += numBytesToAdd;
            }
         }
      }

      /// <summary>
      /// returns # of bytes used in buffer
      /// </summary>
      public uint BytesUsed
      {
         get
         {
            lock (_syncObj)
            {
               // is buffer empty?
               if (_startOfDataIndex == uint.MaxValue)
               {
                  return 0;
               }
               else
               {
                  return _endOfDataIndex - _startOfDataIndex + 1;
               }
            }
         }
      }

      /// <summary>
      /// Checks in the data. This should be used after a call to CheckOutStartOfData()
      /// </summary>
      public void CheckInStartOfData()
      {
         lock (_syncObj)
         {
            _pStartOfCheckedOutData = IntPtr.Zero;
         }
      }

      /// <summary>
      /// This returns a pointer to the start of the new data
      /// </summary>
      /// <param name="numBytesInReturnedBuffer">The number of bytes that the returned pointer contains</param>
      /// <returns>A pointer to the data</returns>
      public IntPtr CheckOutStartOfData(ref uint numBytesInReturnedBuffer)
      {
         lock (_syncObj)
         {
            // hold onto the start index of the checked out data
            _startOfCheckedOutDataIndex = _startOfDataIndex;

            numBytesInReturnedBuffer = 0;

            // if nothing is in the buffer, _startOfCheckedOutDataIndex will be MaxValue
            if (_startOfCheckedOutDataIndex != uint.MaxValue)
            {
               // wrap around condition
               if (_endOfDataIndex < _startOfCheckedOutDataIndex)
               {
                  // set the number of bytes from start of data to the end of the buffer
                  numBytesInReturnedBuffer = (uint)_buffer.Length - _startOfCheckedOutDataIndex + 1;
               }
               else
               {
                  numBytesInReturnedBuffer = _endOfDataIndex - _startOfCheckedOutDataIndex + 1;
               }
            }

            _pStartOfCheckedOutData = IntPtr.Add(_pStartOfBuffer, (int)_startOfCheckedOutDataIndex);

            return _pStartOfCheckedOutData;
         }
      }

      /// <summary>
      /// Copies data into specified array from buffer
      /// Throws exception if asking to copy more bytes than buffer contains
      /// </summary>
      /// <param name="data">array into which to copy data from buffer</param>
      /// <param name="numBytesToCopy"># bytes to copy from buffer to array</param>
      public void Copy(ref byte[] data, uint numBytesToCopy)
      {
         lock (_syncObj)
         {
            // asking to copy more bytes than we have?
            if (numBytesToCopy > BytesUsed)
            {
               throw new Exception("DataBuffer::Copy() - Asking to copy more bytes than exist in buffer");
            }

            Array.Copy(_buffer, _startOfDataIndex, data, 0, numBytesToCopy);
         }
      }

      /// <summary>
      /// Disposes of resources
      /// </summary>
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly")]
      public void Dispose()
      {
         try
         {
            lock (_syncObj)
            {
               Dispose(true);

               GC.SuppressFinalize(this);
            }
         }
         catch (Exception err)
         {
            try
            {
               ErrorLogger.Instance().Write(err.Message + "\r\n" + err.StackTrace);
            }
            catch (Exception)
            {
               //Do not rethrow. Exception from error logger that has already been garbage collected
            }
         }
      }

      /// <summary>
      /// Gets the size of the buffer
      /// </summary>
      /// <returns>returns the number of bytes of the buffer.</returns>
      public uint GetSize()
      {
         lock (_syncObj)
         {
            return (uint)(_buffer.Length);
         }
      }

      /// <summary>
      /// Removes data from the buffer
      /// </summary>
      /// <param name="numBytesToRemove">The number of bytes to remove</param>
      public void Remove(uint numBytesToRemove)
      {
         lock (_syncObj)
         {
            // asking to remove more bytes than we have?
            if (numBytesToRemove > BytesUsed)
            {
               throw new Exception("DataBuffer::Remove() - Requested to remove more bytes than exist in buffer");
            }

            // remove all bytes?
            if (numBytesToRemove == BytesUsed)
            {
               _startOfDataIndex = UInt32.MaxValue;

               // all of the data is gone, need to set the _pStartOfCheckedOutData back to zero to prevent a race condition between adding data and the host checking the data back in
               _pStartOfCheckedOutData = IntPtr.Zero;
            }
            else
            {
               _startOfDataIndex += numBytesToRemove;
            }
         }
      }

      /// <summary>
      /// Removes all data from the buffer
      /// </summary>
      public void RemoveAll()
      {
         lock (_syncObj)
         {
            Remove(BytesUsed);
         }
      }

      /// <summary>
      /// Disposes of resources
      /// </summary>
      /// <param name="disposing"></param>
      protected virtual void Dispose(bool disposing)
      {
         try
         {
            if (disposing)
            {
               _pinnedArray.Free();
            }
         }
         catch (Exception err)
         {
            try
            {
               ErrorLogger.Instance().Write(err.Message + "\r\n" + err.StackTrace);
            }
            catch (Exception)
            {
               //Do not rethrow. Exception from error logger that has already been garbage collected
            }
         }
      }
      #endregion
   }
}
