/*
  WDL - filewrite.h
  Copyright (C) 2005 and later Cockos Incorporated

  This software is provided 'as-is', without any express or implied
  warranty.  In no event will the authors be held liable for any damages
  arising from the use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
  

  This file provides the WDL_FileWrite object, which can be used to create/write files.
  On windows systems it supports writing synchronously, asynchronously, and asynchronously without buffering.
  On windows systems it supports files larger than 4gb.
  On non-windows systems it acts as a wrapper for fopen()/etc.


*/


#ifndef _WDL_FILEWRITE_H_
#define _WDL_FILEWRITE_H_

#ifndef WDL_FILEWRITE_ON_ERROR
#define WDL_FILEWRITE_ON_ERROR(is_full)
#endif

#include "ptrlist.h"



#if defined(_WIN32) && !defined(WDL_NO_WIN32_FILEWRITE)
  #ifndef WDL_WIN32_NATIVE_WRITE
    #define WDL_WIN32_NATIVE_WRITE
  #endif
#else
  #ifdef WDL_WIN32_NATIVE_WRITE
    #undef WDL_WIN32_NATIVE_WRITE
  #endif
  #if !defined(WDL_NO_POSIX_FILEWRITE)
    #include <fcntl.h>
    #include <sys/file.h>
    #include <sys/stat.h>
    #include <errno.h>
    #define WDL_POSIX_NATIVE_WRITE
    extern struct stat wdl_stat_chk;
    // if this fails on linux, use CFLAGS += -D_FILE_OFFSET_BITS=64
    typedef char wdl_filewrite_assert_failed_stat_not_64[sizeof(wdl_stat_chk.st_size)!=8 ? -1 : 1];
    typedef char wdl_filewrite_assert_failed_off_t_64[sizeof(off_t)!=8 ? -1 : 1];
  #endif
#endif



#ifdef _MSC_VER
#define WDL_FILEWRITE_POSTYPE __int64
#else
#define WDL_FILEWRITE_POSTYPE long long
#endif

class WDL_FileWrite
{
#ifdef WDL_WIN32_NATIVE_WRITE

class WDL_FileWrite__WriteEnt
{
public:
  WDL_FileWrite__WriteEnt(int sz)
  {
    m_last_writepos=0;
    m_bufused=0;
    m_bufsz=sz;
    m_bufptr = (char *)__buf.Resize(sz+4095);
    int a=((int)(INT_PTR)m_bufptr)&4095;
    if (a) m_bufptr += 4096-a;

    memset(&m_ol,0,sizeof(m_ol));
    m_ol.hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);
  }
  ~WDL_FileWrite__WriteEnt()
  {
    CloseHandle(m_ol.hEvent);
  }

  WDL_FILEWRITE_POSTYPE m_last_writepos;

  int m_bufused,m_bufsz;
  OVERLAPPED m_ol;
  char *m_bufptr;
  WDL_TypedBuf<char> __buf;
};

#endif

#if defined(_WIN32) && !defined(WDL_NO_SUPPORT_UTF8)
  BOOL HasUTF8(const char *_str)
  {
    const unsigned char *str = (const unsigned char *)_str;
    if (!str) return FALSE;
    while (*str) 
    {
      unsigned char c = *str;
      if (c >= 0xC2)
      {
        if (c <= 0xDF && str[1] >=0x80 && str[1] <= 0xBF) return TRUE;
        else if (c <= 0xEF && str[1] >=0x80 && str[1] <= 0xBF && str[2] >=0x80 && str[2] <= 0xBF) return TRUE;
        else if (c <= 0xF4 && str[1] >=0x80 && str[1] <= 0xBF && str[2] >=0x80 && str[2] <= 0xBF) return TRUE;
      }
      str++;
      if (((const char *)str-_str) >= 256) return TRUE; // long filenames get converted to wide
    }
    return FALSE;
  }
#endif


