typedef void OSErrorReport(const char *, ...);

#if OS_POSIX
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <linux/limits.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <spawn.h>
#include <poll.h>
#include <execinfo.h>
#include <backtrace.h>

API void (*Error)(const char *, ...);

struct backtrace_state *backtrace_state = NULL;

void BacktraceOnError(void *data, const char *msg, int errnum) {
    Error("libbacktrace error: %s (errnum: %d)\n", msg, errnum);
}

int BacktraceOnPrint(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) {
    bool printed = false;
    if (filename != NULL) {
        char buffer[1024];
        char *f = realpath(filename, buffer);
        printf("%s:%d:1: ", f, lineno);
        printed = true;
    }
    if (function != NULL) {
        printf("%s", function);
        printed = true;
    }
    if (printed) {
        printf("\n");
    }
    return 0;
}

void CrashHandler(int signal, siginfo_t* info, void* context) {
    backtrace_full(backtrace_state, 2, BacktraceOnPrint, BacktraceOnError, NULL);
    exit(1);
}

void RegisterCrashHandler(void) {
    backtrace_state = backtrace_create_state(NULL, 1, BacktraceOnError, NULL);

    struct sigaction sa;
    sa.sa_sigaction = CrashHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_NODEFER | SA_RESETHAND;  // Important flags!


    sigaction(SIGSEGV, &sa, NULL);   // Segmentation fault
    sigaction(SIGABRT, &sa, NULL);   // Abort
    sigaction(SIGBUS, &sa, NULL);    // Bus error
    sigaction(SIGILL, &sa, NULL);    // Illegal instruction
    sigaction(SIGFPE, &sa, NULL);    // Floating point exception
    sigaction(SIGTRAP, &sa, NULL);   // Breakpoint/trap
    sigaction(SIGSYS, &sa, NULL);    // Bad system call
    sigaction(SIGXCPU, &sa, NULL);   // CPU time limit exceeded
    sigaction(SIGXFSZ, &sa, NULL);   // File size limit exceeded
    sigaction(SIGTERM, &sa, NULL);   // Termination request (optional)
}

API void InitOS(void (*error_proc)(const char *, ...)) {
    Error = error_proc;
    RegisterCrashHandler();
}

API String ReadFile(Allocator al, String path) {
    Scratch scratch(al);
    String null_term = Copy(scratch, path);
    String result = {};
    FILE *f = fopen(null_term.data, "rb");
    if (f) {
        fseek(f, 0, SEEK_END);
        result.len = ftell(f);
        fseek(f, 0, SEEK_SET);

        result.data = (char *)AllocSize(al, result.len + 1);
        fread(result.data, result.len, 1, f);
        result.data[result.len] = 0;

        fclose(f);
    }
    return result;
}

API bool WriteFile(String path, String data) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    bool result = false;
    FILE *f = fopen((const char *)null_term.data, "w");
    if (f) {
        size_t written = fwrite(data.data, 1, data.len, f);
        if (written == data.len) {
            result = true;
        }
        fclose(f);
    }
    return result;
}

API bool DeleteFile(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    int result = unlink(null_term.data);
    return result == 0;
}

API MakeDirResult MakeDir(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    int error = mkdir(null_term.data, 0755);
    MakeDirResult result = MakeDirResult_Success;
    if (error != 0) {
        result = MakeDirResult_ErrorOther;
        if (errno == EEXIST) result = MakeDirResult_Exists;
    }
    return result;
}

API int64_t GetFileModTime(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat attrib = {};
    stat(null_term.data, &attrib);
    struct timespec ts = attrib.st_mtim;
    int64_t result = (((int64_t)ts.tv_sec) * 1000000ll) + ((int64_t)ts.tv_nsec) / 1000ll;
    return result;
}

API String GetAbsolutePath(Allocator al, String path) {
    Scratch scratch(al);
    String null_term = Copy(scratch, path);
    char *buffer = AllocArray(al, char, PATH_MAX);
    realpath(null_term.data, buffer);
    String result = buffer;
    return result;
}

API bool FileExists(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    bool result = false;
    if (access((char *)null_term.data, F_OK) == 0) {
        result = true;
    }
    return result;
}

API bool IsDir(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat s;
    if (stat(null_term.data, &s) != 0)
        return false;
    return S_ISDIR(s.st_mode);
}

API bool IsFile(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat s;
    if (stat(null_term.data, &s) != 0)
        return false;
    return S_ISREG(s.st_mode);
}

API bool IsAbsolute(String path) {
    bool result = path.len && path.data[0] == '/';
    return result;
}

