/* -----------------------------------------------------------------------------
 *
 * (c) Tamar Christina 2018-2019
 *
 * Windows I/O routines for file opening.
 *
 * NOTE: Only modify this file in utils/fs/ and rerun configure. Do not edit
 *       this file in any other directory as it will be overwritten.
 *
 * ---------------------------------------------------------------------------*/
#include "fs.h"
#include <stdio.h>

#if defined(_WIN32)

#include <stdbool.h>
#include <stdlib.h>
#include <stdint.h>

#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <wchar.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <share.h>
#include <errno.h>

/* Duplicate a string, but in wide form. The caller is responsible for freeing
   the result. */
static wchar_t* FS(to_wide) (const char *path) {
  size_t len = mbstowcs (NULL, path, 0);
  wchar_t *w_path = malloc (sizeof (wchar_t) * (len + 1));
  mbstowcs (w_path, path, len);
  w_path[len] = L'\0';
  return w_path;
}

/* This function converts Windows paths between namespaces. More specifically
   It converts an explorer style path into a NT or Win32 namespace.
   This has several caveats but they are caveats that are native to Windows and
   not POSIX. See
   https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx.
   Anything else such as raw device paths we leave untouched.  The main benefit
   of doing any of this is that we can break the MAX_PATH restriction and also
   access raw handles that we couldn't before.

   The resulting string is dynamically allocated and so callers are expected to
   free this string.  */
wchar_t* FS(create_device_name) (const wchar_t* filename) {
  const wchar_t* win32_dev_namespace  = L"\\\\.\\";
  const wchar_t* win32_file_namespace = L"\\\\?\\";
  const wchar_t* nt_device_namespace  = L"\\Device\\";
  const wchar_t* unc_prefix           = L"UNC\\";
  const wchar_t* network_share        = L"\\\\";

  wchar_t* result = _wcsdup (filename);
  wchar_t ns[10] = {0};

  /* If the file is already in a native namespace don't change it.  */
  if (   wcsncmp (win32_dev_namespace , filename, 4) == 0
      || wcsncmp (win32_file_namespace, filename, 4) == 0
      || wcsncmp (nt_device_namespace , filename, 8) == 0)
    return result;

  /* Since we're using the lower level APIs we must normalize slashes now.  The
     Win32 API layer will no longer convert '/' into '\\' for us.  */
  for (size_t i = 0; i < wcslen (result); i++)
    {
      if (result[i] == L'/')
        result[i] = L'\\';
    }

  /* We need to expand dos short paths as well.  */
  DWORD nResult = GetLongPathNameW (result, NULL, 0) + 1;
  wchar_t* temp = NULL;
  if (nResult > 1)
    {
      temp = _wcsdup (result);
      free (result);
      result = malloc (nResult * sizeof (wchar_t));
      if (GetLongPathNameW (temp, result, nResult) == 0)
        {
          result = memcpy (result, temp, wcslen (temp));
          goto cleanup;
        }
      free (temp);
    }

  /* Now resolve any . and .. in the path or subsequent API calls may fail since
     Win32 will no longer resolve them.  */
  nResult = GetFullPathNameW (result, 0, NULL, NULL) + 1;
  temp = _wcsdup (result);
  free (result);
  result = malloc (nResult * sizeof (wchar_t));
  if (GetFullPathNameW (temp, nResult, result, NULL) == 0)
    {
      result = memcpy (result, temp, wcslen (temp));
      goto cleanup;
    }

  free (temp);

  int startOffset = 0;
  /* When remapping a network share, \\foo needs to become
     \\?\UNC\foo and not \\?\\UNC\\foo which is an invalid path.  */
  if (wcsncmp (network_share, result, 2) == 0)
    {
      if (swprintf (ns, 10, L"%ls%ls", win32_file_namespace, unc_prefix) <= 0)
        {
          goto cleanup;
        }
      startOffset = 2;
    }
  else if (swprintf (ns, 10, L"%ls", win32_file_namespace) <= 0)
    {
      goto cleanup;
    }

  /* Create new string.  */
  int bLen = wcslen (result) + wcslen (ns) + 1 - startOffset;
  temp = _wcsdup (result + startOffset);
  free (result);
  result = malloc (bLen * sizeof (wchar_t));
  if (swprintf (result, bLen, L"%ls%ls", ns, temp) <= 0)
    {
      goto cleanup;
    }

  free (temp);

  return result;

cleanup:
  free (temp);
  free (result);
  return NULL;
}

