/*
 * Copyright 2009 - 2010 Kevin Ackley (kackley@gwi.net)
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

#ifdef _WIN32
#if defined(_MSC_VER)
#include <io.h>
#elif defined(__GNUC__)
#define _LARGEFILE64_SOURCE
#define __LARGE64_FILES
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#else
#error "no supported compiler defined"
#endif

#elif defined(__linux__) || defined(__GNU__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__DragonFly__)
#define _LARGEFILE64_SOURCE
#define __LARGE64_FILES
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#elif defined(__APPLE__)
#include <sys/types.h>
#include <unistd.h>
#else
#error "no supported OS platform defined"
#endif

#include <cmath>
#include <cstring>
#include <fcntl.h>

#include "CRC.h"

#include "CheckedFile.h"

//#define E57_CHECK_FILE_DEBUG
#ifdef E57_CHECK_FILE_DEBUG
#include <cassert>
#endif

#ifndef O_BINARY
#define O_BINARY (0)
#endif

#ifndef _unlink
#define _unlink unlink
#endif

using namespace e57;
using namespace std;

// These extra definitions are required in C++11.
// In C++17, "static constexpr" is implicitly inline, so these are not required.
constexpr size_t     CheckedFile::physicalPageSizeLog2;
constexpr size_t     CheckedFile::physicalPageSize;
constexpr FileOffset CheckedFile::physicalPageSizeMask;
constexpr size_t     CheckedFile::logicalPageSize;

CheckedFile::CheckedFile( const ustring &fileName, Mode mode, ReadChecksumPolicy policy ) :
   fileName_(fileName),
   checkSumPolicy_( policy )
{
   switch (mode)
   {
      case ReadOnly:
         fd_ = portableOpen(fileName_, O_RDONLY|O_BINARY, 0);

         readOnly_ = true;
         physicalLength_ = portableSeek(0LL, SEEK_END);
         portableSeek( 0, SEEK_SET );

         logicalLength_ = physicalToLogical( physicalLength_ );
         break;

      case WriteCreate:
         /// File truncated to zero length if already exists
         fd_ = portableOpen(fileName_, O_RDWR|O_CREAT|O_TRUNC|O_BINARY, S_IWRITE|S_IREAD);
         break;

      case WriteExisting:
         fd_ = portableOpen(fileName_, O_RDWR|O_BINARY, 0);

         logicalLength_ = physicalToLogical(length(Physical)); //???
         break;
   }
}

int CheckedFile::portableOpen( const ustring &fileName, int flags, int mode )
{
   //??? handle utf-8 file names?
#if defined(_MSC_VER)
   int handle;
   int err = _sopen_s(&handle, fileName_.c_str(), flags, _SH_DENYNO, mode);
   if (handle < 0)
   {
      throw E57_EXCEPTION2(E57_ERROR_OPEN_FAILED,
                           "err=" + toString(err)
                           + " fileName=" + fileName
                           + " flags=" + toString(flags)
                           + " mode=" + toString(mode));
   }
   return handle;
#elif defined(__GNUC__)
   int result = open(fileName_.c_str(), flags, mode);
   if (result < 0)
   {
      throw E57_EXCEPTION2(E57_ERROR_OPEN_FAILED,
                           "result=" + toString(result)
                           + " fileName=" + fileName
                           + " flags=" + toString(flags)
                           + " mode=" + toString(mode));
   }
   return result;
#else
#  error "no supported compiler defined"
#endif
}


CheckedFile::~CheckedFile()
{
   try {
      close();  ///??? what if already closed?
   } catch (...) {
      //??? report?
   }
}

void CheckedFile::read(char* buf, size_t nRead, size_t /*bufSize*/)
{
   //??? what if read past logical end?, or physical end?
   //??? need to keep track of logical length?
   //??? check bufSize OK

   const FileOffset end = position( Logical ) + nRead;
   const FileOffset logicalLength = length( Logical );

   if (end > logicalLength)
   {
      throw E57_EXCEPTION2(E57_ERROR_INTERNAL, "fileName=" + fileName_ + " end=" + toString(end) + " length=" + toString(logicalLength));
   }

   FileOffset page = 0;
   size_t pageOffset = 0;

   getCurrentPageAndOffset(page, pageOffset);

   size_t n = min( nRead, logicalPageSize - pageOffset );

   /// Allocate temp page buffer
   vector<char> page_buffer_v( physicalPageSize );
   char* page_buffer = &page_buffer_v[0];

   auto   checksumMod = static_cast<unsigned int>( std::nearbyint( 100.0 / checkSumPolicy_ ) );

   while ( nRead > 0 )
   {
      readPhysicalPage( page_buffer, page );

      switch ( checkSumPolicy_ )
      {
         case CHECKSUM_POLICY_NONE:
            break;

         case CHECKSUM_POLICY_ALL:
            verifyChecksum( page_buffer, page );
            break;

         default:
            if ( !(page % checksumMod) || (nRead < physicalPageSize) )
            {
               verifyChecksum( page_buffer, page );
            }
            break;
      }

      memcpy( buf, page_buffer+pageOffset, n );

      buf += n;
      nRead -= n;
      pageOffset = 0;
      ++page;

      n = min( nRead, logicalPageSize );
   }

   /// When done, leave cursor just past end of last byte read
   seek(end, Logical);
}