API String GetWorkingDir(Allocator al) {
    char *buffer = AllocArray(al, char, PATH_MAX);
    char *cwd = getcwd(buffer, PATH_MAX);
    return cwd;
}

String GetExePath(Allocator al) {
    char *buffer = AllocArray(al, char, PATH_MAX);
    readlink("/proc/self/exe", buffer, PATH_MAX);
    return buffer;
}

API String GetExeDir(Allocator al) {
    Scratch scratch(al);
    String exe_path = GetExePath(scratch);
    String dir = ChopLastSlash(exe_path);
    String result = Copy(al, dir);
    return result;
}

API uint64_t GetTimeNanos(void) {
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    uint64_t ts = ((uint64_t)spec.tv_sec * 1000000000ull) + (uint64_t)spec.tv_nsec;
    return ts;
}

API double GetTimeMicros(void) {
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    return (((double)spec.tv_sec) * 1000000) + (((double)spec.tv_nsec) / 1000);
}

API bool IsValid(const FileIter &it) {
    return it.is_valid;
}

API void Advance(FileIter *it) {
    struct dirent *file = NULL;
    while ((file = readdir((DIR *)it->dir)) != NULL) {
        if (file->d_name[0] == '.' && file->d_name[1] == '.' && file->d_name[2] == 0) {
            continue;
        }
        if (file->d_name[0] == '.' && file->d_name[1] == 0) {
            continue;
        }

        it->is_directory = file->d_type == DT_DIR;
        it->filename = Copy(it->allocator, file->d_name);

        const char *dir_char_ending = it->is_directory ? "/" : "";
        const char *separator = it->path.data[it->path.len - 1] == '/' ? "" : "/";
        it->relative_path = Format(it->allocator, "%S%s%s%s", it->path, separator, file->d_name, dir_char_ending);
        it->absolute_path = GetAbsolutePath(it->allocator, it->relative_path);
        if (it->is_directory) it->absolute_path = Format(it->allocator, "%S/", it->absolute_path);
        it->is_valid = true;
        return;
    }
    it->is_valid = false;
    closedir((DIR *)it->dir);
}

API FileIter IterateFiles(Allocator alo, String path) {
    FileIter it = {};
    it.allocator = alo;
    it.path = path;
    Scratch scratch(alo);
    String null_term = Copy(scratch, path);
    it.dir = (void *)opendir((char *)null_term.data);
    if (it.dir) {
        Advance(&it);
    }
    return it;
}

struct UnixProcess {
    pid_t pid;
    int child_stdout_read;
    int stdin_write;
};

API Array<char *> SplitCommand(Allocator allocator, String command_line) {
    Array<char *> cmd = {allocator};

    String curr = {};
    for (int i = 0; i < command_line.len; i += 1) {
        if (command_line.data[i] == ' ') {
            if (curr.len > 0) {
                Add(&cmd, Copy(allocator, curr).data);
                curr = {};
            }
            continue;
        }
        if (curr.len == 0) {
            curr.data = command_line.data + i;
        }
        curr.len += 1;
    }

    if (curr.len > 0) {
        Add(&cmd, Copy(allocator, curr).data);
    }

    return cmd;
}