static int setErrNoFromWin32Error (void);
/* Sets errno to the right error value and returns -1 to indicate the failure.
   This function should only be called when the creation of the fd actually
   failed and you want to return -1 for the fd.  */
static
int setErrNoFromWin32Error () {
  switch (GetLastError()) {
    case ERROR_SUCCESS:
      errno = 0;
      break;
    case ERROR_ACCESS_DENIED:
    case ERROR_FILE_READ_ONLY:
      errno = EACCES;
      break;
    case ERROR_FILE_NOT_FOUND:
    case ERROR_PATH_NOT_FOUND:
      errno = ENOENT;
      break;
    case ERROR_FILE_EXISTS:
      errno = EEXIST;
      break;
    case ERROR_NOT_ENOUGH_MEMORY:
    case ERROR_OUTOFMEMORY:
      errno = ENOMEM;
      break;
    case ERROR_INVALID_HANDLE:
      errno = EBADF;
      break;
    case ERROR_INVALID_FUNCTION:
      errno = EFAULT;
      break;
    default:
      errno = EINVAL;
      break;
  }
  return -1;
}


#define HAS_FLAG(a,b) (((a) & (b)) == (b))

int FS(swopen) (const wchar_t* filename, int oflag, int shflag, int pmode)
{
  /* Construct access mode.  */
  /* https://docs.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants  */
  DWORD dwDesiredAccess = 0;
  if (HAS_FLAG (oflag, _O_RDONLY))
    dwDesiredAccess |= GENERIC_READ;
  if (HAS_FLAG (oflag, _O_RDWR))
    dwDesiredAccess |= GENERIC_WRITE | GENERIC_READ;
  if (HAS_FLAG (oflag, _O_WRONLY))
    dwDesiredAccess |= GENERIC_WRITE;
  if (HAS_FLAG (oflag, _O_APPEND))
    dwDesiredAccess |= FILE_APPEND_DATA;

  /* Construct shared mode.  */
  /* https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants */
  DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE;
  if (HAS_FLAG (shflag, _SH_DENYRW))
    dwShareMode &= ~(FILE_SHARE_READ | FILE_SHARE_WRITE);
  if (HAS_FLAG (shflag, _SH_DENYWR))
    dwShareMode &= ~FILE_SHARE_WRITE;
  if (HAS_FLAG (shflag, _SH_DENYRD))
    dwShareMode &= ~FILE_SHARE_READ;
  if (HAS_FLAG (pmode, _S_IWRITE))
    dwShareMode |= FILE_SHARE_READ | FILE_SHARE_WRITE;
  if (HAS_FLAG (pmode, _S_IREAD))
    dwShareMode |= FILE_SHARE_READ;

  /* Override access mode with pmode if creating file.  */
  if (HAS_FLAG (oflag, _O_CREAT))
    {
      if (HAS_FLAG (pmode, _S_IWRITE))
        dwDesiredAccess |= FILE_GENERIC_WRITE;
      if (HAS_FLAG (pmode, _S_IREAD))
        dwDesiredAccess |= FILE_GENERIC_READ;
    }

  /* Create file disposition.  */
  /* https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea */
  DWORD dwCreationDisposition = 0;
  if (HAS_FLAG (oflag, (_O_CREAT | _O_EXCL)))
    dwCreationDisposition |= CREATE_NEW;
  else if (HAS_FLAG (oflag, _O_TRUNC | _O_CREAT))
    dwCreationDisposition |= CREATE_ALWAYS;
  else if (HAS_FLAG (oflag, _O_TRUNC) && !HAS_FLAG (oflag, O_RDONLY))
    dwCreationDisposition |= TRUNCATE_EXISTING;
  else if (HAS_FLAG (oflag, _O_APPEND | _O_CREAT))
    dwCreationDisposition |= OPEN_ALWAYS;
  else if (HAS_FLAG (oflag, _O_APPEND))
    dwCreationDisposition |= OPEN_EXISTING;
  else if (HAS_FLAG (oflag, _O_CREAT))
    dwCreationDisposition |= OPEN_ALWAYS;
  else
    dwCreationDisposition |= OPEN_EXISTING;

  /* Set file access attributes.  */
  DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
  if (HAS_FLAG (oflag, _O_RDONLY))
    dwFlagsAndAttributes |= 0; /* No special attribute.  */
  if (HAS_FLAG (oflag, _O_TEMPORARY))
    {
      dwFlagsAndAttributes |= FILE_FLAG_DELETE_ON_CLOSE;
      dwShareMode |= FILE_SHARE_DELETE;
    }
  if (HAS_FLAG (oflag, _O_SHORT_LIVED))
    dwFlagsAndAttributes |= FILE_ATTRIBUTE_TEMPORARY;
  if (HAS_FLAG (oflag, _O_RANDOM))
    dwFlagsAndAttributes |= FILE_FLAG_RANDOM_ACCESS;
  if (HAS_FLAG (oflag, _O_SEQUENTIAL))
    dwFlagsAndAttributes |= FILE_FLAG_SEQUENTIAL_SCAN;
  /* Flag is only valid on it's own.  */
  if (dwFlagsAndAttributes != FILE_ATTRIBUTE_NORMAL)
    dwFlagsAndAttributes &= ~FILE_ATTRIBUTE_NORMAL;

  /* Ensure we have shared read for files which are opened read-only. */
  if (HAS_FLAG (dwCreationDisposition, OPEN_EXISTING)
      && ((dwDesiredAccess & (GENERIC_WRITE|GENERIC_READ)) == GENERIC_READ))
    dwShareMode |= FILE_SHARE_READ;

  /* Set security attributes.  */
  SECURITY_ATTRIBUTES securityAttributes;
  ZeroMemory (&securityAttributes, sizeof(SECURITY_ATTRIBUTES));
  securityAttributes.bInheritHandle       = !(oflag & _O_NOINHERIT);
  securityAttributes.lpSecurityDescriptor = NULL;
  securityAttributes.nLength              = sizeof(SECURITY_ATTRIBUTES);

  wchar_t* _filename = FS(create_device_name) (filename);
  if (!_filename)
    return -1;

  HANDLE hResult
    = CreateFileW (_filename, dwDesiredAccess, dwShareMode, &securityAttributes,
                   dwCreationDisposition, dwFlagsAndAttributes, NULL);

  free (_filename);
  if (INVALID_HANDLE_VALUE == hResult)
    return setErrNoFromWin32Error ();

  /* Now we have a Windows handle, we have to convert it to an FD and apply
     the remaining flags.  */
  const int flag_mask = _O_APPEND | _O_RDONLY | _O_TEXT | _O_WTEXT;
  int fd = _open_osfhandle ((intptr_t)hResult, oflag & flag_mask);
  if (-1 == fd)
    return setErrNoFromWin32Error ();

  /* Finally we can change the mode to the requested one.  */
  const int mode_mask = _O_TEXT | _O_BINARY | _O_U16TEXT | _O_U8TEXT | _O_WTEXT;
  if ((oflag & mode_mask) && (-1 == _setmode (fd, oflag & mode_mask)))
    return setErrNoFromWin32Error ();

  return fd;
}

