//-----------------------------------------------------------------------------
// Copyright (c) 2013 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

#include "fileStream.h"
#include "platform/platform.h"
#include "math/mMath.h"

//-----------------------------------------------------------------------------
// FileStream methods...
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
FileStream::FileStream()
{
   // initialize the file stream
   init();
}

//-----------------------------------------------------------------------------
FileStream::~FileStream()
{
   // make sure the file stream is closed
   close();
}

//-----------------------------------------------------------------------------
bool FileStream::hasCapability(const Capability i_cap) const
{
   return(0 != (U32(i_cap) & mStreamCaps));
}

//-----------------------------------------------------------------------------
U32 FileStream::getPosition() const
{
   AssertFatal(0 != mStreamCaps, "FileStream::getPosition: the stream isn't open");
   //AssertFatal(true == hasCapability(StreamPosition), "FileStream::getPosition(): lacks positioning capability");

   // return the position inside the buffer if its valid, otherwise return the underlying file position
   return((BUFFER_INVALID != mBuffHead) ? mBuffPos : mFile.getPosition());
}

//-----------------------------------------------------------------------------
bool FileStream::setPosition(const U32 i_newPosition)
{
   AssertFatal(0 != mStreamCaps, "FileStream::setPosition: the stream isn't open");
   AssertFatal(true == hasCapability(StreamPosition), "FileStream::setPosition: lacks positioning capability");

   // if the buffer is valid, test the new position against the bounds of the buffer
   if ((BUFFER_INVALID != mBuffHead) && (i_newPosition >= mBuffHead) && (i_newPosition <= mBuffTail))
   {
      // set the position and return
      mBuffPos = i_newPosition;
      
      // FIXME [tom, 9/5/2006] This needs to be checked. Basically, when seeking within
      // the buffer, if the stream has an EOS status before the seek then if you try to
      // read immediately after seeking, you'll incorrectly get an EOS.
      //
      // I am not 100% sure if this fix is correct, but it seems to be working for the undo system.
      if(mBuffPos < mBuffTail)
         Stream::setStatus(Ok);
      
      return(true);
   }
   // otherwise the new position lies in some block not in memory
   else
   {
      // flush the buffer if its dirty
      if (true == mDirty)
         Flush();
      // and clear out the state of the file stream
      clearBuffer();

      // and set the new position
      mFile.setPosition((S32)i_newPosition);
      // update the stream to reflect the file's state
      setStatus();
      // taking end-of-file into consideration
      if ((S32)EOS == (S32)(mFile.getStatus()))
         mEOF = true;
      // and return good states
      return(Ok == getStatus() || EOS == getStatus());
   }
}

//-----------------------------------------------------------------------------
U32 FileStream::getStreamSize()
{
   AssertWarn(0 != mStreamCaps, "FileStream::getStreamSize: the stream isn't open");
   AssertFatal((BUFFER_INVALID != mBuffHead && true == mDirty) || false == mDirty, "FileStream::getStreamSize: buffer must be valid if its dirty");

   // the stream size may not match the size on-disk if its been written to...
   if (true == mDirty)
      return(getMax(mFile.getSize(), mBuffTail + 1));
   // otherwise just get the size on disk...
   else
      return(mFile.getSize());
}

//-----------------------------------------------------------------------------
bool FileStream::open(const char *i_pFilename, AccessMode i_openMode)
{
   AssertWarn(0 == mStreamCaps, "FileStream::setPosition: the stream is already open");
   AssertFatal(NULL != i_pFilename, "FileStream::open: NULL filename");

   // make sure the file stream's state is clean
   clearBuffer();
   
   File::Status status = mFile.open(i_pFilename, (File::AccessMode)i_openMode);

   if (status == File::Ok || status == File::EOS)
   {
      setStatus();
      switch (i_openMode)
      {
         case Read:
            mStreamCaps = U32(StreamRead) |
                          U32(StreamPosition);
            break;
         case Write:
         case WriteAppend:
            mStreamCaps = U32(StreamWrite) |
                          U32(StreamPosition);
            break;
         case ReadWrite:
            mStreamCaps = U32(StreamRead)  |
                          U32(StreamWrite) |
                          U32(StreamPosition);
            break;
         default:
            AssertFatal(false, "FileStream::open: bad access mode");
      }
   }
   else
   {
      setStatus();
      return(false);
   }
   return(true);
}