API Process SpawnProcess(String command_line, String working_dir, String write_stdin, Array<String> enviroment) {
    Scratch scratch;
    const int PIPE_READ = 0;
    const int PIPE_WRITE = 1;
    bool error = false;

    working_dir = Copy(scratch, working_dir);
    chdir(working_dir.data);

    Process process = {};
    UnixProcess *plat = (UnixProcess *)&process.platform;
    Array<char *> args = SplitCommand(scratch, command_line);
    Array<char *> env = {scratch};

    For (enviroment) {
        Add(&env, Copy(scratch, it).data);
    }

    int stdout_desc[2] = {};
    int stdin_desc[2] = {};
    posix_spawn_file_actions_t actions = {};

    if (posix_spawn_file_actions_init(&actions) != 0) {
        Error("Libc function failed: posix_spawn_file_actions_init, with error: %s", strerror(errno));
        return process;
    }
    defer {
        posix_spawn_file_actions_destroy(&actions);
    };

    if (pipe(stdout_desc) == -1) {
        Error("Libc function failed: pipe, with error: %s", strerror(errno));
        return process;
    }
    defer {
        if (error) {
            close(stdout_desc[PIPE_READ]);
            close(stdout_desc[PIPE_WRITE]);
        } else {
            close(stdout_desc[PIPE_WRITE]);
        }
    };

    if (pipe(stdin_desc) == -1) {
        Error("Libc function failed: pipe, with error: %s", strerror(errno));
        return process;
    }
    defer {
        if (error) {
            close(stdin_desc[PIPE_READ]);
            close(stdin_desc[PIPE_WRITE]);
        } else {
            close(stdin_desc[PIPE_READ]);
        }
    };

    error = posix_spawn_file_actions_addclose(&actions, stdout_desc[PIPE_READ]) != 0;
    if (error) {
        Error("Libc function failed: posix_spawn_file_actions_addclose, with error: %s", strerror(errno));
        return process;
    }

    error = posix_spawn_file_actions_adddup2(&actions, stdout_desc[PIPE_WRITE], STDOUT_FILENO) != 0;
    if (error) {
        Error("Libc function failed: posix_spawn_file_actions_adddup2 STDOUT_FILENO, with error: %s", strerror(errno));
        return process;
    }

    error = posix_spawn_file_actions_adddup2(&actions, stdout_desc[PIPE_WRITE], STDERR_FILENO) != 0;
    if (error) {
        Error("Libc function failed: posix_spawn_file_actions_adddup2 STDERR_FILENO, with error: %s", strerror(errno));
        return process;
    }

    error = posix_spawn_file_actions_addclose(&actions, stdin_desc[PIPE_WRITE]) != 0;
    if (error) {
        Error("Libc function failed: posix_spawn_file_actions_addclose, with error: %s", strerror(errno));
        return process;
    }

    error = posix_spawn_file_actions_adddup2(&actions, stdin_desc[PIPE_READ], STDIN_FILENO) != 0;
    if (error) {
        Error("Libc function failed: posix_spawn_file_actions_adddup2 STDIN_FILENO, with error: %s", strerror(errno));
        return process;
    }

    pid_t process_pid = 0;
    error = posix_spawnp(&process_pid, args[0], &actions, NULL, args.data, env.data) != 0;
    if (error) {
        Error("Libc function failed: failed to create process\n, with error: %s", strerror(errno));
        return process;
    }


    plat->child_stdout_read = stdout_desc[PIPE_READ];
    plat->stdin_write = stdin_desc[PIPE_WRITE];
    plat->pid = process_pid;

    if (write_stdin.len) {
        WriteStdin(&process, write_stdin);
        CloseStdin(&process);
    }

    process.id = process_pid;
    process.is_valid = true;
    return process;
}

API bool IsValid(Process *process) {
    UnixProcess *plat = (UnixProcess *)&process->platform;
    if (process->is_valid == false) {
        return false;
    }

    int status = 0;
    pollfd p = {};
    p.fd = plat->child_stdout_read;
    p.events = POLLRDHUP | POLLERR | POLLHUP | POLLNVAL;
    int res = poll(&p, 1, 0);
    if (res > 0) {
        pid_t result = waitpid(plat->pid, &status, 0);
        process->exit_code = WEXITSTATUS(status);
        return false;
    }

    return true;
}

API void KillProcess(Process *process) {
    Assert(process->is_valid);
    UnixProcess *plat = (UnixProcess *)process->platform;
    kill(plat->pid, SIGKILL);
    process->exit_code = -1;
}

API String PollStdout(Allocator allocator, Process *process, bool force_read) {
    Assert(process->is_valid);
    UnixProcess *plat = (UnixProcess *)process->platform;

    String result = {};
    result.data = AllocArray(allocator, char, 16 * 4096);

    pollfd p = {};
    p.fd = plat->child_stdout_read;
    p.events = POLLIN;
    int res = poll(&p, 1, 0);
    if (res > 0 || force_read) {
        result.len = read(plat->child_stdout_read, result.data, 4 * 4096);
    }
    return result;
}

API void WriteStdin(Process *process, String string) {
    if (string.len == 0) return;
    Assert(process->is_valid);

    UnixProcess *plat = (UnixProcess *)process->platform;
    ssize_t size = write(plat->stdin_write, string.data, string.len);

    Assert(size == string.len);
}

API void CloseStdin(Process *process) {
    UnixProcess *plat = (UnixProcess *)process->platform;
    close(plat->stdin_write);
}

#elif OS_WINDOWS

#ifndef NOMINMAX
    #define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
    #define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <stdio.h>
#include <intrin.h>

API void (*Error)(const char *, ...);

API void InitOS(void (*error_proc)(const char *, ...)) {
    Error = error_proc;

    SetConsoleOutputCP(65001);
    SetConsoleCP(65001);
}