public:
  // async==2 is write-through
  // async==3 is non-buffered (win32-only)
  WDL_FileWrite(const char *filename, int allow_async=1, int bufsize=8192, int minbufs=16, int maxbufs=16, bool wantAppendTo=false, bool noFileLocking=false) 
  {
    m_file_position=0;
    m_file_max_position=0;
    if(!filename)
    {
#ifdef WDL_WIN32_NATIVE_WRITE
      m_fh = INVALID_HANDLE_VALUE;
      m_async = 0;
#elif defined(WDL_POSIX_NATIVE_WRITE)
      m_filedes_locked=false;
      m_filedes=-1;
      m_bufspace_used=0;
#else
      m_fp = NULL;
#endif
      return;
    }

#ifdef WDL_WIN32_NATIVE_WRITE
    #ifdef WDL_SUPPORT_WIN9X
    const bool isNT = (GetVersion()<0x80000000);
    #else
    const bool isNT = true;
    #endif
    m_async = allow_async && isNT ? 1 : 0;
    if (m_async && allow_async == 3 && !wantAppendTo)
    {
      m_async = 3;
      bufsize = (bufsize+4095)&~4095;
      if (bufsize<4096) bufsize=4096;
    }

    int rwflag = GENERIC_WRITE;
    int createFlag= wantAppendTo?OPEN_ALWAYS:CREATE_ALWAYS;
    int shareFlag = noFileLocking ? (FILE_SHARE_READ|FILE_SHARE_WRITE) : FILE_SHARE_READ;
    int flag = FILE_ATTRIBUTE_NORMAL;

    if (m_async)
    {
      rwflag |= GENERIC_READ;
      if (m_async == 3)
        flag |= FILE_FLAG_OVERLAPPED|FILE_FLAG_NO_BUFFERING|FILE_FLAG_WRITE_THROUGH;
      else
        flag |= FILE_FLAG_OVERLAPPED|(allow_async>1 ? FILE_FLAG_WRITE_THROUGH: 0);
    }

    {
#ifndef WDL_NO_SUPPORT_UTF8
      bool allow_ansi = true;
      m_fh=INVALID_HANDLE_VALUE;
      if (isNT && HasUTF8(filename))
      {
        int szreq=MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,filename,-1,NULL,0);
        if (szreq > 1000)
        {
          WDL_TypedBuf<WCHAR> wfilename;
          wfilename.Resize(szreq+20);
          if (MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,filename,-1,wfilename.Get(),wfilename.GetSize()-10))
          {
            correctlongpath(wfilename.Get());
            allow_ansi = false;
            m_fh = CreateFileW(wfilename.Get(),rwflag,shareFlag,NULL,createFlag,flag,NULL);
          }
        }
        else
        {
          WCHAR wfilename[1024];
          if (MultiByteToWideChar(CP_UTF8,MB_ERR_INVALID_CHARS,filename,-1,wfilename,1024-10))
          {
            correctlongpath(wfilename);
            allow_ansi = false;
            m_fh = CreateFileW(wfilename,rwflag,shareFlag,NULL,createFlag,flag,NULL);
          }
        }
      }
      
      if (m_fh == INVALID_HANDLE_VALUE && allow_ansi)
#endif
        m_fh = CreateFileA(filename,rwflag,shareFlag,NULL,createFlag,flag,NULL);
    }

    if (m_async && m_fh != INVALID_HANDLE_VALUE)
    {
      m_async_bufsize=bufsize;
      m_async_maxbufs=maxbufs;
      m_async_minbufs=minbufs;
      int x;
      for (x = 0; x < m_async_minbufs; x ++)
      {
        WDL_FileWrite__WriteEnt *t=new WDL_FileWrite__WriteEnt(m_async_bufsize);
        m_empties.Add(t);
      }
    }

    if (m_fh != INVALID_HANDLE_VALUE && wantAppendTo)
      SetPosition(GetSize());
 