//-----------------------------------------------------------------------------
void FileStream::close()
{
   if (Closed == getStatus())
      return;

   // make sure nothing in the buffer differs from what is on disk
   if (true == mDirty)
      Flush();
    
   // and close the file
   File::Status closeResult;
   closeResult = mFile.close();

   AssertFatal(File::Closed == closeResult, "FileStream::close: close failed");

   // clear the file stream's state
   init();
}

//-----------------------------------------------------------------------------
bool FileStream::Flush()
{
   AssertWarn(0 != mStreamCaps, "FileStream::flush: the stream isn't open");
   AssertFatal(false == mDirty || BUFFER_INVALID != mBuffHead, "FileStream::flush: buffer must be valid if its dirty");

   // if the buffer is dirty
   if (true == mDirty)
   {
      AssertFatal(true == hasCapability(StreamWrite), "FileStream::flush: a buffer without write-capability should never be dirty");
      // align the file pointer to the buffer head
      if (mBuffHead != mFile.getPosition())
      {
         mFile.setPosition(mBuffHead);
         if (File::Ok != mFile.getStatus() && File::EOS != mFile.getStatus())
            return(false);
      }
      // write contents of the buffer to disk
      U32 blockHead;
      calcBlockHead(mBuffHead, &blockHead);
      mFile.write(mBuffTail - mBuffHead + 1, (char *)mBuffer + (mBuffHead - blockHead));
      // and update the file stream's state
      setStatus();
      if (EOS == getStatus())
         mEOF = true;

      if (Ok == getStatus() || EOS == getStatus())
         // and update the status of the buffer
         mDirty = false;
      else
         return(false);
   }
   return(true);
}

//-----------------------------------------------------------------------------
bool FileStream::_read(const U32 i_numBytes, void *o_pBuffer)
{
   AssertFatal(0 != mStreamCaps, "FileStream::_read: the stream isn't open");
   AssertFatal(NULL != o_pBuffer || i_numBytes == 0, "FileStream::_read: NULL destination pointer with non-zero read request");

   if (false == hasCapability(Stream::StreamRead))
   {
      AssertFatal(false, "FileStream::_read: file stream lacks capability");
      Stream::setStatus(IllegalCall);
      return(false);
   }

   // exit on pre-existing errors
   if (Ok != getStatus())
      return(false);

   // if a request of non-zero length was made
   if (0 != i_numBytes)
   {
      U8 *pDst = (U8 *)o_pBuffer;
      U32 readSize;
      U32 remaining = i_numBytes;
      U32 bytesRead;
      U32 blockHead;
      U32 blockTail;

      // check if the buffer has some data in it
      if (BUFFER_INVALID != mBuffHead)
      {
         // copy as much as possible from the buffer into the destination
         readSize = ((mBuffTail + 1) >= mBuffPos) ? (mBuffTail + 1 - mBuffPos) : 0;
         readSize = getMin(readSize, remaining);
         calcBlockHead(mBuffPos, &blockHead);
         dMemcpy(pDst, mBuffer + (mBuffPos - blockHead), readSize);
         // reduce the remaining amount to read
         remaining -= readSize;
         // advance the buffer pointers
         mBuffPos += readSize;
         pDst += readSize;

         if (mBuffPos > mBuffTail && remaining != 0)
         {
            Flush();
            mBuffHead = BUFFER_INVALID;
            if (mEOF == true)
               Stream::setStatus(EOS);
         }
      }

      // if the request wasn't satisfied by the buffer and the file has more data
      if (false == mEOF && 0 < remaining)
      {
         // flush the buffer if its dirty, since we now need to go to disk
         if (true == mDirty)
            Flush();

         // make sure we know the current read location in the underlying file
         mBuffPos = mFile.getPosition();
         calcBlockBounds(mBuffPos, &blockHead, &blockTail);

         // check if the data to be read falls within a single block
         if ((mBuffPos + remaining) <= blockTail)
         {
            // fill the buffer from disk
            if (true == fillBuffer(mBuffPos))
            {
               // copy as much as possible from the buffer to the destination
               remaining = getMin(remaining, mBuffTail - mBuffPos + 1);
               dMemcpy(pDst, mBuffer + (mBuffPos - blockHead), remaining);
               // advance the buffer pointer
               mBuffPos += remaining;
            }
            else
               return(false);
         }
         // otherwise the remaining spans multiple blocks
         else
         {
            clearBuffer();
            // read from disk directly into the destination
            mFile.read(remaining, (char *)pDst, &bytesRead);
            setStatus();
            // check to make sure we read as much as expected
            if (Ok == getStatus() || EOS == getStatus())
            {
               // if not, update the end-of-file status
               if (0 != bytesRead && EOS == getStatus())
               {
                  Stream::setStatus(Ok);
                  mEOF = true;
               }
            }
            else
               return(false);
         }
      }
   }
   return(true);
}