API String ReadFile(Allocator arena, String path) {
    bool   success = false;
    String result  = {};

    Scratch  scratch(arena);
    String16 string16 = ToString16(scratch, path);
    HANDLE   handle   = CreateFileW((wchar_t *)string16.data, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (handle != INVALID_HANDLE_VALUE) {
        LARGE_INTEGER file_size;
        if (GetFileSizeEx(handle, &file_size)) {
            if (file_size.QuadPart != 0) {
                result.len  = (int64_t)file_size.QuadPart;
                result.data = (char *)AllocSize(arena, result.len + 1);
                DWORD read;
                if (ReadFile(handle, result.data, (DWORD)result.len, &read, NULL)) { // @todo: can only read 32 byte size files?
                    if (read == result.len) {
                        success                 = true;
                        result.data[result.len] = 0;
                    }
                }
            }
        }
        CloseHandle(handle);
    }

    if (!success) {
        Dealloc(arena, result.data);
        result = {};
    }

    return result;
}

typedef struct Win32_FileIter {
    HANDLE           handle;
    WIN32_FIND_DATAW data;
} Win32_FileIter;

API String GetAbsolutePath(Allocator arena, String relative) {
    Scratch  scratch(arena);
    String16 wpath     = ToString16(scratch, relative);
    wchar_t *wpath_abs = AllocArray(scratch, wchar_t, 4096);
    DWORD    written   = GetFullPathNameW((wchar_t *)wpath.data, 4096, wpath_abs, 0);
    if (written == 0)
        return {};
    String path = ToString(arena, {(char16_t *)wpath_abs, written});
    NormalizePathInPlace(path);
    return path;
}

API bool IsValid(const FileIter &it) {
    return it.is_valid;
}

API void Advance(FileIter *it) {
    while (FindNextFileW(it->w32->handle, &it->w32->data) != 0) {
        WIN32_FIND_DATAW *data = &it->w32->data;

        // Skip '.' and '..'
        if (data->cFileName[0] == '.' && data->cFileName[1] == '.' && data->cFileName[2] == 0) continue;
        if (data->cFileName[0] == '.' && data->cFileName[1] == 0) continue;

        it->is_directory      = data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
        it->filename          = ToString(it->allocator, (char16_t *)data->cFileName, WideLength((char16_t *)data->cFileName));
        const char *is_dir    = it->is_directory ? "/" : "";
        const char *separator = it->path.data[it->path.len - 1] == '/' ? "" : "/";
        it->relative_path     = Format(it->allocator, "%S%s%S%s", it->path, separator, it->filename, is_dir);
        it->absolute_path     = GetAbsolutePath(it->allocator, it->relative_path);
        it->is_valid          = true;

        if (it->is_directory) {
            Assert(it->relative_path.data[it->relative_path.len - 1] == '/');
            Assert(it->absolute_path.data[it->absolute_path.len - 1] == '/');
        }
        return;
    }

    it->is_valid = false;
    DWORD error  = GetLastError();
    Assert(error == ERROR_NO_MORE_FILES);
    FindClose(it->w32->handle);
}

API FileIter IterateFiles(Allocator alo, String path) {
    FileIter it  = {0};
    it.allocator = alo;
    it.path      = path;

    String   modified_path   = Format(it.allocator, "%S\\*", path);
    String16 modified_path16 = ToString16(it.allocator, modified_path);

    it.w32         = AllocType(it.allocator, Win32_FileIter);
    it.w32->handle = FindFirstFileW((wchar_t *)modified_path16.data, &it.w32->data);
    if (it.w32->handle == INVALID_HANDLE_VALUE) {
        it.is_valid = false;
        return it;
    }

    Advance(&it);
    return it;
}

API uint64_t GetTimeNanos(void) {
    static double invfreq;
    if (!invfreq) {
        LARGE_INTEGER frequency;
        QueryPerformanceFrequency(&frequency);
        invfreq = 1000000000.0 / frequency.QuadPart;
    }
    LARGE_INTEGER counter;
    QueryPerformanceCounter(&counter);
    uint64_t ts = (uint64_t)((double)counter.QuadPart * invfreq);
    return ts;
}

API double GetTimeMicros(void) {
    static double invfreq;
    if (!invfreq) {
        LARGE_INTEGER frequency;
        QueryPerformanceFrequency(&frequency);
        invfreq = 1000000.0 / frequency.QuadPart;
    }
    LARGE_INTEGER counter;
    QueryPerformanceCounter(&counter);
    return counter.QuadPart * invfreq;
}

API bool WriteFile(String path, String data) {
    bool result = false;

    Scratch  scratch;
    String16 wpath = ToString16(scratch, path);

    DWORD  access               = GENERIC_WRITE;
    DWORD  creation_disposition = CREATE_ALWAYS;
    HANDLE handle               = CreateFileW((wchar_t *)wpath.data, access, 0, NULL, creation_disposition, FILE_ATTRIBUTE_NORMAL, NULL);
    if (handle != INVALID_HANDLE_VALUE) {
        DWORD bytes_written = 0;
        Assert(data.len == (DWORD)data.len); // @Todo: can only read 32 byte size files?
        BOOL error = WriteFile(handle, data.data, (DWORD)data.len, &bytes_written, NULL);
        if (error == TRUE) {
            if (bytes_written == data.len) {
                result = true;
            }
        }
        CloseHandle(handle);
    }

    return result;
}

API String GetExePath(Allocator allocator) {
    Scratch  scratch(allocator);
    wchar_t *wbuffer = AllocArray(scratch, wchar_t, 4096);
    DWORD    wsize   = GetModuleFileNameW(0, wbuffer, 4096);
    Assert(wsize != 0);

    String path = ToString(allocator, (char16_t *)wbuffer, wsize);
    NormalizePathInPlace(path);
    return path;
}

API String GetExeDir(Allocator allocator) {
    Scratch scratch(allocator);
    String  path = GetExePath(scratch);
    path         = ChopLastSlash(path);
    path         = Copy(allocator, path);
    return path;
}

API bool FileExists(String path) {
    Scratch  scratch;
    String16 wbuff   = ToString16(scratch, path);
    DWORD    attribs = GetFileAttributesW((wchar_t *)wbuff.data);
    bool     result  = attribs == INVALID_FILE_ATTRIBUTES ? false : true;
    return result;
}

API bool IsDir(String path) {
    Scratch  scratch;
    String16 wbuff    = ToString16(scratch, path);
    DWORD    dwAttrib = GetFileAttributesW((wchar_t *)wbuff.data);
    return dwAttrib != INVALID_FILE_ATTRIBUTES && (dwAttrib & FILE_ATTRIBUTE_DIRECTORY);
}

API bool IsFile(String path) {
    Scratch  scratch;
    String16 wbuff    = ToString16(scratch, path);
    DWORD    dwAttrib = GetFileAttributesW((wchar_t *)wbuff.data);
    bool     is_file  = (dwAttrib & FILE_ATTRIBUTE_DIRECTORY) == 0;
    return dwAttrib != INVALID_FILE_ATTRIBUTES && is_file;
}

API String GetWorkingDir(Allocator arena) {
    Scratch  scratch(arena);
    wchar_t *wbuffer = AllocArray(scratch, wchar_t, 4096);
    DWORD    wsize   = GetCurrentDirectoryW(4096, wbuffer);
    Assert(wsize != 0);
    wbuffer[wsize] = 0;

    String path = ToString(arena, (char16_t *)wbuffer, wsize);
    NormalizePathInPlace(path);
    return path;
}

API bool IsAbsolute(String path) {
    bool result = path.len > 3 && IsAlphabetic(path.data[0]) && path.data[1] == ':' && (path.data[2] == '/' || path.data[2] == '\\');
    return result;
}

API bool DeleteFile(String path) {
    Scratch  scratch;
    String16 wpath = ToString16(scratch, path);

    BOOL success = DeleteFileW((wchar_t *)wpath.data);
    bool result  = true;
    if (success == 0) result = false;
    return result;
}

API int64_t GetFileModTime(String file) {
    Scratch          scratch;
    String16         string16 = ToString16(scratch, file);
    WIN32_FIND_DATAW data;
    HANDLE           handle = FindFirstFileW((wchar_t *)string16.data, &data);
    if (handle != INVALID_HANDLE_VALUE) {
        FindClose(handle);
        FILETIME time   = data.ftLastWriteTime;
        int64_t  result = (int64_t)time.dwHighDateTime << 32 | time.dwLowDateTime;
        return result;
    } else {
        return -1;
    }
}

API MakeDirResult MakeDir(String path) {
    Scratch scratch;
    MakeDirResult result = MakeDirResult_Success;
    String16 string16 = ToString16(scratch, path);
    BOOL success = CreateDirectoryW((wchar_t *)string16.data, NULL);
    if (success == 0) {
        DWORD error = GetLastError();
        if (error == ERROR_ALREADY_EXISTS) {
            result = MakeDirResult_Exists;
        } else if (error == ERROR_PATH_NOT_FOUND) {
            result = MakeDirResult_NotFound;
        } else {
            result = MakeDirResult_ErrorOther;
        }
    }
    return result;
}

struct Win32Process {
    HANDLE handle;
    HANDLE child_stdout_read;
    HANDLE child_stdout_write;
    HANDLE child_stdin_read;
    HANDLE child_stdin_write;
};
// static_assert(sizeof(Win32Process) < sizeof(Process::platform));

static void Win32ReportError(String msg, String cmd) {
    LPVOID lpMsgBuf;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL);
    defer { LocalFree(lpMsgBuf); };
    char  *buff    = (char *)lpMsgBuf;
    size_t buffLen = strlen((const char *)buff);
    if (buffLen > 0 && buff[buffLen - 1] == '\n') buff[buffLen - 1] = 0;
    Error("%S: %S! %s", msg, cmd, (char *)lpMsgBuf);
}