#elif defined(WDL_POSIX_NATIVE_WRITE)
    m_bufspace_used=0;
    m_filedes_locked=false;
    m_filedes=open(filename,O_WRONLY|O_CREAT
        // todo: use fcntl() for platforms when O_CLOEXEC is not available (if we ever need to support them)
        // (currently the only platform that meets this criteria is macOS w/ old SDK, but we don't use execve()
        // there
#ifdef O_CLOEXEC
        | O_CLOEXEC
#endif
        ,0644);
    if (m_filedes>=0)
    {

      if (!noFileLocking)
      {
        m_filedes_locked = !flock(m_filedes,LOCK_EX|LOCK_NB);
        if (!m_filedes_locked)
        {
          // this check might not be necessary, it might be sufficient to just fail and close if no exclusive lock possible
          if (errno == EWOULDBLOCK)  
          {
            // FAILED exclusive locking because someone else has a lock
            close(m_filedes); 
            m_filedes=-1;
          }
          else  // failed for some other reason, try to keep a shared lock at least
          {
            m_filedes_locked = !flock(m_filedes,LOCK_SH|LOCK_NB);
          }
        }
      }

      if (m_filedes>=0)
      {
        if (!wantAppendTo)
        {
          if (ftruncate(m_filedes,0) < 0)
          {
            WDL_ASSERT( false /* ftruncate() failed in WDL_FileWrite */ );
          }
        }
        else
        {
          struct stat st;
          if (!fstat(m_filedes,&st))  SetPosition(st.st_size);
        }
      }

      
#ifdef __APPLE__
      if (m_filedes >= 0 && allow_async>1) fcntl(m_filedes,F_NOCACHE,1);
#endif
    }
    if (minbufs * bufsize >= 16384) m_bufspace.Resize((minbufs*bufsize+4095)&~4095);
#else
    m_fp=fopen(filename,wantAppendTo ? "a+b" : "wb");
    if (wantAppendTo && m_fp) 
      fseek(m_fp,0,SEEK_END);
#endif
  }

  ~WDL_FileWrite()
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    // todo, async close stuff?
    if (m_fh != INVALID_HANDLE_VALUE && m_async)
    {
      SyncOutput(true);
    }

    m_empties.Empty(true);
    m_pending.Empty(true);

    if (m_fh != INVALID_HANDLE_VALUE) CloseHandle(m_fh);
    m_fh=INVALID_HANDLE_VALUE;
#elif defined(WDL_POSIX_NATIVE_WRITE)
   if (m_filedes >= 0)
   {
     if (m_bufspace.GetSize() > 0 && m_bufspace_used>0)
     {
       int v=(int)pwrite(m_filedes,m_bufspace.Get(),m_bufspace_used,m_file_position);
       if (v>0) m_file_position+=v;
       if (m_file_position > m_file_max_position) m_file_max_position=m_file_position;
       m_bufspace_used=0;
     }
     if (m_filedes_locked) flock(m_filedes,LOCK_UN);
     close(m_filedes);
   }
   m_filedes=-1;
   
#else
    if (m_fp) fclose(m_fp);
    m_fp=0;
#endif

  }

  bool IsOpen()
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    return (m_fh != INVALID_HANDLE_VALUE);
#elif defined(WDL_POSIX_NATIVE_WRITE)
    return m_filedes >= 0;
#else
    return m_fp != NULL;
