#include <io.h>
#include <math.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <pthread.h>

#include "iwcfg.h"

#define _NANOSECONDS_PER_TICK   100ULL
#define _NANOSECONDS_PER_SECOND 1000000000ULL
#define _TICKS_PER_SECOND       10000000ULL
#define _TICKS_PER_MILLISECOND  10000ULL
#define _SEC_TO_UNIX_EPOCH      11644473600ULL
#define _TICKS_TO_UNIX_EPOCH    (_TICKS_PER_SECOND * _SEC_TO_UNIX_EPOCH)
#define _TIMESPEC2MS(IW_ts) ((IW_ts).tv_sec * 1000) + (uint64_t) round((IW_ts).tv_nsec / 1.0e6)

IW_INLINE uint64_t _iwp_filetime2ticks(const FILETIME *ft) {
  uint64_t ticks = ((uint64_t) ft->dwHighDateTime << 32) + ft->dwLowDateTime;
  if (ticks < _TICKS_TO_UNIX_EPOCH) {
    return 0;
  }
  ticks -= _TICKS_TO_UNIX_EPOCH;
  return ticks;
}

IW_INLINE void _iwp_filetime2timespec(const FILETIME *ft, struct timespec *spec) {
  uint64_t ticks = _iwp_filetime2ticks(ft);
  spec->tv_sec = ticks / _TICKS_PER_SECOND;
  spec->tv_nsec = (ticks % _TICKS_PER_SECOND) * _NANOSECONDS_PER_TICK;
}

IW_INLINE uint64_t _iwp_filetime2millisecons(const FILETIME *ft) {
  uint64_t ticks = _iwp_filetime2ticks(ft);
  return ticks / _TICKS_PER_MILLISECOND;
}

iwrc iwp_clock_get_time(int clock_id, struct timespec *spec) {
  int rci = clock_gettime(clock_id, spec);
  if (rci) {
    return iwrc_set_errno(IW_ERROR_ERRNO, errno);
  }
  return 0;
}

iwrc iwp_current_time_ms(uint64_t *time, bool monotonic) {
  FILETIME ft;
  GetSystemTimeAsFileTime(&ft);
  *time = _iwp_filetime2millisecons(&ft);
  return 0;
}