static void Win32CloseProcess(Process *process) {
    Win32Process *p = (Win32Process *)process->platform;
    if (p->handle != INVALID_HANDLE_VALUE) CloseHandle(p->handle);
    if (p->child_stdout_write != INVALID_HANDLE_VALUE) CloseHandle(p->child_stdout_write);
    if (p->child_stdout_read != INVALID_HANDLE_VALUE) CloseHandle(p->child_stdout_read);
    if (p->child_stdin_read != INVALID_HANDLE_VALUE) CloseHandle(p->child_stdin_read);
    if (p->child_stdin_write != INVALID_HANDLE_VALUE) CloseHandle(p->child_stdin_write);
    process->is_valid = false;
}

static void Win32ProcessError(Process *process, String msg, String cmd) {
    Win32ReportError(msg, cmd);
    Win32CloseProcess(process);
}

API void WriteStdin(Process *process, String string) {
    if (string.len == 0) return;
    Assert(process->is_valid);

    Win32Process *p = (Win32Process *)process->platform;
    Assert(p->child_stdin_write != INVALID_HANDLE_VALUE);

    DWORD written     = 0;
    bool  write_error = WriteFile(p->child_stdin_write, string.data, (DWORD)string.len, &written, NULL) == 0;

    Assert(write_error == false);
    Assert(written == string.len);
}