#endif
  }


  int Write(const void *buf, int len)
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    if (m_fh == INVALID_HANDLE_VALUE) return 0;

    if (m_async)
    {
      int rdpos = 0;
      while (len > 0)
      {
        if (!m_empties.GetSize()) 
        {
          WDL_FileWrite__WriteEnt *ent=m_pending.Get(0);
          DWORD s=0;
          if (ent)
          {
            bool wasabort=false;
            DWORD err;
            if (GetOverlappedResult(m_fh,&ent->m_ol,&s,FALSE)||
                (wasabort=((err=GetLastError())==ERROR_OPERATION_ABORTED))) 
            {
              m_pending.Delete(0);

              if (wasabort) 
              {
                if (!RunAsyncWrite(ent,false)) m_empties.Add(ent);
              }
              else
              {
                m_empties.Add(ent);
                ent->m_bufused=0;
              }
            }
            else if (err != ERROR_IO_PENDING && err != ERROR_IO_INCOMPLETE)
            {
              WDL_FILEWRITE_ON_ERROR(err == ERROR_DISK_FULL)
            }
          }
        }


        WDL_FileWrite__WriteEnt *ent=m_empties.Get(0);
        if (!ent) 
        {
          if (m_pending.GetSize()>=m_async_maxbufs)
          {
            SyncOutput(false);
          }

          if (!(ent=m_empties.Get(0))) 
            m_empties.Add(ent = new WDL_FileWrite__WriteEnt(m_async_bufsize)); // new buffer

          
        }

        int ml=ent->m_bufsz-ent->m_bufused;
        if (ml>len) ml=len;
        memcpy(ent->m_bufptr+ent->m_bufused,(const char *)buf + rdpos,ml);

        ent->m_bufused+=ml;
        len-=ml;
        rdpos+=ml;

        if (ent->m_bufused >= ent->m_bufsz)
        {
          if (RunAsyncWrite(ent,true)) m_empties.Delete(0); // if queued remove from list
        }
      }
      return rdpos; 
    }
    else
    {
      DWORD dw=0;
      if (!WriteFile(m_fh,buf,len,&dw,NULL))
      {
        WDL_FILEWRITE_ON_ERROR(GetLastError() == ERROR_DISK_FULL)
      }
      m_file_position+=dw;
      if (m_file_position>m_file_max_position) m_file_max_position=m_file_position;
      return dw;
    }
#elif defined(WDL_POSIX_NATIVE_WRITE)
   if (m_bufspace.GetSize()>0)
   {
     char *rdptr = (char *)buf;
     int rdlen = len;
     while (rdlen>0)
     {
       int amt = m_bufspace.GetSize() - m_bufspace_used;
       if (amt>0)
       {
         if (amt>rdlen) amt=rdlen;
         memcpy((char *)m_bufspace.Get()+m_bufspace_used,rdptr,amt);
         m_bufspace_used += amt;
         rdptr+=amt;
         rdlen -= amt;

         if (m_file_position+m_bufspace_used > m_file_max_position) m_file_max_position=m_file_position + m_bufspace_used;
       }
       if (m_bufspace_used >= m_bufspace.GetSize())
       {
         int v=(int)pwrite(m_filedes,m_bufspace.Get(),m_bufspace_used,m_file_position);
         if (v != m_bufspace_used) { WDL_FILEWRITE_ON_ERROR(v>=0 || errno == EDQUOT || errno == ENOSPC) }
         if (v>0) m_file_position+=v;
         m_bufspace_used=0;
       }
     }    
     return len;
   }
   else
   {
     int v=(int)pwrite(m_filedes,buf,len,m_file_position);
     if (v != len) { WDL_FILEWRITE_ON_ERROR(v>=0 || errno == EDQUOT || errno == ENOSPC) }
     if (v>0) m_file_position+=v;
     if (m_file_position > m_file_max_position) m_file_max_position=m_file_position;
     return v;
   }
#else
   int written = (int)fwrite(buf,1,len,m_fp);
   if (written != len) { WDL_FILEWRITE_ON_ERROR(false) }
   return written;
#endif

    
  }

  WDL_FILEWRITE_POSTYPE GetSize()
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    if (m_fh == INVALID_HANDLE_VALUE) return 0;
    DWORD h=0;
    DWORD l=GetFileSize(m_fh,&h);
    WDL_FILEWRITE_POSTYPE tmp=(((WDL_FILEWRITE_POSTYPE)h)<<32)|l;
    WDL_FILEWRITE_POSTYPE tmp2=GetPosition();
    if (tmp<m_file_max_position) return m_file_max_position;
    if (tmp<tmp2) return tmp2;
    
    return tmp;