void CheckedFile::write(const char* buf, size_t nWrite)
{
#ifdef E57_MAX_VERBOSE
   // cout << "write nWrite=" << nWrite << " position()="<< position() << endl; //???
#endif
   if (readOnly_)
   {
      throw E57_EXCEPTION2(E57_ERROR_FILE_IS_READ_ONLY, "fileName=" + fileName_);
   }

   FileOffset end = position(Logical) + nWrite;
   FileOffset page = 0;
   size_t   pageOffset = 0;

   getCurrentPageAndOffset(page, pageOffset);

   size_t n = min(nWrite, logicalPageSize - pageOffset);

   /// Allocate temp page buffer
   vector<char> page_buffer_v(physicalPageSize);
   char* page_buffer = &page_buffer_v[0];

   while (nWrite > 0)
   {
      const FileOffset physicalLength = length( Physical );

      if ( (FileOffset)(page*physicalPageSize) < physicalLength )
      {
         readPhysicalPage( page_buffer, page );
      }

#ifdef E57_MAX_VERBOSE
      // cout << "  page_buffer[0] read: '" << page_buffer[0] << "'" << endl;
      // cout << "copy " << n << "bytes to page=" << page << " pageOffset=" << pageOffset << " buf='"; //???
      //for (size_t i=0; i < n; i++)
         // cout << buf[i];
         // cout << "'" << endl;
#endif
      memcpy(page_buffer+pageOffset, buf, n);
      writePhysicalPage(page_buffer, page);
#ifdef E57_MAX_VERBOSE
      // cout << "  page_buffer[0] after write: '" << page_buffer[0] << "'" << endl; //???
#endif
      buf += n;
      nWrite -= n;
      pageOffset = 0;
      page++;
      n = min(nWrite, logicalPageSize);
   }

   if (end > logicalLength_)
   {
      logicalLength_ = end;
   }

   /// When done, leave cursor just past end of buf
   seek(end, Logical);
}

CheckedFile& CheckedFile::operator<<(const ustring& s)
{
   write(s.c_str(), s.length()); //??? should be times size of uchar?
   return(*this);
}

CheckedFile& CheckedFile::operator<<(int64_t i)
{
   stringstream ss;
   ss << i;
   return(*this << ss.str());
}

CheckedFile& CheckedFile::operator<<(uint64_t i)
{
   stringstream ss;
   ss << i;
   return(*this << ss.str());
}

CheckedFile& CheckedFile::operator<<(float f)
{
   //??? is 7 digits right number?
   return writeFloatingPoint( f, 7 );
}