API void CloseStdin(Process *process) {
    Win32Process *p = (Win32Process *)process->platform;
    CloseHandle(p->child_stdin_write);
    p->child_stdin_write = INVALID_HANDLE_VALUE;
}

API Process SpawnProcess(String command_line, String working_dir, String write_stdin, Array<String> enviroment) {
    Process       process = {};
    Win32Process *p       = (Win32Process *)process.platform;

    Scratch scratch;
    command_line = Format(scratch, "/C %S", command_line);

    p->handle             = INVALID_HANDLE_VALUE;
    p->child_stdout_write = INVALID_HANDLE_VALUE;
    p->child_stdout_read  = INVALID_HANDLE_VALUE;
    p->child_stdin_read   = INVALID_HANDLE_VALUE;
    p->child_stdin_write  = INVALID_HANDLE_VALUE;

    SECURITY_ATTRIBUTES security_atrb = {};
    security_atrb.nLength             = sizeof(SECURITY_ATTRIBUTES);
    security_atrb.bInheritHandle      = TRUE;

    if (!CreatePipe(&p->child_stdout_read, &p->child_stdout_write, &security_atrb, 0)) {
        Win32ProcessError(&process, "Failed to create process at create pipe stage", command_line);
        return process;
    }

    if (!SetHandleInformation(p->child_stdout_read, HANDLE_FLAG_INHERIT, 0)) {
        Win32ProcessError(&process, "Failed to create process at create pipe stage", command_line);
        return process;
    }

    if (!CreatePipe(&p->child_stdin_read, &p->child_stdin_write, &security_atrb, 0)) {
        Win32ProcessError(&process, "Failed to create process at create pipe stage", command_line);
        return process;
    }

    if (!SetHandleInformation(p->child_stdin_write, HANDLE_FLAG_INHERIT, 0)) {
        Win32ProcessError(&process, "Failed to create process at create pipe stage", command_line);
        return process;
    }

    STARTUPINFOW startup = {};
    startup.cb           = sizeof(STARTUPINFO);
    startup.dwFlags      = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    startup.hStdInput    = p->child_stdin_read;
    startup.hStdOutput   = p->child_stdout_write;
    startup.hStdError    = p->child_stdout_write;
    startup.wShowWindow  = SW_HIDE;

    String16 cwd = ToString16(scratch, working_dir);
    String16 cmd = ToString16(scratch, command_line);

    char *env = NULL;
    if (enviroment.len) {
        Int size = GetSize(enviroment) + enviroment.len + 1;
        env = (char *)PushSize(scratch, size);
        Int i = 0;
        For(enviroment) {
            MemoryCopy(env + i, it.data, it.len);
            i += it.len;

            env[i++] = 0;
        }
        env[i++] = 0;
    }

    DWORD dwCreationFlags = 0;
    BOOL  bInheritHandles = TRUE;

    PROCESS_INFORMATION info = {};
    if (!CreateProcessW(L"c:\\windows\\system32\\cmd.exe", (wchar_t *)cmd.data, 0, 0, bInheritHandles, dwCreationFlags, env, (wchar_t *)cwd.data, &startup, &info)) {
        Win32ProcessError(&process, "failed to create process", command_line);
        return process;
    }

    // Close handles to the stdin and stdout pipes no longer needed by the child process.
    // If they are not explicitly closed, there is no way to recognize that the child process has ended.
    CloseHandle(info.hThread);
    CloseHandle(p->child_stdin_read);
    CloseHandle(p->child_stdout_write);
    p->child_stdin_read   = INVALID_HANDLE_VALUE;
    p->child_stdout_write = INVALID_HANDLE_VALUE;

    p->handle        = info.hProcess;
    process.is_valid = true;
    process.id = (int64_t)p->handle;

    if (write_stdin.len) {
        WriteStdin(&process, write_stdin);
        CloseStdin(&process);
    }

    return process;
}