//-----------------------------------------------------------------------------
bool FileStream::_write(const U32 i_numBytes, const void *i_pBuffer)
{
   AssertFatal(0 != mStreamCaps, "FileStream::_write: the stream isn't open");
   AssertFatal(NULL != i_pBuffer || i_numBytes == 0, "FileStream::_write: NULL source buffer pointer on non-zero write request");

   if (false == hasCapability(Stream::StreamWrite))
   {
      AssertFatal(false, "FileStream::_write: file stream lacks capability");
      Stream::setStatus(IllegalCall);
      return(false);
   }

   // exit on pre-existing errors
   if (Ok != getStatus() && EOS != getStatus())
      return(false);

   // if a request of non-zero length was made
   if (0 != i_numBytes)
   {
      U8 *pSrc = (U8 *)i_pBuffer;
      U32 writeSize;
      U32 remaining = i_numBytes;
      U32 bytesWrit;
      U32 blockHead;
      U32 blockTail;

      // check if the buffer is valid
      if (BUFFER_INVALID != mBuffHead)
      {
         // copy as much as possible from the source to the buffer
         calcBlockBounds(mBuffHead, &blockHead, &blockTail);
         writeSize = (mBuffPos > blockTail) ? 0 : blockTail - mBuffPos + 1;
         writeSize = getMin(writeSize, remaining);

         AssertFatal(0 == writeSize || (mBuffPos - blockHead) < BUFFER_SIZE, "FileStream::_write: out of bounds buffer position");
         dMemcpy(mBuffer + (mBuffPos - blockHead), pSrc, writeSize);
         // reduce the remaining amount to be written
         remaining -= writeSize;
         // advance the buffer pointers
         mBuffPos += writeSize;
         mBuffTail = getMax(mBuffTail, mBuffPos - 1);
         pSrc += writeSize;
         // mark the buffer dirty
         if (0 < writeSize)
            mDirty = true;
      }

      // if the request wasn't satisfied by the buffer
      if (0 < remaining)
      {
         // flush the buffer if its dirty, since we now need to go to disk
         if (true == mDirty)
            Flush();

         // make sure we know the current write location in the underlying file
         mBuffPos = mFile.getPosition();
         calcBlockBounds(mBuffPos, &blockHead, &blockTail);

         // check if the data to be written falls within a single block
         if ((mBuffPos + remaining) <= blockTail)
         {
            // write the data to the buffer
            dMemcpy(mBuffer + (mBuffPos - blockHead), pSrc, remaining);
            // update the buffer pointers
            mBuffHead = mBuffPos;
            mBuffPos += remaining;
            mBuffTail = mBuffPos - 1;
            // mark the buffer dirty
            mDirty = true;
         }
         // otherwise the remaining spans multiple blocks
         else
         {
            clearBuffer();
            // write to disk directly from the source
            mFile.write(remaining, (char *)pSrc, &bytesWrit);
            setStatus();
            return(Ok == getStatus() || EOS == getStatus());
         }
      }
   }
   return(true);
}