CheckedFile& CheckedFile::operator<<(double d)
{
   //??? is 17 digits right number?
   return writeFloatingPoint( d, 17 );
}

template<class FTYPE> CheckedFile& CheckedFile::writeFloatingPoint(FTYPE value, int precision)
{
#ifdef E57_MAX_VERBOSE
   cout << "CheckedFile::writeFloatingPoint, value=" << value << " precision=" << precision << endl;
#endif

   stringstream ss;
   ss << scientific << setprecision(precision) << value;

   /// Try to remove trailing zeroes and decimal point
   /// E.g. 1.23456000000000000e+005  ==> 1.23456e+005
   /// E.g. 2.00000000000000000e+005  ==> 2e+005

   ustring s = ss.str();
   const size_t len = s.length();

#ifdef E57_MAX_DEBUG
   ustring old_s = s;
#endif

   /// Split into mantissa and exponent
   /// E.g. 1.23456000000000000e+005  ==> "1.23456000000000000" + "e+005"
   ustring mantissa = s.substr(0, len-5);
   ustring exponent = s.substr(len-5, 5);

   /// Double check that we understand the formatting
   if (exponent[0] == 'e')
   {
      /// Trim of any trailing zeros in mantissa
      while (mantissa[mantissa.length()-1] == '0')
      {
         mantissa = mantissa.substr(0, mantissa.length()-1);
      }

      /// Make one attempt to trim off trailing decimal point
      if (mantissa[mantissa.length()-1] == '.')
      {
         mantissa = mantissa.substr(0, mantissa.length()-1);
      }

      /// Reassemble whole floating point number
      /// Check if can drop exponent.
      if (exponent == "e+000")
      {
         s = mantissa;
      }
      else
      {
         s = mantissa + exponent;
      }
   }

   // Disable these checks because they compare floats using "!=" which is invalid
#if 0 //E57_MAX_DEBUG
   /// Double check same value
   FTYPE old_value = static_cast<FTYPE>(atof(old_s.c_str()));
   FTYPE new_value = static_cast<FTYPE>(atof(s.c_str()));
   if (old_value != new_value)
      throw E57_EXCEPTION2(E57_ERROR_INTERNAL, "fileName=" + fileName_ + " oldValue=" + toString(old_value) + " newValue=" + toString(new_value));
   if (new_value != value)
      throw E57_EXCEPTION2(E57_ERROR_INTERNAL, "fileName=" + fileName_ + " newValue=" + toString(new_value) + " value=" + toString(value));
#endif

   return(*this << s);
}

void CheckedFile::seek(FileOffset offset, OffsetMode omode)
{
    //??? check for seek beyond logicalLength_
    FileOffset pos = (omode==Physical ?  offset : logicalToPhysical(offset));
    portableSeek(pos, SEEK_SET);
}

FileOffset CheckedFile::portableSeek(FileOffset offset, int whence)
{
   FileOffset result;
#ifdef _WIN32
   result = _lseeki64(fd_, offset, whence);
#else
   result = ::lseek(fd_, offset, whence);
#endif
   if (result < 0)
   {
      throw E57_EXCEPTION2(E57_ERROR_LSEEK_FAILED,
                           "fileName=" + fileName_
                           + " offset=" + toString(offset)
                           + " whence=" + toString(whence)
                           + " result=" + toString(result));
   }

   return result;
}

FileOffset CheckedFile::position(OffsetMode omode)
{
   /// Get current file cursor position
   const FileOffset pos = portableSeek(0LL, SEEK_CUR);

   if ( omode == Physical )
   {
      return pos;
   }

   return physicalToLogical( pos );
}

