#ifdef _WIN32

#include "fs.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#define FS_PATH_MAX 1024

bool fs_open(const char* path, OpenMode mode, fs_handle* file) {
  WCHAR wpath[FS_PATH_MAX];
  if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX)) {
    return false;
  }

  DWORD access;
  DWORD creation;
  switch (mode) {
    case OPEN_READ: access = GENERIC_READ; creation = OPEN_EXISTING; break;
    case OPEN_WRITE: access = GENERIC_WRITE; creation = CREATE_ALWAYS; break;
    case OPEN_APPEND: access = GENERIC_WRITE; creation = OPEN_ALWAYS; break;
    default: return false;
  }

  DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE;
  file->handle = CreateFileW(wpath, access, share, NULL, creation, FILE_ATTRIBUTE_NORMAL, NULL);
  if (file->handle == INVALID_HANDLE_VALUE) {
    return false;
  }

  if (mode == OPEN_APPEND && SetFilePointer(file->handle, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER) {
    CloseHandle(file->handle);
    return false;
  }

  return true;
}

bool fs_close(fs_handle file) {
  return CloseHandle(file.handle);
}

bool fs_read(fs_handle file, void* buffer, size_t* bytes) {
  DWORD bytes32 = *bytes > UINT32_MAX ? UINT32_MAX : (DWORD) *bytes;
  bool success = ReadFile(file.handle, buffer, bytes32, &bytes32, NULL);
  *bytes = bytes32;
  return success;
}

bool fs_write(fs_handle file, const void* buffer, size_t* bytes) {
  DWORD bytes32 = *bytes > UINT32_MAX ? UINT32_MAX : (DWORD) *bytes;
  bool success = WriteFile(file.handle, buffer, bytes32, &bytes32, NULL);
  *bytes = bytes32;
  return success;
}

void* fs_map(const char* path, size_t* size) {
  WCHAR wpath[FS_PATH_MAX];
  if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX)) {
    return false;
  }

  fs_handle file;
  file.handle = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (file.handle == INVALID_HANDLE_VALUE) {
    return NULL;
  }

  DWORD hi;
  DWORD lo = GetFileSize(file.handle, &hi);
  if (lo == INVALID_FILE_SIZE) {
    CloseHandle(file.handle);
    return NULL;
  }

  if (SIZE_MAX > UINT32_MAX) {
    *size = ((size_t) hi << 32) | lo;
  } else if (hi > 0) {
    CloseHandle(file.handle);
    return NULL;
  } else {
    *size = lo;
  }

  HANDLE mapping = CreateFileMappingA(file.handle, NULL, PAGE_READONLY, hi, lo, NULL);
  if (mapping == NULL) {
    CloseHandle(file.handle);
    return NULL;
  }

  void* data = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, *size);

  CloseHandle(mapping);
  CloseHandle(file.handle);
  return data;
}

bool fs_unmap(void* data, size_t size) {
  return UnmapViewOfFile(data);
}

bool fs_stat(const char* path, FileInfo* info) {
  WCHAR wpath[FS_PATH_MAX];
  if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX)) {
    return false;
  }

  WIN32_FILE_ATTRIBUTE_DATA attributes;
  if (!GetFileAttributesExW(wpath, GetFileExInfoStandard, &attributes)) {
    return false;
  }

  FILETIME lastModified = attributes.ftLastWriteTime;
  info->type = (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_DIRECTORY : FILE_REGULAR;
  info->lastModified = ((uint64_t) lastModified.dwHighDateTime << 32) | lastModified.dwLowDateTime;
  info->lastModified /= 10000000ULL; // Convert windows 100ns ticks to seconds
  info->lastModified -= 11644473600ULL; // Convert windows epoch (1601) to POSIX epoch (1970)
  info->size = ((uint64_t) attributes.nFileSizeHigh << 32) | attributes.nFileSizeLow;
  return true;
}