//-----------------------------------------------------------------------------
void FileStream::init()
{
   mStreamCaps = 0;
   Stream::setStatus(Closed);
   clearBuffer();
}

//-----------------------------------------------------------------------------
bool FileStream::fillBuffer(const U32 i_startPosition)
{
   AssertFatal(0 != mStreamCaps, "FileStream::fillBuffer: the stream isn't open");
   AssertFatal(false == mDirty, "FileStream::fillBuffer: buffer must be clean to fill");

   // make sure start position and file pointer jive
   if (i_startPosition != mFile.getPosition())
   {
      mFile.setPosition(i_startPosition);
      if (File::Ok != mFile.getStatus() && File::EOS != mFile.getStatus())
      {
         setStatus();
         return(false);
      }
      else
         // update buffer pointer
         mBuffPos = i_startPosition;
   }

   // check if file pointer is at end-of-file
   if (EOS == getStatus())
   {
      // invalidate the buffer
      mBuffHead = BUFFER_INVALID;
      // set the status to end-of-stream
      mEOF = true;
   }
   // otherwise
   else
   {
      U32 bytesRead = 0;
      U32 blockHead;
      // locate bounds of buffer containing current position
      calcBlockHead(mBuffPos, &blockHead);
      // read as much as possible from input file
      mFile.read(BUFFER_SIZE - (i_startPosition - blockHead), (char *)mBuffer + (i_startPosition - blockHead), &bytesRead);
      setStatus();
      if (Ok == getStatus() || EOS == getStatus())
      {
         // update buffer pointers
         mBuffHead = i_startPosition;
         mBuffPos = i_startPosition;
         mBuffTail = i_startPosition + bytesRead - 1;
         // update end-of-file status
         if (0 != bytesRead && EOS == getStatus())
         {
            Stream::setStatus(Ok);
            mEOF = true;
         }
      }
      else
      {
         mBuffHead = BUFFER_INVALID;
         return(false);
      }
   }
   return(true);
}

//-----------------------------------------------------------------------------
void FileStream::clearBuffer()
{
   mBuffHead = BUFFER_INVALID;
   mBuffPos  = 0;
   mBuffTail = 0;
   mDirty = false;
   mEOF = false;
}

//-----------------------------------------------------------------------------
void FileStream::calcBlockHead(const U32 i_position, U32 *o_blockHead)
{
   AssertFatal(NULL != o_blockHead, "FileStream::calcBlockHead: NULL pointer passed for block head");

   *o_blockHead = i_position/BUFFER_SIZE * BUFFER_SIZE;
}

//-----------------------------------------------------------------------------
void FileStream::calcBlockBounds(const U32 i_position, U32 *o_blockHead, U32 *o_blockTail)
{
   AssertFatal(NULL != o_blockHead, "FileStream::calcBlockBounds: NULL pointer passed for block head");
   AssertFatal(NULL != o_blockTail, "FileStream::calcBlockBounds: NULL pointer passed for block tail");

   *o_blockHead = i_position/BUFFER_SIZE * BUFFER_SIZE;
   *o_blockTail = *o_blockHead + BUFFER_SIZE - 1;
}

//-----------------------------------------------------------------------------
void FileStream::setStatus()
{
   switch (mFile.getStatus())
   {
      case File::Ok:
         Stream::setStatus(Ok);
         break;
      case File::IOError:
         Stream::setStatus(IOError);
         break;
      case File::EOS:
         Stream::setStatus(EOS);
         break;
      case File::IllegalCall:
         Stream::setStatus(IllegalCall);
         break;
      case File::Closed:
         Stream::setStatus(Closed);
         break;
      case File::UnknownError:
         Stream::setStatus(UnknownError);
         break;
      default:
         AssertFatal(false, "FileStream::setStatus: invalid error mode");
   }
}