FileOffset CheckedFile::length( OffsetMode omode )
{
   if ( omode == Physical )
   {
      if ( readOnly_ )
      {
         return physicalLength_;
      }

      // Current file position
      FileOffset original_pos = portableSeek( 0LL, SEEK_CUR );

      // End file position
      FileOffset end_pos = portableSeek( 0LL, SEEK_END );

      // Restore original position
      portableSeek( original_pos, SEEK_SET );

      return end_pos;
   }

   return logicalLength_;
}

void CheckedFile::extend(FileOffset newLength, OffsetMode omode)
{
#ifdef E57_MAX_VERBOSE
   // cout << "extend newLength=" << newLength << " omode="<< omode << endl; //???
#endif
   if (readOnly_)
   {
      throw E57_EXCEPTION2(E57_ERROR_FILE_IS_READ_ONLY, "fileName=" + fileName_);
   }

   FileOffset newLogicalLength = 0;

   if (omode==Physical)
   {
      newLogicalLength = physicalToLogical(newLength);
   }
   else
   {
      newLogicalLength = newLength;
   }

   FileOffset currentLogicalLength = length(Logical);

   /// Make sure we are trying to make file longer
   if (newLogicalLength < currentLogicalLength) {
      throw E57_EXCEPTION2(E57_ERROR_INTERNAL,
                           "fileName=" + fileName_
                           + " newLength=" + toString(newLogicalLength)
                           + " currentLength=" + toString(currentLogicalLength));
   }

   /// Calc how may zero bytes we have to add to end
   FileOffset nWrite = newLogicalLength - currentLogicalLength;

   /// Seek to current end of file
   seek(currentLogicalLength, Logical);

   FileOffset page = 0;
   size_t   pageOffset = 0;

   getCurrentPageAndOffset(page, pageOffset);

   /// Calc first write size (may be partial page)
   /// Watch out for different int sizes here.
   size_t n = 0;

   if (nWrite < (FileOffset)(logicalPageSize - pageOffset))
   {
      n = static_cast<size_t>(nWrite);
   }
   else
   {
      n = logicalPageSize - pageOffset;
   }

   /// Allocate temp page buffer
   vector<char> page_buffer_v(physicalPageSize);
   char* page_buffer = &page_buffer_v[0];

   while (nWrite > 0)
   {
      const FileOffset physicalLength = length( Physical );

      if ( (FileOffset)(page*physicalPageSize) < physicalLength )
      {
         readPhysicalPage( page_buffer, page );
      }

#ifdef E57_MAX_VERBOSE
      // cout << "extend " << n << "bytes on page=" << page << " pageOffset=" << pageOffset << endl; //???
#endif
      memset(page_buffer+pageOffset, 0, n);
      writePhysicalPage(page_buffer, page);

      nWrite -= n;
      pageOffset = 0;
      ++page;

      if (nWrite < (FileOffset)logicalPageSize)
      {
         n = static_cast<size_t>(nWrite);
      }
      else
      {
         n = logicalPageSize;
      }
   }

   //??? what if loop above throws, logicalLength_ may be wrong
   logicalLength_ = newLogicalLength;

   /// When done, leave cursor at end of file
   seek(newLogicalLength, Logical);
}

void CheckedFile::close()
{
   if (fd_ >= 0)
   {
#if defined(_MSC_VER)
      int result = ::_close(fd_);
#elif defined(__GNUC__)
      int result = ::close(fd_);
#else
#  error "no supported compiler defined"
#endif
      if (result < 0)
      {
         throw E57_EXCEPTION2(E57_ERROR_CLOSE_FAILED, "fileName=" + fileName_ + " result=" + toString(result));
      }

      fd_ = -1;
   }
}

void CheckedFile::unlink()
{
   close();

   /// Try to unlink the file, don't report a failure
   int result = ::_unlink(fileName_.c_str()); //??? unicode support here
#ifdef E57_MAX_VERBOSE
   if (result < 0)
   {
      cout << "::unlink() failed, result=" << result << endl;
   }
#endif
}

inline uint32_t swap_uint32( uint32_t val )
{
    val = ((val << 8) & 0xFF00FF00 ) | ((val >> 8) & 0xFF00FF );

    return (val << 16) | (val >> 16);
}

