#include "file.h"

#include <assert.h>
#include <stdlib.h>
#include <string.h>

enum { PATH_BUF_SIZE = 4096 };

#if defined(_WIN32) && !defined(__CYGWIN__)
#  ifndef WIN32_LEAN_AND_MEAN
#    define WIN32_LEAN_AND_MEAN
#  endif
#  ifndef NOMINMAX
#    define NOMINMAX
#  endif
#  include <windows.h>
#  include <shlwapi.h>
#else
#  include <dirent.h>
#  include <sys/mman.h>
#  include <sys/stat.h>
#  include <fcntl.h>
#  include <unistd.h>
#  include <limits.h>
#endif

#ifdef __APPLE__
#  define st_mtim st_mtimespec
#endif

#ifndef PATH_MAX
#  define PATH_MAX MAX_PATH
#endif

static i32 is_delim(char c) {
  return c == '/' || c == '\\';
}

kit_str_builder_t kit_get_env(kit_str_t        name,
                              kit_allocator_t *alloc) {
  char *val  = getenv(BS(name));
  i64   size = val != NULL ? (i64) strlen(val) : 0;

  str_builder_t result;
  DA_INIT(result, size, alloc);
  assert(result.size == size);

  if (result.size == size && size > 0)
    memcpy(result.values, val, result.size);
  else
    DA_RESIZE(result, 0);

  return result;
}

kit_str_builder_t kit_path_norm(kit_str_t        path,
                                kit_allocator_t *alloc) {
  str_t parent = SZ("..");
  i64   i, i1, j;

  str_builder_t norm;
  DA_INIT(norm, path.size, alloc);
  assert(norm.size == path.size);

  if (norm.size != path.size)
    return norm;

  memcpy(norm.values, path.values, path.size);

  for (i1 = 0, i = 0; i < path.size; i++) {
    if (!is_delim(path.values[i]))
      continue;

    str_t s = { .size = i - i1 - 1, .values = path.values + i1 + 1 };
    if (AR_EQUAL(s, parent)) {
      i32 have_parent = 0;
      i64 i0          = 0;

      for (j = 0; j < i1; j++) {
        if (norm.values[j] != '\0')
          have_parent = 1;
        if (is_delim(norm.values[j]))
          i0 = j;
      }

      if (have_parent) {
        memset(norm.values + i0, '\0', i - i0);

        if (!is_delim(path.values[i0]))
          norm.values[i] = '\0';
      }
    }

    i1 = i;
  }

  i64 size = 0;

  for (i = 0; i < norm.size; i++) {
    if (norm.values[i] != '\0') {
      if (is_delim(norm.values[i]))
        norm.values[size] = KIT_PATH_DELIM_C;
      else
        norm.values[size] = norm.values[i];
      size++;
    }
  }

  norm.size = size;
  return norm;
}

kit_str_builder_t kit_path_join(kit_str_t left, kit_str_t right,
                                kit_allocator_t *alloc) {
  i64   left_size    = left.size;
  i64   right_size   = right.size;
  char *right_values = right.values;

  if (left_size > 0 && is_delim(left.values[left_size - 1]))
    left_size--;
  if (right_size > 0 && is_delim(right.values[0])) {
    right_size--;
    right_values++;
  }

  kit_str_builder_t joined;
  DA_INIT(joined, left_size + right_size + 1, alloc);
  assert(joined.size == left_size + right_size + 1);

  if (joined.size != left_size + right_size + 1)
    return joined;

  memcpy(joined.values, left.values, left_size);
  joined.values[left_size] = KIT_PATH_DELIM_C;
  memcpy(joined.values + left_size + 1, right_values, right_size);

  return joined;
}

kit_str_builder_t kit_path_user(kit_allocator_t *alloc) {
  kit_str_builder_t user = kit_get_env(SZ(KIT_ENV_HOME), alloc);
  if (user.size == 0) {
    DA_RESIZE(user, 1);
    if (user.size == 1)
      user.values[0] = '.';
  }
  return user;
}