API bool IsValid(Process *process) {
    if (process->is_valid == false) return false;
    Win32Process *p = (Win32Process *)process->platform;

    if (WaitForSingleObject(p->handle, 0) != WAIT_OBJECT_0) {
        return true;
    }

    DWORD exit_code;
    bool  get_exit_code_failed = GetExitCodeProcess(p->handle, &exit_code) == 0;
    if (get_exit_code_failed) {
        exit_code = -1;
    }

    process->exit_code = (int)exit_code;
    Win32CloseProcess(process);
    return false;
}

API void KillProcess(Process *process) {
    Assert(process->is_valid);
    Win32Process *p = (Win32Process *)process->platform;

    bool terminate_process_error = TerminateProcess(p->handle, -1) == 0;
    if (terminate_process_error) {
        Assert(0);
    }
    Win32CloseProcess(process);
    process->exit_code = -1;
}

API String PollStdout(Allocator allocator, Process *process, bool force_read) {
    Assert(process->is_valid);
    Win32Process *p = (Win32Process *)process->platform;

    DWORD bytes_avail = 0;
    bool  peek_error  = PeekNamedPipe(p->child_stdout_read, NULL, 0, NULL, &bytes_avail, NULL) == 0;
    if (peek_error) {
        return {};
    } else if (bytes_avail == 0) {
        return {};
    }

    size_t buffer_size = ClampTop(bytes_avail, (DWORD)(4096 * 16));
    char  *buffer      = AllocArray(allocator, char, buffer_size);

    DWORD bytes_read = 0;
    bool  read_error = ReadFile(p->child_stdout_read, buffer, (DWORD)buffer_size, &bytes_read, 0) == 0;
    if (read_error) {
        Win32ReportError("Failed to read the stdout of child process", "ReadFile");
        Dealloc(allocator, buffer);
        Win32CloseProcess(process);
        return {};
    }

    String result = {buffer, bytes_read};
    return result;
}

#elif OS_WASM
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <spawn.h>
#include <poll.h>
#define PATH_MAX 1024
#include <emscripten.h>

API void (*Error)(const char *, ...);

API void InitOS(void (*error_proc)(const char *, ...)) {
    Error = error_proc;
}

API String ReadFile(Allocator al, String path) {
    Scratch scratch(al);
    String null_term = Copy(scratch, path);
    String result = {};
    FILE *f = fopen(null_term.data, "rb");
    if (f) {
        fseek(f, 0, SEEK_END);
        result.len = ftell(f);
        fseek(f, 0, SEEK_SET);

        result.data = (char *)AllocSize(al, result.len + 1);
        fread(result.data, result.len, 1, f);
        result.data[result.len] = 0;

        fclose(f);
    }
    return result;
}

API bool WriteFile(String path, String data) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    bool result = false;
    FILE *f = fopen((const char *)null_term.data, "w");
    if (f) {
        size_t written = fwrite(data.data, 1, data.len, f);
        if (written == data.len) {
            result = true;
        }
        fclose(f);
    }
    return result;
}

API bool DeleteFile(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    int result = unlink(null_term.data);
    return result == 0;
}

API MakeDirResult MakeDir(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    int error = mkdir(null_term.data, 0755);
    MakeDirResult result = MakeDirResult_Success;
    if (error != 0) {
        result = MakeDirResult_ErrorOther;
        if (errno == EEXIST) result = MakeDirResult_Exists;
    }
    return result;
}