int FS(translate_mode) (const wchar_t* mode)
{
  int oflag = 0;
  int len = wcslen (mode);
  int i;
  #define IS_EXT(X) ((i < (len - 1)) && mode[i+1] == X)

  for (i = 0; i < len; i++)
    {
      switch (mode[i])
        {
          case L'a':
            if (IS_EXT (L'+'))
              oflag |= _O_RDWR | _O_CREAT | _O_APPEND;
            else
              oflag |= _O_WRONLY | _O_CREAT | _O_APPEND;
            break;
          case L'r':
            if (IS_EXT (L'+'))
              oflag |= _O_RDWR;
            else
              oflag |= _O_RDONLY;
            break;
          case L'w':
            if (IS_EXT (L'+'))
              oflag |= _O_RDWR | _O_CREAT | _O_TRUNC;
            else
              oflag |= _O_WRONLY | _O_CREAT | _O_TRUNC;
            break;
          case L'b':
            oflag |= _O_BINARY;
            break;
          case L't':
            oflag |= _O_TEXT;
            break;
          case L'c':
          case L'n':
            oflag |= 0;
            break;
          case L'S':
            oflag |= _O_SEQUENTIAL;
            break;
          case L'R':
            oflag |= _O_RANDOM;
            break;
          case L'T':
            oflag |= _O_SHORT_LIVED;
            break;
          case L'D':
            oflag |= _O_TEMPORARY;
            break;
          default:
            if (wcsncmp (mode, L"ccs=UNICODE", 11) == 0)
              oflag |= _O_WTEXT;
            else if (wcsncmp (mode, L"ccs=UTF-8", 9) == 0)
              oflag |= _O_U8TEXT;
            else if (wcsncmp (mode, L"ccs=UTF-16LE", 12) == 0)
              oflag |= _O_U16TEXT;
            else continue;
        }
    }
  #undef IS_EXT

  return oflag;
}

