#ifndef _WIN32
#define _POSIX_C_SOURCE 200809L
#endif

#include "platform.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <direct.h>
#include <io.h>
#include <process.h>
#else
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

static bool platform_error(Error* error, const char* action, const char* description) {
  error->line = 0;
  snprintf(error->message, sizeof(error->message), "cannot %s %s", action, description);
  return false;
}

static bool append_bytes(char** buffer, size_t* length, size_t* capacity,
                         const char* data, size_t count) {
  if (count > SIZE_MAX - *length - 1) return false;
  size_t needed = *length + count + 1;
  if (needed > *capacity) {
    size_t grown = *capacity == 0 ? 4096 : *capacity;
    while (grown < needed) {
      if (grown > SIZE_MAX / 2) { grown = needed; break; }
      grown *= 2;
    }
    char* replacement = realloc(*buffer, grown);
    if (replacement == NULL) return false;
    *buffer = replacement;
    *capacity = grown;
  }
  memcpy(*buffer + *length, data, count);
  *length += count;
  (*buffer)[*length] = '\0';
  return true;
}

#ifdef _WIN32
typedef struct { char* data; size_t length; size_t capacity; } CommandLine;

static bool command_append(CommandLine* line, const char* text, size_t count) {
  return append_bytes(&line->data, &line->length, &line->capacity, text, count);
}

/* Quote an argv element using the parsing rules used by the Microsoft C runtime. */
static bool command_argument(CommandLine* line, const char* argument) {
  bool quote = *argument == '\0' || strpbrk(argument, " \t\"") != NULL;
  if (!quote) return command_append(line, argument, strlen(argument));
  if (!command_append(line, "\"", 1)) return false;
  size_t slashes = 0;
  for (const char* p = argument;; p++) {
    if (*p == '\\') { slashes++; continue; }
    if (*p == '\"' || *p == '\0') {
      size_t count = slashes * 2 + (*p == '\"');
      for (size_t i = 0; i < count; i++)
        if (!command_append(line, "\\", 1)) return false;
      slashes = 0;
      if (*p == '\0') break;
    } else {
      for (size_t i = 0; i < slashes; i++)
        if (!command_append(line, "\\", 1)) return false;
      slashes = 0;
    }
    if (!command_append(line, p, 1)) return false;
  }
  return command_append(line, "\"", 1);
}

static char* windows_command_line(char* const argv[]) {
  CommandLine line = {0};
  for (size_t i = 0; argv[i] != NULL; i++) {
    if (i != 0 && !command_append(&line, " ", 1)) goto failure;
    if (!command_argument(&line, argv[i])) goto failure;
  }
  return line.data;
failure:
  free(line.data);
  return NULL;
}

bool platform_run(char* const argv[], int* exit_code, Error* error, const char* description) {
  intptr_t result = _spawnvp(_P_WAIT, argv[0], (const char* const*)argv);
  if (result == -1) return platform_error(error, "start", description);
  *exit_code = (int)result;
  return true;
}

bool platform_capture(char* const argv[], char** output, int* exit_code, Error* error,
                      const char* description) {
  SECURITY_ATTRIBUTES security = {
    .nLength = sizeof(security), .lpSecurityDescriptor = NULL, .bInheritHandle = TRUE,
  };
  HANDLE read_pipe = NULL, write_pipe = NULL;
  if (!CreatePipe(&read_pipe, &write_pipe, &security, 0) ||
      !SetHandleInformation(read_pipe, HANDLE_FLAG_INHERIT, 0)) {
    if (read_pipe != NULL) CloseHandle(read_pipe);
    if (write_pipe != NULL) CloseHandle(write_pipe);
    return platform_error(error, "create output pipe for", description);
  }
  char* command_line = windows_command_line(argv);
  if (command_line == NULL) {
    CloseHandle(read_pipe); CloseHandle(write_pipe);
    return platform_error(error, "build command line for", description);
  }
  STARTUPINFOA startup = {.cb = sizeof(startup)};
  startup.dwFlags = STARTF_USESTDHANDLES;
  startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
  startup.hStdOutput = write_pipe;
  startup.hStdError = write_pipe;
  PROCESS_INFORMATION process = {0};
  BOOL started = CreateProcessA(NULL, command_line, NULL, NULL, TRUE, CREATE_NO_WINDOW,
                                NULL, NULL, &startup, &process);
  free(command_line);
  CloseHandle(write_pipe);
  if (!started) {
    CloseHandle(read_pipe);
    return platform_error(error, "start", description);
  }
  char* buffer = NULL;
  size_t length = 0, capacity = 0;
  char chunk[4096];
  DWORD count = 0;
  bool ok = true;
  while (ReadFile(read_pipe, chunk, sizeof(chunk), &count, NULL) && count != 0) {
    if (ok && !append_bytes(&buffer, &length, &capacity, chunk, count)) ok = false;
  }
  CloseHandle(read_pipe);
  WaitForSingleObject(process.hProcess, INFINITE);
  DWORD result = 1;
  if (!GetExitCodeProcess(process.hProcess, &result)) ok = false;
  CloseHandle(process.hThread);
  CloseHandle(process.hProcess);
  if (buffer == NULL) buffer = calloc(1, 1);
  if (!ok || buffer == NULL) {
    free(buffer);
    return platform_error(error, "capture output from", description);
  }
  *output = buffer;
  *exit_code = (int)result;
  return true;
}