iwrc iwp_fsync(HANDLE h) {
  if (INVALIDHANDLE(h)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (!FlushFileBuffers(h)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  return 0;
}

iwrc iwp_fdatasync(HANDLE fh) {
  return iwp_fsync(fh);
}

static SYSTEM_INFO sysinfo;

static void _iwp_getsysinfo() {
  GetSystemInfo(&sysinfo);
}

size_t iwp_page_size(void) {
  return sysinfo.dwPageSize;
}

size_t iwp_alloc_unit(void) {
  return sysinfo.dwAllocationGranularity;
}

uint16_t iwp_num_cpu_cores() {
  return sysinfo.dwNumberOfProcessors;
}

iwrc iwp_ftruncate(HANDLE fh, off_t len) {
  LARGE_INTEGER size;
  size.QuadPart = len;
  if (!SetFilePointerEx(fh, size, NULL, FILE_BEGIN)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  if (!SetEndOfFile(fh)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  return 0;
}

iwrc iwp_fallocate(HANDLE fh, off_t len) {
  return iwp_ftruncate(fh, len);
}

iwrc iwp_sleep(uint64_t ms) {
  iwrc rc = 0;
  struct timespec req;
  req.tv_sec = ms / 1000UL;
  req.tv_nsec = (ms % 1000UL) * 1000UL * 1000UL;
  if (nanosleep(&req, NULL)) {
    rc = iwrc_set_errno(IW_ERROR_THREADING_ERRNO, errno);
  }
  return rc;
}

iwrc iwp_fstat(const char *path, IWP_FILE_STAT *fs) {
  memset(fs, 0, sizeof(*fs));
  struct stat st = { 0 };
  if (stat(path, &st)) {
    return (errno == ENOENT) ? IW_ERROR_NOT_EXISTS : iwrc_set_errno(IW_ERROR_IO_ERRNO, errno);
  }
  fs->atime = 1000ULL * st.st_atime;
  fs->mtime = 1000ULL * st.st_mtime;
  fs->ctime = 1000ULL * st.st_ctime;
  fs->size = st.st_size;
  if (S_ISREG(st.st_mode)) {
    fs->ftype = IWP_TYPE_FILE;
  } else if (S_ISDIR(st.st_mode)) {
    fs->ftype = IWP_TYPE_DIR;
  } else {
    fs->ftype = IWP_OTHER;
  }
  return 0;
}

iwrc iwp_fstath(HANDLE fh, IWP_FILE_STAT *fs) {
  memset(fs, 0, sizeof(*fs));
  BY_HANDLE_FILE_INFORMATION info;
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (!GetFileInformationByHandle(fh, &info)) {
    uint32_t err = GetLastError();
    if (err == ERROR_FILE_NOT_FOUND) {
      return IW_ERROR_NOT_EXISTS;
    }
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  fs->atime = _iwp_filetime2millisecons(&info.ftLastAccessTime);
  fs->ctime = _iwp_filetime2millisecons(&info.ftCreationTime);
  fs->mtime = _iwp_filetime2millisecons(&info.ftLastWriteTime);
  ULARGE_INTEGER ul = {
    .LowPart  = info.nFileSizeLow,
    .HighPart = info.nFileSizeHigh
  };
  fs->size = ul.QuadPart;
  if (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
    fs->ftype = IWP_TYPE_DIR;
  } else {
    fs->ftype = IWP_TYPE_FILE;
  }
  return 0;
}

iwrc iwp_closefh(HANDLE fh) {
  if (INVALIDHANDLE(fh)) {
    return 0;
  }
  if (!CloseHandle(fh)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  return 0;
}

iwrc iwp_flock(HANDLE fh, iwp_lockmode lmode) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (lmode == IWP_NOLOCK) {
    return 0;
  }
  DWORD type = 0; /* shared lock with waiting */
  OVERLAPPED offset = { 0 };
  if (lmode & IWP_WLOCK) {
    type = LOCKFILE_EXCLUSIVE_LOCK;
  }
  if (lmode & IWP_NBLOCK) {
    type |= LOCKFILE_FAIL_IMMEDIATELY;
  }
  if (!LockFileEx(fh, type, 0, ULONG_MAX, ULONG_MAX, &offset)) {
    return iwrc_set_werror(IW_ERROR_ERRNO, GetLastError());
  }
  return 0;
}

iwrc iwp_unlock(HANDLE fh) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  OVERLAPPED offset = { 0 };
  if (!UnlockFileEx(fh, 0, ULONG_MAX, ULONG_MAX, &offset)) {
    return iwrc_set_werror(IW_ERROR_ERRNO, GetLastError());
  } else {
    return 0;
  }
}

iwrc iwp_pread(HANDLE fh, off_t off, void *buf, size_t siz, size_t *sp) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (!buf || !sp) {
    return IW_ERROR_INVALID_ARGS;
  }
  DWORD rdb;
  ULARGE_INTEGER bigint;
  OVERLAPPED offset = { 0 };
  bigint.QuadPart = off;
  offset.Offset = bigint.LowPart;
  offset.OffsetHigh = bigint.HighPart;
  if (!ReadFile(fh, buf, siz, &rdb, &offset)) {
    uint32_t err = GetLastError();
    if (err == ERROR_HANDLE_EOF) {
      *sp = rdb;
      return 0;
    }
    *sp = 0;
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, err);
  }
  *sp = rdb;
  return 0;
}

iwrc iwp_read(HANDLE fh, void *buf, size_t siz, size_t *sp) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (!buf || !sp) {
    return IW_ERROR_INVALID_ARGS;
  }
  DWORD rdb;
  if (!ReadFile(fh, buf, siz, &rdb, NULL)) {
    uint32_t err = GetLastError();
    if (err == ERROR_HANDLE_EOF) {
      *sp = rdb;
      return 0;
    }
    *sp = 0;
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, err);
  }
  *sp = rdb;
  return 0;
}

iwrc iwp_pwrite(HANDLE fh, off_t off, const void *buf, size_t siz, size_t *sp) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (!buf || !sp) {
    return IW_ERROR_INVALID_ARGS;
  }
  DWORD wrb;
  ULARGE_INTEGER bigint;
  OVERLAPPED offset = { 0 };
  bigint.QuadPart = off;
  offset.Offset = bigint.LowPart;
  offset.OffsetHigh = bigint.HighPart;
  if (!WriteFile(fh, buf, siz, &wrb, &offset)) {
    *sp = 0;
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  *sp = wrb;
  return 0;
}

iwrc iwp_write(HANDLE fh, const void *buf, size_t size) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  DWORD written;
  if (!WriteFile(fh, buf, size, &written, NULL)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  return 0;
}

iwrc iwp_lseek(HANDLE fh, off_t offset, iwp_seek_origin origin, off_t *pos) {
  if (INVALIDHANDLE(fh)) {
    return IW_ERROR_INVALID_HANDLE;
  }
  if (pos) {
    *pos = 0;
  }
  int w;
  LARGE_INTEGER loff, noff;
  loff.QuadPart = offset;
  if (origin == IWP_SEEK_CUR) {
    w = FILE_CURRENT;
  } else if (origin == IWP_SEEK_END) {
    w = FILE_END;
  } else {
    w = FILE_BEGIN;
  }
  if (!SetFilePointerEx(fh, loff, &noff, w)) {
    return iwrc_set_werror(IW_ERROR_IO_ERRNO, GetLastError());
  }
  if (pos) {
    *pos = noff.QuadPart;
  }
  return 0;
}

void iwp_set_current_thread_name(const char *name) {
  pthread_setname_np(pthread_self(), name);
}

size_t iwp_tmpdir(char *out, size_t len) {
  return GetTempPathA(len, out);
}

static iwrc _iwp_init_impl() {
  _iwp_getsysinfo();
  return 0;
}