#elif defined(WDL_POSIX_NATIVE_WRITE)
    if (m_filedes < 0) return -1;
    return m_file_max_position;
#else
    if (!m_fp) return -1;
    int opos=ftell(m_fp);
    fseek(m_fp,0,SEEK_END);
    int a=ftell(m_fp);
    fseek(m_fp,opos,SEEK_SET);
    return a;

#endif
  }

  WDL_FILEWRITE_POSTYPE GetPosition()
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    if (m_fh == INVALID_HANDLE_VALUE) return -1;

    WDL_FILEWRITE_POSTYPE pos=m_file_position;
    if (m_async)
    {
      WDL_FileWrite__WriteEnt *ent=m_empties.Get(0);
      if (ent) pos+=ent->m_bufused;
    }
    return pos;
#elif defined(WDL_POSIX_NATIVE_WRITE)
    if (m_filedes < 0) return -1;
    return m_file_position + m_bufspace_used;
#else
    if (!m_fp) return -1;
    return ftell(m_fp);

#endif
  }

#ifdef WDL_WIN32_NATIVE_WRITE

  bool RunAsyncWrite(WDL_FileWrite__WriteEnt *ent, bool updatePosition) // returns true if ent is added to pending
  {
    if (ent && ent->m_bufused>0) 
    {
      if (updatePosition) 
      {
        ent->m_last_writepos = m_file_position;
        m_file_position += ent->m_bufused;
        if (m_file_position>m_file_max_position) m_file_max_position=m_file_position;
      }

      if (m_async == 3 && (ent->m_bufused&4095))
      {
        int offs=(ent->m_bufused&4095);
        char tmp[4096];
        memset(tmp,0,4096);

        *(WDL_FILEWRITE_POSTYPE *)&ent->m_ol.Offset = ent->m_last_writepos + ent->m_bufused - offs;
        ResetEvent(ent->m_ol.hEvent);

        DWORD dw=0;
        if (!ReadFile(m_fh,tmp,4096,&dw,&ent->m_ol))
        {
          if (GetLastError() == ERROR_IO_PENDING) 
            WaitForSingleObject(ent->m_ol.hEvent,INFINITE);
        }
        memcpy(ent->m_bufptr+ent->m_bufused,tmp+offs,4096-offs);

        ent->m_bufused += 4096-offs;
      }

      DWORD d=0;

      *(WDL_FILEWRITE_POSTYPE *)&ent->m_ol.Offset = ent->m_last_writepos;

      ResetEvent(ent->m_ol.hEvent);

      if (!WriteFile(m_fh,ent->m_bufptr,ent->m_bufused,&d,&ent->m_ol))
      {
        if (GetLastError()==ERROR_IO_PENDING)
        {
          m_pending.Add(ent);
          return true;
        }
        else { WDL_FILEWRITE_ON_ERROR(GetLastError()==ERROR_DISK_FULL) }
      }
      ent->m_bufused=0;
    }
    return false;
  }

  void SyncOutput(bool syncall)
  {
    if (syncall)
    {
      if (RunAsyncWrite(m_empties.Get(0),true)) m_empties.Delete(0);
    }
    for (;;)
    {
      WDL_FileWrite__WriteEnt *ent=m_pending.Get(0);
      if (!ent) break;
      DWORD s=0;
      m_pending.Delete(0);
      BOOL ok = GetOverlappedResult(m_fh,&ent->m_ol,&s,TRUE);
      int errcode;
      if (!ok && (errcode=GetLastError())==ERROR_OPERATION_ABORTED)
      {
        // rewrite this one
        if (!RunAsyncWrite(ent,false)) m_empties.Add(ent);
      }
      else
      {
        if (!ok) { WDL_FILEWRITE_ON_ERROR(errcode==ERROR_DISK_FULL) }
        m_empties.Add(ent);
        ent->m_bufused=0;
        if (!syncall) break;
      }
    }
  }