kit_str_builder_t kit_path_cache(kit_allocator_t *alloc) {
  kit_str_builder_t cache, user;

  cache = kit_get_env(SZ("XDG_CACHE_HOME"), alloc);
  if (cache.size != 0)
    return cache;
  DA_DESTROY(cache);

#if defined(_WIN32) && !defined(__CYGWIN__)
  cache = kit_get_env(SZ("TEMP"), alloc);
  if (cache.size != 0)
    return cache;
  DA_DESTROY(cache);
#endif

  user = kit_path_user(alloc);
  cache =
#ifdef __APPLE__
      kit_path_join(WRAP_STR(user), SZ("Library" PATH_DELIM "Caches"),
                    alloc);
#else
      kit_path_join(WRAP_STR(user), SZ(".cache"), alloc);
#endif
  DA_DESTROY(user);

  return cache;
}

kit_str_builder_t kit_path_data(kit_allocator_t *alloc) {
  kit_str_builder_t data, user;

  data = kit_get_env(SZ("XDG_DATA_HOME"), alloc);
  if (data.size != 0)
    return data;
  DA_DESTROY(data);

#if defined(_WIN32) && !defined(__CYGWIN__)
  data = kit_get_env(SZ("LOCALAPPDATA"), alloc);
  if (data.size != 0)
    return data;
  DA_DESTROY(data);
#endif

  user = kit_path_user(alloc);
  data =
#ifdef __APPLE__
      kit_path_join(WRAP_STR(user), SZ("Library"), alloc);
#else
      kit_path_join(WRAP_STR(user), SZ(".local" PATH_DELIM "share"),
                    alloc);
#endif
  DA_DESTROY(user);

  return data;
}

kit_str_t kit_path_index(kit_str_t path, i64 index) {
  str_t s = { .size = 0, .values = NULL };

  i64 i0 = 0;
  i64 i  = 0;
  i64 n  = 0;

  for (; i < path.size; i++) {
    if (!is_delim(path.values[i]))
      continue;

    if (i0 < i) {
      if (n++ == index) {
        s.values = path.values + i0;
        s.size   = i - i0;
        return s;
      }
    }

    i0 = i + 1;
  }

  if (n == index) {
    s.values = path.values + i0;
    s.size   = i - i0;
  }

  return s;
}

kit_str_t kit_path_take(kit_str_t path, i64 count) {
  str_t s = { .size = 0, .values = path.values };

  i64 i0 = 0;
  i64 i  = 0;
  i64 n  = 0;

  for (; i < path.size; i++) {
    if (!is_delim(path.values[i]))
      continue;

    if (i0 < i) {
      if (n++ == count) {
        s.size = i;
        return s;
      }
    }

    i0 = i + 1;
  }

  if (n == count)
    s.size = i;

  return s;
}

//  TODO
//  Long path support for Windows
//
static void kit_prepare_path_(char *buf, kit_str_t path) {
  assert(path.size == 0 || path.values != NULL);
  assert(path.size + 1 < PATH_BUF_SIZE);

  memset(buf, 0, PATH_BUF_SIZE);
  if (path.size > 0 && path.size + 1 < PATH_BUF_SIZE)
    memcpy(buf, path.values, path.size);
}

#define PREPARE_PATH_BUF_  \
  char buf[PATH_BUF_SIZE]; \
  kit_prepare_path_(buf, path)

s32 kit_folder_create(kit_str_t path) {
  PREPARE_PATH_BUF_;
#if defined(_WIN32) && !defined(__CYGWIN__)
  return CreateDirectoryA(buf, NULL) ? KIT_OK
                                     : KIT_ERROR_MKDIR_FAILED;
#else
  return mkdir(buf, 0775) == 0 ? KIT_OK : KIT_ERROR_MKDIR_FAILED;
#endif
}

s32 kit_folder_create_recursive(kit_str_t path) {
  for (i32 i = 0;; i++) {
    str_t part = kit_path_take(path, i);
    i32   type = kit_path_type(part);
    if (type == KIT_PATH_FILE)
      return KIT_ERROR_FILE_ALREADY_EXISTS;
    if (type == KIT_PATH_NONE) {
      s32 s = kit_folder_create(part);
      if (s != KIT_OK)
        return s;
    }
    if (part.size == path.size)
      break;
  }

  return KIT_OK;
}