FILE *FS(fwopen) (const wchar_t* filename, const wchar_t* mode)
{
  int shflag = 0;
  int pmode  = 0;
  int oflag  = FS(translate_mode) (mode);

  int fd = FS(swopen) (filename, oflag, shflag, pmode);
  if (fd < 0)
    return NULL;

  FILE* file = _wfdopen (fd, mode);
  return file;
}

FILE *FS(fopen) (const char* filename, const char* mode)
{
  wchar_t * const w_filename = FS(to_wide) (filename);
  wchar_t * const w_mode = FS(to_wide) (mode);

  FILE *result = FS(fwopen) (w_filename, w_mode);
  free (w_filename);
  free (w_mode);

  return result;
}

int FS(sopen) (const char* filename, int oflag, int shflag, int pmode)
{
  wchar_t * const w_filename = FS(to_wide) (filename);
  int result = FS(swopen) (w_filename, oflag, shflag, pmode);
  free (w_filename);

  return result;
}

int FS(_stat) (const char *path, struct _stat *buffer)
{
  wchar_t * const w_path = FS(to_wide) (path);
  int result = FS(_wstat) (w_path, buffer);
  free (w_path);

  return result;
}

int FS(_stat64) (const char *path, struct __stat64 *buffer)
{
  wchar_t * const w_path = FS(to_wide) (path);
  int result = FS(_wstat64) (w_path, buffer);
  free (w_path);

  return result;
}

static __time64_t ftToPosix(FILETIME ft)
{
  /* takes the last modified date.  */
  LARGE_INTEGER date, adjust;
  date.HighPart = ft.dwHighDateTime;
  date.LowPart = ft.dwLowDateTime;

  /* 100-nanoseconds = milliseconds * 10000.  */
  /* A UNIX timestamp contains the number of seconds from Jan 1, 1970, while the
     FILETIME documentation says: Contains a 64-bit value representing the
     number of 100-nanosecond intervals since January 1, 1601 (UTC).

     Between Jan 1, 1601 and Jan 1, 1970 there are 11644473600 seconds */
  adjust.QuadPart = 11644473600000 * 10000;

  /* removes the diff between 1970 and 1601.  */
  date.QuadPart -= adjust.QuadPart;

  /* converts back from 100-nanoseconds to seconds.  */
  return (__time64_t)date.QuadPart / 10000000;
}