#endif


  bool SetPosition(WDL_FILEWRITE_POSTYPE pos) // returns 0 on success
  {
#ifdef WDL_WIN32_NATIVE_WRITE
    if (m_fh == INVALID_HANDLE_VALUE) return true;
    if (m_async)
    {
      SyncOutput(true);
      m_file_position=pos;
      if (m_file_position>m_file_max_position) m_file_max_position=m_file_position;

      if (m_async==3 && (m_file_position&4095))
      {
        WDL_FileWrite__WriteEnt *ent=m_empties.Get(0);
        if (ent)
        {
          int psz=(int) (m_file_position&4095);

          m_file_position -= psz;
          *(WDL_FILEWRITE_POSTYPE *)&ent->m_ol.Offset = m_file_position;
          ResetEvent(ent->m_ol.hEvent);

          DWORD dwo=0;
          if (!ReadFile(m_fh,ent->m_bufptr,4096,&dwo,&ent->m_ol))
          {
            if (GetLastError() == ERROR_IO_PENDING) 
              WaitForSingleObject(ent->m_ol.hEvent,INFINITE);
          }
          ent->m_bufused=(int)psz;
        }
      }
      return false;
    }

    m_file_position=pos;
    if (m_file_position>m_file_max_position) m_file_max_position=m_file_position;

    LONG high=(LONG) (m_file_position>>32);
    return SetFilePointer(m_fh,(LONG)(m_file_position&((WDL_FILEWRITE_POSTYPE)0xFFFFFFFF)),&high,FILE_BEGIN)==0xFFFFFFFF && GetLastError() != NO_ERROR;
#elif defined(WDL_POSIX_NATIVE_WRITE)

    if (m_filedes < 0) return true;
    if (m_bufspace.GetSize() > 0 && m_bufspace_used>0)
    {
      int v=(int)pwrite(m_filedes,m_bufspace.Get(),m_bufspace_used,m_file_position);
      if (v>0) m_file_position+=v;
      if (m_file_position > m_file_max_position) m_file_max_position=m_file_position;
      m_bufspace_used=0;
    }

    m_file_position = pos; // seek!
    if (m_file_position>m_file_max_position) m_file_max_position=m_file_position;
    return false;
#else
    if (!m_fp) return true;
    return !!fseek(m_fp,pos,SEEK_SET);
#endif
  }

  WDL_FILEWRITE_POSTYPE m_file_position, m_file_max_position;

#ifdef WDL_WIN32_NATIVE_WRITE
  HANDLE GetHandle() { return m_fh; }
  HANDLE m_fh;
  int m_async; // 3 = unbuffered

  int m_async_bufsize, m_async_minbufs, m_async_maxbufs;

  WDL_PtrList<WDL_FileWrite__WriteEnt> m_empties;
  WDL_PtrList<WDL_FileWrite__WriteEnt> m_pending;

#elif defined(WDL_POSIX_NATIVE_WRITE)
  int GetHandle() { return m_filedes; }

  WDL_HeapBuf m_bufspace;
  int m_bufspace_used;
  int m_filedes;

  bool m_filedes_locked;

#else
  int GetHandle() { return fileno(m_fp); }
 
  FILE *m_fp;
#endif

#ifdef _WIN32
  static void correctlongpath(WCHAR *buf) // this also exists as wdl_utf8_correctlongpath
  {
    const WCHAR *insert;
    WCHAR *wr;
    int skip = 0;
    if (!buf || !buf[0] || wcslen(buf) < 256) return;
    if (buf[1] == ':') insert=L"\\\\?\\";
    else if (buf[0] == '\\' && buf[1] == '\\') { insert = L"\\\\?\\UNC\\"; skip=2; }
    else return;

    wr = buf + wcslen(insert);
    memmove(wr, buf + skip, (wcslen(buf+skip)+1)*2);
    memmove(buf,insert,wcslen(insert)*2);
    while (*wr)
    {
      if (*wr == '/') *wr = '\\';
      wr++;
    }
  }
#endif
} WDL_FIXALIGN;






#endif