bool platform_current_executable(char* path, size_t capacity) {
  if (capacity == 0 || capacity > (size_t)MAXDWORD) return false;
  DWORD count = GetModuleFileNameA(NULL, path, (DWORD)capacity);
  return count != 0 && count < capacity;
}

bool platform_temporary_file(char* path, size_t capacity) {
  char directory[MAX_PATH + 1];
  DWORD count = GetTempPathA(sizeof(directory), directory);
  if (count == 0 || count >= sizeof(directory) || capacity < MAX_PATH + 1) return false;
  return GetTempFileNameA(directory, "vdt", 0, path) != 0;
}

bool platform_temporary_directory(char* path, size_t capacity) {
  if (!platform_temporary_file(path, capacity)) return false;
  if (!DeleteFileA(path)) return false;
  return CreateDirectoryA(path, NULL) != 0;
}

bool platform_delete_file(const char* path) { return DeleteFileA(path) != 0; }
bool platform_remove_directory(const char* path) { return RemoveDirectoryA(path) != 0; }
bool platform_ensure_directory(const char* path) {
  if (CreateDirectoryA(path, NULL)) return true;
  return GetLastError() == ERROR_ALREADY_EXISTS;
}
char platform_path_separator(void) { return '\\'; }

#else
bool platform_run(char* const argv[], int* exit_code, Error* error, const char* description) {
  pid_t child = fork();
  if (child < 0) return platform_error(error, "start", description);
  if (child == 0) { execvp(argv[0], argv); _exit(127); }
  int status = 0;
  pid_t waited;
  do { waited = waitpid(child, &status, 0); } while (waited < 0 && errno == EINTR);
  if (waited < 0) return platform_error(error, "wait for", description);
  *exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
  return true;
}

bool platform_capture(char* const argv[], char** output, int* exit_code, Error* error,
                      const char* description) {
  int pipes[2];
  if (pipe(pipes) != 0) return platform_error(error, "create output pipe for", description);
  pid_t child = fork();
  if (child < 0) {
    close(pipes[0]); close(pipes[1]);
    return platform_error(error, "start", description);
  }
  if (child == 0) {
    close(pipes[0]);
    dup2(pipes[1], STDOUT_FILENO); dup2(pipes[1], STDERR_FILENO);
    close(pipes[1]);
    execvp(argv[0], argv); _exit(127);
  }
  close(pipes[1]);
  char* buffer = NULL;
  size_t length = 0, capacity = 0;
  char chunk[4096];
  bool ok = true;
  for (;;) {
    ssize_t count = read(pipes[0], chunk, sizeof(chunk));
    if (count == 0) break;
    if (count < 0) { if (errno == EINTR) continue; ok = false; break; }
    if (!append_bytes(&buffer, &length, &capacity, chunk, (size_t)count)) { ok = false; break; }
  }
  close(pipes[0]);
  int status = 0;
  pid_t waited;
  do { waited = waitpid(child, &status, 0); } while (waited < 0 && errno == EINTR);
  if (buffer == NULL) buffer = calloc(1, 1);
  if (!ok || waited < 0 || buffer == NULL) {
    free(buffer);
    return platform_error(error, "capture output from", description);
  }
  *output = buffer;
  *exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
  return true;
}

bool platform_current_executable(char* path, size_t capacity) {
  if (capacity == 0) return false;
  ssize_t count = readlink("/proc/self/exe", path, capacity - 1);
  if (count < 0 || (size_t)count >= capacity - 1) return false;
  path[count] = '\0';
  return true;
}

bool platform_temporary_file(char* path, size_t capacity) {
  const char pattern[] = "/tmp/vedit-subtitle-XXXXXX";
  if (capacity < sizeof(pattern)) return false;
  memcpy(path, pattern, sizeof(pattern));
  int fd = mkstemp(path);
  if (fd < 0) return false;
  return close(fd) == 0;
}

bool platform_temporary_directory(char* path, size_t capacity) {
  const char pattern[] = "/tmp/vedit-fonts-XXXXXX";
  if (capacity < sizeof(pattern)) return false;
  memcpy(path, pattern, sizeof(pattern));
  return mkdtemp(path) != NULL;
}

bool platform_delete_file(const char* path) { return unlink(path) == 0; }
bool platform_remove_directory(const char* path) { return rmdir(path) == 0; }
bool platform_ensure_directory(const char* path) {
  return mkdir(path, 0700) == 0 || errno == EEXIST;
}
char platform_path_separator(void) { return '/'; }
#endif