s32 kit_file_remove(kit_str_t path) {
  PREPARE_PATH_BUF_;
#if defined(_WIN32) && !defined(__CYGWIN__)
  return DeleteFileA(buf) ? KIT_OK : KIT_ERROR_UNLINK_FAILED;
#else
  return unlink(buf) == 0 ? KIT_OK : KIT_ERROR_UNLINK_FAILED;
#endif
}

s32 kit_folder_remove(kit_str_t path) {
  PREPARE_PATH_BUF_;
#if defined(_WIN32) && !defined(__CYGWIN__)
  return RemoveDirectoryA(buf) ? KIT_OK : KIT_ERROR_RMDIR_FAILED;
#else
  return rmdir(buf) == 0 ? KIT_OK : KIT_ERROR_RMDIR_FAILED;
#endif
}

s32 kit_file_remove_recursive(kit_str_t        path,
                              kit_allocator_t *alloc) {
  i32 type = kit_path_type(path);
  i64 i;

  switch (type) {
    case KIT_PATH_FILE: {
      s32 s = kit_file_remove(path);
      assert(s == KIT_OK);
      return s;
    }

    case KIT_PATH_FOLDER: {
      kit_path_list_t list = kit_folder_enum(path, alloc);

      assert(list.status == KIT_OK);
      if (list.status != KIT_OK) {
        kit_path_list_destroy(list);
        return list.status;
      }

      for (i = 0; i < list.files.size; i++) {
        str_builder_t full_path = kit_path_join(
            path, WRAP_STR(list.files.values[i]), alloc);
        s32 s = kit_file_remove_recursive(WRAP_STR(full_path), alloc);
        DA_DESTROY(full_path);
        assert(s == KIT_OK);
      }

      kit_path_list_destroy(list);

      s32 s = kit_folder_remove(path);
      assert(s == KIT_OK);
      return s;
    }

    default:;
  }

  return KIT_ERROR_FILE_DOES_NOT_EXIST;
}

kit_path_type_t kit_path_type(kit_str_t path) {
  PREPARE_PATH_BUF_;
#if defined(_WIN32) && !defined(__CYGWIN__)
  if (PathFileExistsA(buf)) {
    if ((GetFileAttributesA(buf) & FILE_ATTRIBUTE_DIRECTORY) != 0)
      return KIT_PATH_FOLDER;
    else
      return KIT_PATH_FILE;
  }
#else
  struct stat info;
  if (stat(buf, &info) == 0) {
    if (S_ISREG(info.st_mode))
      return KIT_PATH_FILE;
    if (S_ISDIR(info.st_mode))
      return KIT_PATH_FOLDER;
  }
#endif
  return KIT_PATH_NONE;
}