/// Calc CRC32C of given data
uint32_t CheckedFile::checksum(char* buf, size_t size) const
{
   static const CRC::Parameters<crcpp_uint32, 32> sCRCParams{
      0x1EDC6F41,
      0xFFFFFFFF,
      0xFFFFFFFF,
      true,
      true
   };

   static const CRC::Table<crcpp_uint32, 32>   sCRCTable = sCRCParams.MakeTable();

   auto crc = CRC::Calculate<crcpp_uint32, 32>( buf, size, sCRCTable );

   // (Andy) I don't understand why we need to swap bytes here
   crc = swap_uint32( crc );

   return crc;
}

void CheckedFile::verifyChecksum( char *page_buffer, size_t page )
{
   const uint32_t check_sum = checksum( page_buffer, logicalPageSize );
   const uint32_t check_sum_in_page = *reinterpret_cast<uint32_t*>(&page_buffer[logicalPageSize]);

   if ( check_sum_in_page != check_sum )
   {
      const FileOffset physicalLength = length( Physical );

      throw E57_EXCEPTION2(E57_ERROR_BAD_CHECKSUM,
                           "fileName=" + fileName_
                           + " computedChecksum=" + toString( check_sum )
                           + " storedChecksum=" + toString( check_sum_in_page )
                           + " page=" + toString( page )
                           + " length=" + toString( physicalLength ));
   }
}

void CheckedFile::getCurrentPageAndOffset(FileOffset& page,
    size_t& pageOffset, OffsetMode omode)
{
   const FileOffset pos = position( omode );

   if (omode == Physical)
   {
      page = pos >> physicalPageSizeLog2;
      pageOffset = static_cast<size_t>(pos & physicalPageSizeMask);
   }
   else
   {
      page = pos / logicalPageSize;
      pageOffset = static_cast<size_t>(pos - page * logicalPageSize);
   }
}

void CheckedFile::readPhysicalPage(char* page_buffer, FileOffset page)
{
#ifdef E57_MAX_VERBOSE
   // cout << "readPhysicalPage, page:" << page << endl;
#endif

#ifdef E57_CHECK_FILE_DEBUG
   const FileOffset physicalLength = length( Physical );

   assert( page*physicalPageSize < physicalLength );
#endif

   /// Seek to start of physical page
   seek( page*physicalPageSize, Physical );

#if defined(_MSC_VER)
   int result = ::_read( fd_, page_buffer, physicalPageSize );
#elif defined(__GNUC__)
   ssize_t result = ::read( fd_, page_buffer, physicalPageSize );
#else
#  error "no supported compiler defined"
#endif

   if ( result < 0 || static_cast<size_t>(result) != physicalPageSize )
   {
      throw E57_EXCEPTION2(E57_ERROR_READ_FAILED, "fileName=" + fileName_ + " result=" + toString(result));
   }
}

void CheckedFile::writePhysicalPage(char* page_buffer, FileOffset page)
{
#ifdef E57_MAX_VERBOSE
   // cout << "writePhysicalPage, page:" << page << endl;
#endif

   /// Append checksum
   uint32_t check_sum = checksum(page_buffer, logicalPageSize);
   *reinterpret_cast<uint32_t*>(&page_buffer[logicalPageSize]) = check_sum;  //??? little endian dependency

   /// Seek to start of physical page
   seek(page*physicalPageSize, Physical);

#if defined(_MSC_VER)
   int result = ::_write(fd_, page_buffer, physicalPageSize);
#elif defined(__GNUC__)
   ssize_t result = ::write(fd_, page_buffer, physicalPageSize);
#else
#  error "no supported compiler defined"
#endif

   if (result < 0)
   {
      throw E57_EXCEPTION2(E57_ERROR_WRITE_FAILED, "fileName=" + fileName_ + " result=" + toString(result));
   }
}