API int64_t GetFileModTime(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat attrib = {};
    stat(null_term.data, &attrib);
    struct timespec ts = attrib.st_mtim;
    int64_t result = (((int64_t)ts.tv_sec) * 1000000ll) + ((int64_t)ts.tv_nsec) / 1000ll;
    return result;
}

API String GetAbsolutePath(Allocator al, String path) {
    return path;
}

API bool FileExists(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    bool result = false;
    if (access((char *)null_term.data, F_OK) == 0) {
        result = true;
    }
    return result;
}

API bool IsDir(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat s;
    if (stat(null_term.data, &s) != 0)
        return false;
    return S_ISDIR(s.st_mode);
}

API bool IsFile(String path) {
    Scratch scratch;
    String null_term = Copy(scratch, path);
    struct stat s;
    if (stat(null_term.data, &s) != 0)
        return false;
    return S_ISREG(s.st_mode);
}

API bool IsAbsolute(String path) {
    bool result = path.len && path.data[0] == '/';
    return result;
}

API String GetWorkingDir(Allocator al) {
    return Copy(al, "/workingdir");
}

API String GetExePath(Allocator al) {
    return Copy(al, "/text_editor");
}

API String GetExeDir(Allocator al) {
    Scratch scratch(al);
    String exe_path = GetExePath(scratch);
    String dir = ChopLastSlash(exe_path);
    String result = Copy(al, dir);
    return result;
}

API uint64_t GetTimeNanos(void) {
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    uint64_t ts = ((uint64_t)spec.tv_sec * 1000000000ull) + (uint64_t)spec.tv_nsec;
    return ts;
}

API double GetTimeMicros(void) {
    struct timespec spec;
    clock_gettime(CLOCK_MONOTONIC, &spec);
    return (((double)spec.tv_sec) * 1000000) + (((double)spec.tv_nsec) / 1000);
}

API bool IsValid(const FileIter &it) {
    return it.is_valid;
}

API void Advance(FileIter *it) {
    struct dirent *file = NULL;
    while ((file = readdir((DIR *)it->dir)) != NULL) {
        if (file->d_name[0] == '.' && file->d_name[1] == '.' && file->d_name[2] == 0) {
            continue;
        }
        if (file->d_name[0] == '.' && file->d_name[1] == 0) {
            continue;
        }

        it->is_directory = file->d_type == DT_DIR;
        it->filename = Copy(it->allocator, file->d_name);

        const char *dir_char_ending = it->is_directory ? "/" : "";
        const char *separator = it->path.data[it->path.len - 1] == '/' ? "" : "/";
        it->relative_path = Format(it->allocator, "%.*s%s%s%s", FmtString(it->path), separator, file->d_name, dir_char_ending);
        it->absolute_path = GetAbsolutePath(it->allocator, it->relative_path);
        if (it->is_directory) it->absolute_path = Format(it->allocator, "%.*s/", FmtString(it->absolute_path));
        it->is_valid = true;
        return;
    }
    it->is_valid = false;
    closedir((DIR *)it->dir);
}

API FileIter IterateFiles(Allocator alo, String path) {
    FileIter it = {};
    it.allocator = alo;
    it.path = path;
    Scratch scratch(alo);
    String null_term = Copy(scratch, path);
    it.dir = (void *)opendir((char *)null_term.data);
    if (it.dir) {
        Advance(&it);
    }
    return it;
}

API Array<char *> SplitCommand(Allocator allocator, String command_line) {
    Array<char *> cmd = {allocator};

    String curr = {};
    for (int i = 0; i < command_line.len; i += 1) {
        if (command_line.data[i] == ' ') {
            if (curr.len > 0) {
                Add(&cmd, Copy(allocator, curr).data);
                curr = {};
            }
            continue;
        }
        if (curr.len == 0) {
            curr.data = command_line.data + i;
        }
        curr.len += 1;
    }

    if (curr.len > 0) {
        Add(&cmd, Copy(allocator, curr).data);
    }

    return cmd;
}

API Process SpawnProcess(String command_line, String working_dir, String write_stdin, Array<String> enviroment) {
    return {};
}

API bool IsValid(Process *process) {
    return false;
}

API void KillProcess(Process *process) {
}

API String PollStdout(Allocator allocator, Process *process, bool force_read) {
    return {};
}

API void WriteStdin(Process *process, String string) {
    return;
}

API void CloseStdin(Process *process) {
    return;
}

#endif

API double GetTimeSeconds() {
    return GetTimeMicros() / 1000000.0;
}