kit_file_info_t kit_file_info(kit_str_t path) {
  kit_file_info_t result;
  memset(&result, 0, sizeof result);

  PREPARE_PATH_BUF_;

#if defined(_WIN32) && !defined(__CYGWIN__)
  HANDLE f = CreateFileA(buf, GENERIC_READ, FILE_SHARE_READ, NULL,
                         OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (f != INVALID_HANDLE_VALUE) {
    FILETIME ft;
    if (GetFileTime(f, NULL, NULL, &ft) != 0) {
      i64 nsec100 = (((u64) ft.dwHighDateTime) << 32) |
                    (u64) ft.dwLowDateTime;
      result.time_modified_sec  = (i64) (nsec100 / 10000000);
      result.time_modified_nsec = (i32) (100 * (nsec100 % 10000000));
    } else {
      assert(0);
    }

    DWORD high;
    DWORD low = GetFileSize(f, &high);

    result.size   = (i64) ((((u64) high) << 32) | (u64) low);
    result.status = KIT_OK;

    CloseHandle(f);
    return result;
  }
#else
  struct stat info;
  if (stat(buf, &info) == 0 && S_ISREG(info.st_mode)) {
    result.size = (i64) info.st_size;
#  ifndef st_mtime
    //  No support for nanosecond timestamps.
    //
    result.time_modified_sec = (i64) info.st_mtime;
#  else
    result.time_modified_sec  = (i64) info.st_mtim.tv_sec;
    result.time_modified_nsec = (i32) info.st_mtim.tv_nsec;
#  endif
    result.status            = KIT_OK;
    return result;
  }
#endif

  result.status = KIT_ERROR_FILE_DOES_NOT_EXIST;
  return result;
}

kit_path_list_t kit_folder_enum(kit_str_t        path,
                                kit_allocator_t *alloc) {
  PREPARE_PATH_BUF_;

  kit_path_list_t result = { .status = KIT_OK };
  DA_INIT(result.files, 0, alloc);

#if defined(_WIN32) && !defined(__CYGWIN__)
  if (path.size + 3 >= PATH_BUF_SIZE) {
    result.status = KIT_ERROR_PATH_TOO_LONG;
    return result;
  }

  buf[path.size]     = '\\';
  buf[path.size + 1] = '*';

  WIN32_FIND_DATAA data;
  HANDLE           find = FindFirstFileA(buf, &data);

  if (find == INVALID_HANDLE_VALUE)
    return result;

  do {
    if (strcmp(data.cFileName, ".") == 0 ||
        strcmp(data.cFileName, "..") == 0)
      continue;

    i64 n = result.files.size;
    DA_RESIZE(result.files, n + 1);
    if (result.files.size != n + 1) {
      result.status = KIT_ERROR_OUT_OF_MEMORY;
      break;
    }

    i64 size = (i64) strlen(data.cFileName);
    DA_INIT(result.files.values[n], size, alloc);
    if (result.files.values[n].size != size) {
      DA_RESIZE(result.files, n);
      result.status = KIT_ERROR_OUT_OF_MEMORY;
      break;
    }

    memcpy(result.files.values[n].values, data.cFileName, size);
  } while (FindNextFileA(find, &data) != 0);

  FindClose(find);
#else
  DIR *directory = opendir(buf);

  if (directory == NULL)
    return result;

  for (;;) {
    struct dirent *entry = readdir(directory);

    if (entry == NULL)
      break;

    if (entry->d_name[0] == '.')
      continue;

    i64 n = result.files.size;
    DA_RESIZE(result.files, n + 1);
    if (result.files.size != n + 1) {
      result.status = KIT_ERROR_OUT_OF_MEMORY;
      break;
    }

    i64 size = (i64) strlen(entry->d_name);
    DA_INIT(result.files.values[n], size, alloc);
    if (result.files.values[n].size != size) {
      DA_RESIZE(result.files, n);
      result.status = KIT_ERROR_OUT_OF_MEMORY;
      break;
    }

    if (size > 0)
      memcpy(result.files.values[n].values, entry->d_name, size);
  }

  closedir(directory);
#endif

  return result;
}

void kit_path_list_destroy(kit_path_list_t list) {
  i64 i;
  for (i = 0; i < list.files.size; i++)
    DA_DESTROY(list.files.values[i]);
  DA_DESTROY(list.files);
}

kit_mapped_file_t kit_file_map(kit_str_t path, i64 size, i32 mode) {
  assert(size > 0);
  assert(path.size > 0);
  assert(path.size <= PATH_MAX);
  assert(path.values != NULL);

  kit_mapped_file_t mf;
  memset(&mf, 0, sizeof mf);

  if (size <= 0) {
    mf.status = KIT_ERROR_INVALID_SIZE;
    return mf;
  }

  if (path.size <= 0) {
    mf.status = KIT_ERROR_INVALID_ARGUMENT;
    return mf;
  }

  if (path.size > PATH_MAX) {
    mf.status = KIT_ERROR_PATH_TOO_LONG;
    return mf;
  }

#if defined(_WIN32) && !defined(__CYGWIN__)
  char buf[MAX_PATH + 1];
  memcpy(buf, path.values, path.size);
  buf[path.size] = '\0';

  HANDLE file = CreateFileA(
      buf, GENERIC_READ | GENERIC_WRITE,
      mode == FILE_MAP_SHARED ? FILE_SHARE_READ | FILE_SHARE_WRITE
                              : 0,
      NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

  if (file == INVALID_HANDLE_VALUE) {
    mf.status = KIT_ERROR_OPEN_FAILED;
    return mf;
  }

  LONG high = (LONG) (size >> 32);

  if (SetFilePointer(file, (LONG) size, &high, FILE_BEGIN) ==
      INVALID_SET_FILE_POINTER) {
    CloseHandle(file);
    assert(0);
    mf.status = KIT_ERROR_TRUNCATE_FAILED;
    return mf;
  }

  if (!SetEndOfFile(file)) {
    CloseHandle(file);
    assert(0);
    mf.status = KIT_ERROR_TRUNCATE_FAILED;
    return mf;
  }

  HANDLE map = CreateFileMappingA(file, NULL, PAGE_READWRITE,
                                  (DWORD) (size >> 32), (DWORD) size,
                                  NULL);

  if (map == INVALID_HANDLE_VALUE) {
    CloseHandle(file);
    assert(0);
    mf.status = KIT_ERROR_MAP_FAILED;
    return mf;
  }

  void *p = MapViewOfFile(map, FILE_MAP_ALL_ACCESS, 0, 0,
                          (SIZE_T) size);

  if (p == NULL) {
    CloseHandle(map);
    CloseHandle(file);
    assert(0);
    mf.status = KIT_ERROR_MAP_FAILED;
    return mf;
  }

  mf.status = KIT_OK;
  mf.size   = size;
  mf.bytes  = p;
  mf._file  = file;
  mf._map   = map;
#else
  char buf[PATH_MAX + 1];
  memcpy(buf, path.values, path.size);
  buf[path.size] = '\0';

  i32 fd = open(buf, O_RDWR | O_CREAT, 0664);

  if (fd == -1) {
    mf.status = KIT_ERROR_OPEN_FAILED;
    return mf;
  }

  if (ftruncate(fd, size) == -1) {
    close(fd);
    assert(0);
    mf.status = KIT_ERROR_TRUNCATE_FAILED;
    return mf;
  }

  void *p = mmap(
      NULL, size, PROT_READ | PROT_WRITE,
      mode == KIT_FILE_MAP_SHARED ? MAP_SHARED : MAP_PRIVATE, fd, 0);

  if (p == MAP_FAILED) {
    close(fd);
    assert(0);
    mf.status = KIT_ERROR_MAP_FAILED;
    return mf;
  }

  mf.status = KIT_OK;
  mf.size   = size;
  mf.bytes  = (u8 *) p;
  mf._fd    = fd;
#endif

  return mf;
}

s32 kit_file_sync(kit_mapped_file_t *mf) {
  assert(mf != NULL);

  if (mf == NULL)
    return KIT_ERROR_INVALID_ARGUMENT;

  s32 status = KIT_OK;

#if !defined(_WIN32) || defined(__CYGWIN__)
  if (msync(mf->bytes, mf->size, MS_SYNC) != 0)
    status |= KIT_ERROR_SYNC_FAILED;
#endif

  return status;
}

s32 kit_file_unmap(kit_mapped_file_t *mf) {
  assert(mf != NULL);

  if (mf == NULL)
    return KIT_ERROR_INVALID_ARGUMENT;

  s32 status = KIT_OK;

#if defined(_WIN32) && !defined(__CYGWIN__)
  if (!UnmapViewOfFile(mf->bytes))
    status |= KIT_ERROR_UNMAP_FAILED;
  if (!CloseHandle(mf->_map) || !CloseHandle(mf->_file))
    status |= KIT_ERROR_CLOSE_FAILED;
#else
  if (munmap(mf->bytes, mf->size) != 0)
    status |= KIT_ERROR_UNMAP_FAILED;
  if (close(mf->_fd) != 0)
    status |= KIT_ERROR_CLOSE_FAILED;
#endif

  return status;
}