int FS(_wstat) (const wchar_t *path, struct _stat *buffer)
{
  ZeroMemory (buffer, sizeof (struct _stat));
  wchar_t* _path = FS(create_device_name) (path);
  if (!_path)
    return -1;

    /* Construct shared mode.  */
  DWORD dwShareMode = FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE;
  DWORD dwDesiredAccess = FILE_READ_ATTRIBUTES;
  DWORD dwFlagsAndAttributes = FILE_FLAG_BACKUP_SEMANTICS;
  DWORD dwCreationDisposition = OPEN_EXISTING;

  SECURITY_ATTRIBUTES securityAttributes;
  ZeroMemory (&securityAttributes, sizeof(SECURITY_ATTRIBUTES));
  securityAttributes.bInheritHandle       = false;
  securityAttributes.lpSecurityDescriptor = NULL;
  securityAttributes.nLength              = sizeof(SECURITY_ATTRIBUTES);

  HANDLE hResult
    = CreateFileW (_path, dwDesiredAccess, dwShareMode, &securityAttributes,
                   dwCreationDisposition, dwFlagsAndAttributes, NULL);

  if (INVALID_HANDLE_VALUE == hResult)
    {
      free (_path);
      return setErrNoFromWin32Error ();
    }

  WIN32_FILE_ATTRIBUTE_DATA finfo;
  ZeroMemory (&finfo, sizeof (WIN32_FILE_ATTRIBUTE_DATA));
  if(!GetFileAttributesExW (_path, GetFileExInfoStandard, &finfo))
    {
      free (_path);
      CloseHandle (hResult);
      return setErrNoFromWin32Error ();
    }

  unsigned short mode = _S_IREAD;

  if (finfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
    mode |= (_S_IFDIR | _S_IEXEC);
  else
  {
    mode |= _S_IFREG;
    DWORD type;
    if (GetBinaryTypeW (_path, &type))
      mode |= _S_IEXEC;
  }

  if (!(finfo.dwFileAttributes & FILE_ATTRIBUTE_READONLY))
    mode |= _S_IWRITE;

  buffer->st_mode  = mode;
  buffer->st_nlink = 1;
  buffer->st_size  = ((uint64_t)finfo.nFileSizeHigh << 32) + finfo.nFileSizeLow;
  buffer->st_atime = ftToPosix (finfo.ftLastAccessTime);
  buffer->st_mtime = buffer->st_ctime = ftToPosix (finfo.ftLastWriteTime);
  free (_path);
  CloseHandle (hResult);
  return 0;
}

int FS(_wstat64) (const wchar_t *path, struct __stat64 *buffer)
{
  struct _stat buf;
  ZeroMemory (buffer, sizeof (struct __stat64));

  int result = FS(_wstat) (path, &buf);

  buffer->st_mode = buf.st_mode;
  buffer->st_nlink = 1;
  buffer->st_size = buf.st_size;
  buffer->st_atime = buf.st_atime;
  buffer->st_mtime = buf.st_mtime;

  return result;
}

int FS(_wrename) (const wchar_t *from, const wchar_t *to)
{
  wchar_t* const _from = FS(create_device_name) (from);
  if (!_from)
    return -1;

  wchar_t* const _to = FS(create_device_name) (to);
  if (!_to)
    {
      free (_from);
      return -1;
    }

  if (MoveFileW(_from, _to) == 0) {
    free (_from);
    free (_to);
    return setErrNoFromWin32Error ();
  }


  free (_from);
  free (_to);
  return 0;
}

int FS(rename) (const char *from, const char *to)
{
  wchar_t * const w_from = FS(to_wide) (from);
  wchar_t * const w_to = FS(to_wide) (to);
  int result = FS(_wrename) (w_from, w_to);
  free(w_from);
  free(w_to);

  return result;
}

int FS(unlink) (const char *filename)
{
  return FS(_unlink) (filename);
}

int FS(_unlink) (const char *filename)
{
  wchar_t * const w_filename = FS(to_wide) (filename);
  int result = FS(_wunlink) (w_filename);
  free(w_filename);

  return result;
}

int FS(_wunlink) (const wchar_t *filename)
{
  wchar_t* const _filename = FS(create_device_name) (filename);
  if (!_filename)
    return -1;

  if (DeleteFileW(_filename) == 0) {
    free (_filename);
    return setErrNoFromWin32Error ();
  }


  free (_filename);
  return 0;
}
int FS(remove) (const char *path)
{
  return FS(_unlink) (path);
}
int FS(_wremove) (const wchar_t *path)
{
  return FS(_wunlink) (path);
}
#else
FILE *FS(fopen) (const char* filename, const char* mode)
{
  return fopen (filename, mode);
}
#endif