bool fs_remove(const char* path) {
  WCHAR wpath[FS_PATH_MAX];
  if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX)) {
    return false;
  }
  return DeleteFileW(wpath) || RemoveDirectoryW(wpath);
}

bool fs_mkdir(const char* path) {
  WCHAR wpath[FS_PATH_MAX];
  if (!MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX)) {
    return false;
  }
  return CreateDirectoryW(wpath, NULL);
}

bool fs_list(const char* path, fs_list_cb* callback, void* context) {
  WCHAR wpath[FS_PATH_MAX];

  int length = MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, FS_PATH_MAX);

  if (length == 0 || length + 3 >= FS_PATH_MAX) {
    return false;
  } else {
    wcscat(wpath, L"/*");
  }

  WIN32_FIND_DATAW findData;
  HANDLE handle = FindFirstFileW(wpath, &findData);
  if (handle == INVALID_HANDLE_VALUE) {
    return false;
  }

  char filename[FS_PATH_MAX];
  do {
    if (!WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, filename, FS_PATH_MAX, NULL, NULL)) {
      FindClose(handle);
      return false;
    }

    callback(context, filename);
  } while (FindNextFileW(handle, &findData));

  FindClose(handle);
  return true;
}

#else // !_WIN32

#include "fs.h"
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <dirent.h>
#include <limits.h>
#include <unistd.h>

bool fs_open(const char* path, OpenMode mode, fs_handle* file) {
  int flags;
  switch (mode) {
    case OPEN_READ: flags = O_RDONLY; break;
    case OPEN_WRITE: flags = O_WRONLY | O_CREAT | O_TRUNC; break;
    case OPEN_APPEND: flags = O_APPEND | O_WRONLY | O_CREAT; break;
    default: return false;
  }
  file->fd = open(path, flags, S_IRUSR | S_IWUSR);
  return file->fd >= 0;
}

bool fs_close(fs_handle file) {
  return close(file.fd) == 0;
}

bool fs_read(fs_handle file, void* buffer, size_t* bytes) {
  ssize_t result = read(file.fd, buffer, *bytes);
  if (result < 0 || result > SSIZE_MAX) {
    *bytes = 0;
    return false;
  } else {
    *bytes = (uint32_t) result;
    return true;
  }
}

bool fs_write(fs_handle file, const void* buffer, size_t* bytes) {
  ssize_t result = write(file.fd, buffer, *bytes);
  if (result < 0 || result > SSIZE_MAX) {
    *bytes = 0;
    return false;
  } else {
    *bytes = (uint32_t) result;
    return true;
  }
}

void* fs_map(const char* path, size_t* size) {
  FileInfo info;
  fs_handle file;
  if (!fs_stat(path, &info) || !fs_open(path, OPEN_READ, &file)) {
    return NULL;
  }
  *size = info.size;
  void* data = mmap(NULL, *size, PROT_READ, MAP_PRIVATE, file.fd, 0);
  fs_close(file);
  return data;
}

bool fs_unmap(void* data, size_t size) {
  return munmap(data, size) == 0;
}

bool fs_stat(const char* path, FileInfo* info) {
  struct stat stats;
  if (stat(path, &stats)) {
    return false;
  }

  if (info) {
    info->size = (uint64_t) stats.st_size;
    info->lastModified = (uint64_t) stats.st_mtime;
    info->type = S_ISDIR(stats.st_mode) ? FILE_DIRECTORY : FILE_REGULAR;
  }

  return true;
}

bool fs_remove(const char* path) {
  return unlink(path) == 0 || rmdir(path) == 0;
}

bool fs_mkdir(const char* path) {
  return mkdir(path, S_IRWXU) == 0;
}

bool fs_list(const char* path, fs_list_cb* callback, void* context) {
  DIR* dir = opendir(path);
  if (!dir) {
    return false;
  }

  struct dirent* entry;
  while ((entry = readdir(dir)) != NULL) {
    callback(context, entry->d_name);
  }

  closedir(dir);
  return true;
}

#endif
