#include "sys.h"
#include "memory.h"
#include "app.h"
#include "string.h"
#include "arena.h"
#include "scratch.h"
#include "atomic.h"
#include "work.h"
#include "log.h"
#include "math.h"
#include "util.h"
#include "thread_local.h"
#include "uni.h"

#pragma warning(push, 0)
# define UNICODE
# include <Windows.h>
# include <windowsx.h>
# include <ShlObj_core.h>
# include <fileapi.h>
# include <dwmapi.h>
# include <NTSecAPI.h>
#pragma warning(pop)

#pragma comment(lib, "kernel32")
#pragma comment(lib, "user32")
#pragma comment(lib, "shell32")
#pragma comment(lib, "ole32")
#pragma comment(lib, "advapi32")
#pragma comment(lib, "winmm")
#pragma comment(lib, "dwmapi")

#define SYS_WINDOW_EVENT_LISTENERS_MAX 512
#define WINDOW_CLASS_NAME L"power_play_window_class"

struct win32_thread {
    u64 gen;
    sys_thread_entry_point_func *entry_point;
    void *thread_data;
    char thread_name_cstr[256];

    struct win32_thread *next;
    struct win32_thread *prev;

    HANDLE handle;
};

struct win32_condition_variable {
    CONDITION_VARIABLE condition_variable;
    struct win32_condition_variable *next_free;
};

enum win32_window_cursor_set_flag {
    WIN32_WINDOW_CURSOR_SET_FLAG_NONE                   = 0x0,
    WIN32_WINDOW_CURSOR_SET_FLAG_POSITION               = 0x1,
    WIN32_WINDOW_CURSOR_SET_FLAG_HIDE                   = 0x2,
    WIN32_WINDOW_CURSOR_SET_FLAG_SHOW                   = 0x4,
    WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP            = 0X8,
    WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP           = 0X10
};

struct win32_window {
    u32 flags;

    HWND hwnd;
    u32 tid;
    struct sync_flag ready_sf;

    u16 utf16_high_surrogate_last_input;

    struct sys_rw_mutex settings_rw_mutex;
    struct sys_window_settings settings;

    i32 monitor_width;
    i32 monitor_height;
    /* NOTE: width & height are unaffected by window minimization (they retain
     * their pre-minimized values) */
    i32 x, y, width, height;

    /* FIXME: Use a cmd buffer for updating cursor (and maybe also settings).
     * Current setup means cursor_set calls can be applied out of order */
    u32 cursor_set_flags;
    struct v2 cursor_set_position;
    struct rect cursor_clip_bounds;

    b32 event_thread_shutdown;
    struct sys_thread event_thread;

    struct sys_mutex event_callbacks_mutex;
    sys_window_event_callback_func *event_callbacks[SYS_WINDOW_EVENT_LISTENERS_MAX];
    u64 event_callbacks_count;

    struct win32_window *next_free;
};

/* ========================== *
 * Global state
 * ========================== */

GLOBAL struct {
    SYSTEM_INFO info;
    LARGE_INTEGER timer_frequency;
    LARGE_INTEGER timer_start;
    i32 scheduler_period_ms;
    DWORD thread_tls_index;
    u32 main_thread_id;

    /* Panic */
    struct atomic_i32 panicking;
    wchar_t panic_wstr[4096];
    HANDLE panic_event;

    /* Lookup tables */
    enum sys_btn vk_btn_table[256];

    /* Condition variables */
    struct sys_mutex condition_variables_mutex;
    struct arena condition_variables_arena;
    struct win32_condition_variable *first_free_condition_variable;

    /* Thread params */
    struct sys_rw_mutex threads_rw_mutex;
    struct arena threads_arena;
    struct win32_thread *threads_first;
    struct win32_thread *threads_last;
    struct win32_thread *threads_first_free;

    /* Windows */
    WNDCLASSEXW window_class;
    struct sys_mutex windows_mutex;
    struct arena windows_arena;
    struct win32_window *first_free_window;
} G = { 0 }, DEBUG_ALIAS(G, G_sys_win32);

/* ========================== *
 * Events
 * ========================== */

/* https://git.rfleury.com/community/root_basic/src/commit/9b49fcd24e0c3f875b7c213e81a219bf8544bddb/code/os/gfx/win32/os_gfx_win32.c#L193 */
INTERNAL void win32_init_vk_btn_table(void)
{
    MEMZERO_ARRAY(G.vk_btn_table);

    for (u32 i = 'A', j = SYS_BTN_A; i <= 'Z'; i += 1, j += 1) {
        G.vk_btn_table[i] = (enum sys_btn)j;
    }
    for (u32 i = '0', j = SYS_BTN_0; i <= '9'; i += 1, j += 1) {
        G.vk_btn_table[i] = (enum sys_btn)j;
    }
    for (u32 i = VK_F1, j = SYS_BTN_F1; i <= VK_F24; i += 1, j += 1) {
        G.vk_btn_table[i] = (enum sys_btn)j;
    }
    G.vk_btn_table[VK_ESCAPE]        = SYS_BTN_ESC;
    G.vk_btn_table[VK_OEM_3]         = SYS_BTN_GRAVE_ACCENT;
    G.vk_btn_table[VK_OEM_MINUS]     = SYS_BTN_MINUS;
    G.vk_btn_table[VK_OEM_PLUS]      = SYS_BTN_EQUAL;
    G.vk_btn_table[VK_BACK]          = SYS_BTN_BACKSPACE;
    G.vk_btn_table[VK_TAB]           = SYS_BTN_TAB;
    G.vk_btn_table[VK_SPACE]         = SYS_BTN_SPACE;
    G.vk_btn_table[VK_RETURN]        = SYS_BTN_ENTER;
    G.vk_btn_table[VK_CONTROL]       = SYS_BTN_CTRL;
    G.vk_btn_table[VK_SHIFT]         = SYS_BTN_SHIFT;
    G.vk_btn_table[VK_MENU]          = SYS_BTN_ALT;
    G.vk_btn_table[VK_UP]            = SYS_BTN_UP;
    G.vk_btn_table[VK_LEFT]          = SYS_BTN_LEFT;
    G.vk_btn_table[VK_DOWN]          = SYS_BTN_DOWN;
    G.vk_btn_table[VK_RIGHT]         = SYS_BTN_RIGHT;
    G.vk_btn_table[VK_DELETE]        = SYS_BTN_DELETE;
    G.vk_btn_table[VK_PRIOR]         = SYS_BTN_PAGE_UP;
    G.vk_btn_table[VK_NEXT]          = SYS_BTN_PAGE_DOWN;
    G.vk_btn_table[VK_HOME]          = SYS_BTN_HOME;
    G.vk_btn_table[VK_END]           = SYS_BTN_END;
    G.vk_btn_table[VK_OEM_2]         = SYS_BTN_FORWARD_SLASH;
    G.vk_btn_table[VK_OEM_PERIOD]    = SYS_BTN_PERIOD;
    G.vk_btn_table[VK_OEM_COMMA]     = SYS_BTN_COMMA;
    G.vk_btn_table[VK_OEM_7]         = SYS_BTN_QUOTE;
    G.vk_btn_table[VK_OEM_4]         = SYS_BTN_LEFT_BRACKET;
    G.vk_btn_table[VK_OEM_6]         = SYS_BTN_RIGHT_BRACKET;
    G.vk_btn_table[VK_INSERT]        = SYS_BTN_INSERT;
    G.vk_btn_table[VK_OEM_1]         = SYS_BTN_SEMICOLON;
}

/* ========================== *
 * Memory
 * ========================== */

void *sys_memory_reserve(u64 size)
{
    void *ptr = VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS);
    return ptr;
}

void sys_memory_release(void *address)
{
    VirtualFree(address, 0, MEM_RELEASE);
}

void *sys_memory_commit(void *address, u64 size)
{
    void *ptr = VirtualAlloc(address, size, MEM_COMMIT, PAGE_READWRITE);
    return ptr;
}

void sys_memory_decommit(void *address, u64 size)
{
    VirtualFree(address, size, MEM_DECOMMIT);
}

void sys_memory_set_committed_readonly(void *address, u64 size)
{
    DWORD old = PAGE_READWRITE;
    VirtualProtect(address, size, PAGE_READONLY, &old);
}

/* ========================== *
 * Time
 * ========================== */

INTERNAL struct sys_datetime win32_time_to_sys_time(SYSTEMTIME st)
{
    return (struct sys_datetime) {
        .year = st.wYear,
        .month = st.wMonth,
        .day_of_week = st.wDayOfWeek,
        .day = st.wDay,
        .hour = st.wHour,
        .minute = st.wMinute,
        .second = st.wSecond,
        .milliseconds = st.wMilliseconds
    };
}

/* prevent 64-bit overflow when computing relative timestamp
 * https://github.com/floooh/sokol/blob/d4ac122f36d7659a18b312fd4fa2317fb9e06a63/sokol_time.h#L203
 */
INTERNAL i64 _win32_i64_muldiv(i64 value, i64 numer, i64 denom) {
    i64 q = value / denom;
    i64 r = value % denom;
    return q * numer + r * numer / denom;
}

sys_timestamp_t sys_timestamp(void)
{
    LARGE_INTEGER time;
    QueryPerformanceCounter(&time);
    return (u64)_win32_i64_muldiv(time.QuadPart - G.timer_start.QuadPart, 1000000000, G.timer_frequency.QuadPart);
}

f64 sys_timestamp_seconds(sys_timestamp_t ts)
{
    return (f64)ts / 1000000000.0;
}

struct sys_datetime sys_local_time(void)
{
    SYSTEMTIME lt;
    GetLocalTime(&lt);
    return win32_time_to_sys_time(lt);
}

/* ========================== *
 * File system
 * ========================== */

INTERNAL struct string string_from_win32_path(struct arena *arena, wchar_t *src)
{
    struct string res = {
        .len = 0,
        .text = arena_dry_push(arena, u8)
    };

    while (*src) {
        struct string16 decode_str = { .len = *(src + 1) ? 2 : 1, .text = src };
        struct uni_decode_utf16_result decoded = uni_decode_utf16(decode_str);
        struct uni_encode_utf8_result encoded = uni_encode_utf8(decoded.codepoint);
        u8 *dest = arena_push_array(arena, u8, encoded.count8);
        for (u32 i = 0; i < encoded.count8; ++i) {
            u8 byte = encoded.chars8[i];
            if (byte == '\\') {
                byte = '/';
            }
            dest[i] = byte;
        }
        res.len += encoded.count8;
        src += decoded.advance16;
    }

    return res;
}

struct string sys_get_write_path(struct arena *arena)
{
    u16 *p = NULL;
    /* TODO: cache this? */
    HRESULT res = SHGetKnownFolderPath(
        &FOLDERID_LocalAppData,
        0,
        NULL,
        &p
    );
    struct string path = { 0 };
    if (res == S_OK) {
        path = string_from_win32_path(arena, p);
    }
    CoTaskMemFree(p);
    return path;
}

b32 sys_is_file(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);
    DWORD attributes = GetFileAttributesW(path_wstr);
    scratch_end(scratch);
    return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY);
}

b32 sys_is_dir(struct string path)
{
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);
    DWORD attributes = GetFileAttributesW(path_wstr);
    scratch_end(scratch);
    return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY);
}

void sys_mkdir(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);
    int err_code = SHCreateDirectory(NULL, path_wstr);
    struct string err = { 0 };
    switch (err_code) {
        case ERROR_BAD_PATHNAME: {
            err = STR("Bad path name");
        } break;

        case ERROR_FILENAME_EXCED_RANGE: {
            err = STR("Path name too long");
        } break;

        case ERROR_FILE_EXISTS: {
            err = STR("A file already exists at this location");
        } break;

        case ERROR_CANCELLED: {
            err = STR("User canceled the operation");
        } break;

        default: break;
    }
    if (err.len > 0) {
        struct string msg = string_format(scratch.arena,
                                          STR("Failed to create directory \"%F\": %F"),
                                          FMT_STR(path),
                                          FMT_STR(err));
        sys_panic(msg);
    }
    scratch_end(scratch);
}

struct sys_file sys_file_open_read(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();

    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);
    HANDLE handle = CreateFileW(
        path_wstr,
        GENERIC_READ,
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    scratch_end(scratch);
    struct sys_file file = { (u64)handle };
    return file;
}

struct sys_file sys_file_open_read_wait(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);

    i32 delay_ms = 1;
    HANDLE handle;
    while ((handle = CreateFileW(path_wstr, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE) {
        if (GetLastError() == ERROR_SHARING_VIOLATION) {
            __profscope(file_share_conflict_delay);
            Sleep(delay_ms);
            if (delay_ms < 1024) {
                delay_ms *= 2;
            }
        } else {
            break;
        }
    }

    scratch_end(scratch);
    return (struct sys_file) { (u64)handle };
}

struct sys_file sys_file_open_write(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);

    /* TODO: Handle errors / non-existing file (handle == INVALID_HANDLE_VALUE) */
    HANDLE handle = CreateFileW(
        path_wstr,
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    scratch_end(scratch);
    return (struct sys_file) { (u64)handle };
}

struct sys_file sys_file_open_append(struct string path)
{
    __prof;
    struct temp_arena scratch = scratch_begin_no_conflict();
    wchar_t *path_wstr = wstr_from_string(scratch.arena, path);
    /* TODO: Handle errors / non-existing file (handle == INVALID_HANDLE_VALUE) */
    HANDLE handle = CreateFileW(
        path_wstr,
        FILE_APPEND_DATA,
        FILE_SHARE_READ,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );
    scratch_end(scratch);
    struct sys_file file = (struct sys_file) { (u64)handle };
    return file;
}

void sys_file_close(struct sys_file file)
{
    __prof;
    CloseHandle((HANDLE)file.handle);
}

struct buffer sys_file_read_all(struct arena *arena, struct sys_file file)
{
    __prof;
    i64 size = 0;
    GetFileSizeEx((HANDLE)file.handle, (PLARGE_INTEGER)&size);

    struct buffer buff = {
        .size = size,
        .data = NULL
    };

    if (size > 0) {
        /* ReadFile returns non-zero on success */
        /* TODO: error checking */
        arena_align(arena, 16);
        buff.data = arena_push_array(arena, u8, size);
        (UNUSED)ReadFile(
            (HANDLE)file.handle,
            buff.data,
            (DWORD)buff.size,
            NULL,  /* lpNumberOfBytesRead */
            NULL
        );
    }

    return buff;
}

void sys_file_write(struct sys_file file, struct buffer data)
{
    __prof;
    /* TODO: Check what the real data limit is and chunk sequentially based on
     * that (rather than failing) */
    if (data.size >= 0x7FFF) {
        struct temp_arena scratch = scratch_begin_no_conflict();
        sys_panic(string_format(scratch.arena,
                                STR("Tried to write too many bytes to disk (%F)"),
                                FMT_UINT(data.size)));
        scratch_end(scratch);
    }

    /* WriteFile returns TRUE on success */
    (UNUSED)WriteFile(
        (HANDLE)file.handle,
        data.data,
        (DWORD)data.size,
        NULL,  /* lpNumberOfBytesWritten */
        NULL
    );
}

u64 sys_file_get_size(struct sys_file file)
{
    LARGE_INTEGER li_file_size;
    GetFileSizeEx((HANDLE)file.handle, &li_file_size);
    return (u64)(li_file_size.QuadPart > 0 ? li_file_size.QuadPart : 0);
}

struct sys_file_time sys_file_get_time(struct sys_file file)
{
    __prof;

    /* Get file times */
    FILETIME ft_created;
    FILETIME ft_accessed;
    FILETIME ft_modified;
    b32 success = !!GetFileTime((HANDLE)file.handle, &ft_created, &ft_accessed, &ft_modified);
    if (success) {
        /* Convert file times to local file time */
        FileTimeToLocalFileTime(&ft_created, &ft_created);
        FileTimeToLocalFileTime(&ft_accessed, &ft_accessed);
        FileTimeToLocalFileTime(&ft_modified, &ft_modified);

        /* Convert local file times to system times */
        SYSTEMTIME st_created;
        SYSTEMTIME st_accessed;
        SYSTEMTIME st_modified;
        FileTimeToSystemTime(&ft_created, &st_created);
        FileTimeToSystemTime(&ft_accessed, &st_accessed);
        FileTimeToSystemTime(&ft_modified, &st_modified);

        return (struct sys_file_time) {
            .created = win32_time_to_sys_time(st_created),
            .accessed = win32_time_to_sys_time(st_accessed),
            .modified = win32_time_to_sys_time(st_modified)
        };
    } else {
        return (struct sys_file_time) { 0 };
    }
}

/* ========================== *
 * File map
 * ========================== */

struct sys_file_map sys_file_map_open_read(struct sys_file file)
{
    __prof;

    u64 size = sys_file_get_size(file);
    u8 *base_ptr = NULL;
    HANDLE map_handle = 0;

    if (size > 0) {
        map_handle = CreateFileMappingW(
            (HANDLE)file.handle,
            NULL,
            PAGE_READONLY,
            0,
            0,
            NULL
        );

        if (!map_handle) {
            ASSERT(false);
            return (struct sys_file_map) { 0 };
        }

        base_ptr = MapViewOfFile(
            map_handle,
            FILE_MAP_READ,
            0,
            0,
            0
        );

        if (!base_ptr) {
            /* Failed to create view */
            ASSERT(false);
            CloseHandle(map_handle);
            return (struct sys_file_map) { 0 };
        }
    } else {
        /* File is empty */
        ASSERT(false);
        return (struct sys_file_map) { 0 };
    }

    return (struct sys_file_map) {
        .handle = (u64)map_handle,
        .mapped_memory = BUFFER(size, base_ptr)
    };
}

void sys_file_map_close(struct sys_file_map map)
{
    if (map.mapped_memory.data) {
        UnmapViewOfFile(map.mapped_memory.data);
    }
    if (map.handle) {
        CloseHandle((HANDLE)map.handle);
    }
}

struct buffer sys_file_map_data(struct sys_file_map map)
{
    return map.mapped_memory;
}

/* ========================== *
 * File iter
 * ========================== */

struct win32_file_filter {
    HANDLE find_handle;
    wchar_t *filter_wstr;
};

struct sys_file_filter sys_file_filter_begin(struct arena *arena, struct string pattern)
{
    struct sys_file_filter filter = { 0 };
    struct win32_file_filter *filter_internal = arena_push_zero(arena, struct win32_file_filter);
    filter_internal->filter_wstr = wstr_from_string(arena, pattern);
    filter.handle = (u64)filter_internal;
    return filter;
}

b32 sys_file_filter_next(struct arena *arena, struct sys_file_filter *filter)
{
    struct win32_file_filter *filter_internal = (struct win32_file_filter *)filter->handle;

    WIN32_FIND_DATAW find_file_data = { 0 };
    b32 found = false;

    if (filter_internal->find_handle) {
        found = FindNextFileW(filter_internal->find_handle, &find_file_data);
    } else {
        filter_internal->find_handle = FindFirstFileExW(filter_internal->filter_wstr, FindExInfoStandard, &find_file_data, FindExSearchNameMatch, NULL, FIND_FIRST_EX_CASE_SENSITIVE | FIND_FIRST_EX_LARGE_FETCH);
        found = filter_internal->find_handle != INVALID_HANDLE_VALUE;
    }

    if (found) {
        struct string file_name = string_from_wstr(arena, find_file_data.cFileName);
        if (string_eq(file_name, STR(".")) || string_eq(file_name, STR(".."))) {
            /* Skip initial '.' and '..' matches */
            found = sys_file_filter_next(arena, filter);
        } else {
            filter->info.is_dir = find_file_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY;
            filter->info.filename = string_copy(arena, file_name);
        }
    }

    return found;
}

void sys_file_filter_end(struct sys_file_filter *filter)
{
    struct win32_file_filter *filter_internal = (struct win32_file_filter *)filter->handle;
    if (filter_internal->find_handle) {
        FindClose(filter_internal->find_handle);
    }
}

/* ========================== *
 * Window
 * ========================== */

INTERNAL void win32_update_window_from_system(struct win32_window *window);

INTERNAL void win32_window_process_event(struct win32_window *window, struct sys_event event)
{
    sys_mutex_lock(&window->event_callbacks_mutex);
    {
        for (u64 i = 0; i < window->event_callbacks_count; ++i) {
            window->event_callbacks[i](event);
        }
    }
    sys_mutex_unlock(&window->event_callbacks_mutex);
}

INTERNAL HWND win32_create_window(struct win32_window *window)
{
    /*
     * From martins (https://gist.github.com/mmozeiko/5e727f845db182d468a34d524508ad5f#file-win32_d3d11-c-L66-L70):
     * WS_EX_NOREDIRECTIONBITMAP flag here is needed to fix ugly bug with Windows 10
     * when window is resized and DXGI swap chain uses FLIP presentation model
     * DO NOT use it if you choose to use non-FLIP presentation model
     * read about the bug here: https://stackoverflow.com/q/63096226 and here: https://stackoverflow.com/q/53000291
     */
    DWORD exstyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP;

    /* TODO: Check for hwnd success */
    HWND hwnd = CreateWindowExW(
        exstyle,
        G.window_class.lpszClassName,
        L"",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        G.window_class.hInstance,
        NULL
    );

    /* Dark mode */
    BOOL dark_mode = true;
    DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, (LPCVOID)&dark_mode, sizeof(dark_mode));

    /* Set window as userdata */
    SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)window);

    return hwnd;
}

INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(window_thread_entry_point, arg)
{
    struct win32_window *window = (struct win32_window *)arg;

    /* Win32 limitation: Window must be initialized on same thread that processes events */
    window->hwnd = win32_create_window(window);
    window->tid = sys_thread_id();
    win32_update_window_from_system(window);
    BringWindowToTop(window->hwnd);

    sync_flag_set(&window->ready_sf);

    while (!window->event_thread_shutdown) {
        MSG msg = {0};
        GetMessageW(&msg, 0, 0, 0);

        /* Update cursor */
        if (GetFocus() == window->hwnd) {
            u32 cursor_flags = window->cursor_set_flags;

            /* Hide cursor */
            if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_HIDE) {
                while(ShowCursor(false) >= 0);
            }

            /* Show cursor */
            if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_SHOW) {
                while(ShowCursor(true) < 0);
            }

            /* Update position */
            if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_POSITION) {
                struct v2 window_space_pos = window->cursor_set_position;
                POINT p = { window_space_pos.x, window_space_pos.y };
                ClientToScreen(window->hwnd, &p);
                SetCursorPos(p.x, p.y);
            }

            /* Stop clipping cursor */
            if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP) {
                ClipCursor(NULL);
            }

            /* Clip cursor in window window */
            if (cursor_flags & WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP) {
                i32 left = window->x + math_round_to_int(window->cursor_clip_bounds.x);
                i32 right = left + math_round_to_int(window->cursor_clip_bounds.width);
                i32 top = window->y + math_round_to_int(window->cursor_clip_bounds.y);
                i32 bottom = top + math_round_to_int(window->cursor_clip_bounds.height);
                RECT clip = {
                    .left = clamp_i32(left, window->x, window->x + window->width),
                    .right = clamp_i32(right, window->x, window->x + window->width),
                    .top = clamp_i32(top, window->y, window->y + window->height),
                    .bottom = clamp_i32(bottom, window->y, window->y + window->height)
                };
                ClipCursor(&clip);
            }

            window->cursor_set_flags = 0;
        }

        /* Process message */
        switch (msg.message) {
            case WM_QUIT: {
                win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_QUIT });
            } break;

            default: {
                TranslateMessage(&msg);
                DispatchMessageW(&msg);
            } break;
        }
    }


    /* Destroy window hwnd */
    DestroyWindow(window->hwnd);
}

INTERNAL struct win32_window *win32_window_alloc(void)
{
    struct win32_window *window = NULL;

    sys_mutex_lock(&G.windows_mutex);
    {
        if (G.first_free_window) {
            window = G.first_free_window;
            G.first_free_window = window->next_free;
        } else {
            window = arena_push(&G.windows_arena, struct win32_window);
        }
    }
    sys_mutex_unlock(&G.windows_mutex);
    MEMZERO_STRUCT(window);

    /* Allocate sync flag */
    window->ready_sf = sync_flag_alloc();

    /* Allocate mutexes */
    window->settings_rw_mutex = sys_rw_mutex_alloc();
    window->event_callbacks_mutex = sys_mutex_alloc();

    /* Start window thread for processing events */
    window->event_thread = sys_thread_alloc(&window_thread_entry_point, window, STR("[P8] Window thread"));

    /* Wait for event thread to create actual window */
    sync_flag_wait(&window->ready_sf);

    return window;
}

INTERNAL void win32_window_release(struct win32_window *window)
{
    sys_mutex_lock(&G.windows_mutex);
    {
        window->next_free = G.first_free_window;
        G.first_free_window = window;

        /* Stop window thread */
        window->event_thread_shutdown = true;
        sys_thread_wait_release(&window->event_thread);

        /* Release mutexes */
        sys_mutex_release(&window->event_callbacks_mutex);
        sys_rw_mutex_release(&window->settings_rw_mutex);

        /* Release sync flag */
        sync_flag_release(&window->ready_sf);
    }
    sys_mutex_unlock(&G.windows_mutex);
}

INTERNAL void win32_update_window_from_system(struct win32_window *window)
{
    HWND hwnd = window->hwnd;

    RECT window_rect = {0};
    GetWindowRect(hwnd, &window_rect);

    RECT client_rect = {0};
    GetClientRect(hwnd, (LPRECT)&client_rect);
    ClientToScreen(hwnd, (LPPOINT)&client_rect.left);
    ClientToScreen(hwnd, (LPPOINT)&client_rect.right);

    /* TODO: Error if we can't get monitor info */
    /* Screen dimensions */
    MONITORINFO monitor_info = {sizeof(monitor_info)};
    GetMonitorInfo(MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY), &monitor_info);
    RECT monitor_rect = monitor_info.rcMonitor;
    window->monitor_width = monitor_rect.right - monitor_rect.left;
    window->monitor_height = monitor_rect.bottom - monitor_rect.top;

    /* Minimized / maximized */
    if (window->flags & SYS_WINDOW_FLAG_SHOWING) {
        WINDOWPLACEMENT placement = {sizeof(placement)};
        GetWindowPlacement(hwnd, &placement);
        if (placement.showCmd == SW_SHOWMINIMIZED) {
            window->settings.flags |= SYS_WINDOW_SETTINGS_FLAG_MINIMIZED;
        } else {
            window->settings.flags &= ~SYS_WINDOW_SETTINGS_FLAG_MINIMIZED;
        }
        if (placement.showCmd == SW_SHOWMAXIMIZED
            || ((window->settings.flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) && ((placement.flags & WPF_RESTORETOMAXIMIZED) != 0))) {
            window->settings.flags |= SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED;
        } else {
            window->settings.flags &= ~SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED;
        }
    }

    /* Window dimensions */
    i32 x = client_rect.left;
    i32 y = client_rect.top;
    i32 width = client_rect.right - client_rect.left;
    i32 height = client_rect.bottom - client_rect.top;
    if (!(window->settings.flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED)) {
        window->x = x;
        window->y = y;
        window->width = width;
        window->height = height;
        if (!(window->settings.flags & (SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED | SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN))) {
            /* Treat a window resize in non maximized/fullscreen mode as a
             * settings change.
             *
             * TODO: make sure we check for fullscreen here too if we ever
             * allow it. */
            window->settings.floating_x = x;
            window->settings.floating_y = y;
            window->settings.floating_width = width;
            window->settings.floating_height = height;
        }
    }
}

INTERNAL void win32_update_window_from_settings(struct win32_window *window, struct sys_window_settings *settings)
{
    HWND hwnd = window->hwnd;

    struct sys_window_settings old_settings = window->settings;
    window->settings = *settings;

    i32 show_cmd = SW_HIDE;
    if (window->flags & SYS_WINDOW_FLAG_SHOWING) {
        show_cmd = SW_NORMAL;
        if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED) {
            show_cmd = SW_SHOWMAXIMIZED;
        } else if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) {
            show_cmd = SW_MINIMIZE;
        }
    }

    RECT rect = { 0 };

    b32 old_fullscreen = old_settings.flags & SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN;
    b32 fullscreen = settings->flags & SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN;

    if (fullscreen) {
        if (!old_fullscreen) {
            /* Entering fullscreen */
            SetWindowLongPtrW(hwnd, GWL_STYLE, WS_POPUP);
        }
        rect = (RECT) {
            .left = 0,
            .top = 0,
            .right = window->monitor_width,
            .bottom = window->monitor_height
        };
    } else {
        if (old_fullscreen) {
            /* Leaving fullscreen */
            SetWindowLongPtrW(hwnd, GWL_STYLE, WS_OVERLAPPEDWINDOW);
        }
        rect = (RECT) {
            .left = settings->floating_x,
            .top = settings->floating_y,
            .right = settings->floating_x + settings->floating_width,
            .bottom = settings->floating_y + settings->floating_height
        };
        AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
    }

    WINDOWPLACEMENT wp = {
        .length = sizeof(WINDOWPLACEMENT),
        .showCmd = show_cmd,
        .rcNormalPosition = rect
    };
    SetWindowPlacement(hwnd, &wp);

    {
        struct temp_arena scratch = scratch_begin_no_conflict();
        wchar_t *title_wstr = wstr_from_string(scratch.arena, string_from_cstr(settings->title));
        SetWindowTextW(hwnd, title_wstr);
        scratch_end(scratch);
    }
}

INTERNAL void win32_window_wake(struct win32_window *window)
{
    /* Post a blank message to the window's thread message queue to wake it. */
    PostMessageW(window->hwnd, WM_NULL, 0, 0);
}

INTERNAL LRESULT CALLBACK win32_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
    struct win32_window *window = (struct win32_window *)GetWindowLongPtrW(hwnd, GWLP_USERDATA);

    if (!window) {
        return DefWindowProcW(hwnd, msg, wparam, lparam);
    }

    LRESULT result = 0;
    b32 is_release = false;
    switch (msg) {
        case WM_CLOSE:
        case WM_DESTROY: {
            win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_QUIT });
        } break;

        case WM_PAINT: {
            result = DefWindowProcW(hwnd, msg, wparam, lparam);
        } break;

        case WM_MOVE:
        case WM_MOVING:
        case WM_SIZE:
        case WM_SIZING: {
            win32_update_window_from_system(window);
            result = DefWindowProcW(hwnd, msg, wparam, lparam);
        } break;

        /* Keyboard buttons */
        case WM_SYSKEYUP:
        case WM_SYSKEYDOWN: {
            result = DefWindowProcW(hwnd, msg, wparam, lparam);
        } FALLTHROUGH;
        case WM_KEYUP:
        case WM_KEYDOWN: {
            WORD vk_code = LOWORD(wparam);
            b32 is_repeat = false;

            enum sys_event_kind event_kind = SYS_EVENT_KIND_NONE;
            if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) {
                event_kind = SYS_EVENT_KIND_BUTTON_DOWN;
                is_repeat = (lparam & 0x40000000) != 0;
            } else if (msg == WM_KEYUP || msg == WM_SYSKEYUP) {
                event_kind = SYS_EVENT_KIND_BUTTON_UP;
            }

            enum sys_btn button = SYS_BTN_NONE;
            if (vk_code < ARRAY_COUNT(G.vk_btn_table)) {
                button = G.vk_btn_table[vk_code];
            }

            win32_window_process_event(
                window,
                (struct sys_event) {
                    .kind = event_kind,
                    .button = button,
                    .is_repeat = is_repeat
                }
            );
        } break;

        /* Text */
        case WM_SYSCHAR:
        case WM_CHAR: {
            u16 utf16_char = (u32)wparam;

            /* Decode */
            u32 codepoint = 0;
            if (uni_is_utf16_high_surrogate(utf16_char)) {
                window->utf16_high_surrogate_last_input = utf16_char;
            } else if (uni_is_utf16_low_surrogate(utf16_char)) {
                u16 high = window->utf16_high_surrogate_last_input;
                u16 low = utf16_char;
                if (high) {
                    u16 utf16_pair_bytes[2] = { high, low };
                    struct uni_decode_utf16_result decoded = uni_decode_utf16((struct string16) { .len = ARRAY_COUNT(utf16_pair_bytes), .text = utf16_pair_bytes });
                    if (decoded.advance16 == 2 && decoded.codepoint < U32_MAX) {
                        codepoint = decoded.codepoint;
                    }
                }
                window->utf16_high_surrogate_last_input = 0;
            } else {
                window->utf16_high_surrogate_last_input = 0;
                codepoint = utf16_char;
            }

            if (codepoint) {
                if (codepoint == '\r') {
                    codepoint = '\n';  /* Just treat all \r as newline */
                }
                if((codepoint >= 32 && codepoint != 127) || codepoint == '\t' || codepoint == '\n') {
                    win32_window_process_event(
                        window,
                        (struct sys_event) {
                        .kind = SYS_EVENT_KIND_TEXT,
                        .text_codepoint = codepoint
                    }
                    );
                }
            }


        } break;

        /* Mouse buttons */
        case WM_LBUTTONUP:
        case WM_MBUTTONUP:
        case WM_RBUTTONUP:
        case WM_XBUTTONUP: {
            ReleaseCapture();
            is_release = true;
        } FALLTHROUGH;
        case WM_LBUTTONDOWN:
        case WM_MBUTTONDOWN:
        case WM_RBUTTONDOWN:
        case WM_XBUTTONDOWN: {
            if(!is_release) {
                SetCapture(hwnd);
            }

            enum sys_event_kind event_kind = is_release ? SYS_EVENT_KIND_BUTTON_UP : SYS_EVENT_KIND_BUTTON_DOWN;
            enum sys_btn button = 0;

            switch(msg) {
                case WM_LBUTTONUP: case WM_LBUTTONDOWN: button = SYS_BTN_M1; break;
                case WM_RBUTTONUP: case WM_RBUTTONDOWN: button = SYS_BTN_M2; break;
                case WM_MBUTTONUP: case WM_MBUTTONDOWN: button = SYS_BTN_M3; break;
                case WM_XBUTTONUP: case WM_XBUTTONDOWN: {
                    u32 wparam_xbutton = GET_XBUTTON_WPARAM(wparam);
                    if (wparam_xbutton == XBUTTON1) {
                        button = SYS_BTN_M3;
                    } else if (wparam_xbutton == XBUTTON2) {
                        button = SYS_BTN_M4;
                    }
                } break;
            }

            if (button) {
                win32_window_process_event(
                    window,
                    (struct sys_event) {
                        .kind = event_kind,
                        .button = button
                    }
                );
            }
        } break;

        /* Mouse wheel */
        case WM_MOUSEWHEEL: {
            int delta = GET_WHEEL_DELTA_WPARAM(wparam);
            i32 dir = delta >= 0 ? 1 : -1;
            enum sys_btn button = dir >= 0 ? SYS_BTN_MWHEELUP : SYS_BTN_MWHEELDOWN;
            for (i32 i = 0; i < (dir * delta); i += WHEEL_DELTA) {
                /* Send a button down & button up event simultaneously */
                win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_BUTTON_DOWN, .button = button });
                win32_window_process_event(window, (struct sys_event) { .kind = SYS_EVENT_KIND_BUTTON_UP, .button = button });
            }
        } break;

        /* Mouse move */
        case WM_MOUSEMOVE: {
            i32 x = GET_X_LPARAM(lparam);
            i32 y = GET_Y_LPARAM(lparam);
            win32_window_process_event(
                window,
                (struct sys_event) {
                    .kind = SYS_EVENT_KIND_CURSOR_MOVE,
                    .cursor_position = V2(x, y)
                }
            );
        } break;

        /* Raw mouse move */
        case WM_INPUT: {
            struct temp_arena scratch = scratch_begin_no_conflict();

            /* Read raw input buffer */
            UINT buff_size;
            GetRawInputData((HRAWINPUT)lparam, RID_INPUT, NULL, &buff_size, sizeof(RAWINPUTHEADER));
            u8 *buff = arena_push_array_zero(scratch.arena, u8, buff_size);
            if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, buff, &buff_size, sizeof(RAWINPUTHEADER)) != buff_size) {
                logf_error("GetRawInputData did not return correct size");
                break;
            }
            RAWINPUT *raw = (RAWINPUT *)buff;

            if (raw->header.dwType == RIM_TYPEMOUSE) {
                i32 x = raw->data.mouse.lLastX;
                i32 y = raw->data.mouse.lLastY;
                struct v2 delta = V2(x, y);
                win32_window_process_event(
                    window,
                    (struct sys_event) {
                        .kind = SYS_EVENT_KIND_MOUSE_MOVE,
                        .mouse_delta = delta
                    }
                );
            }

            scratch_end(scratch);
        } break;


        default: {
            result = DefWindowProcW(hwnd, msg, wparam, lparam);
        } break;
    }

    return result;
}

struct sys_window sys_window_alloc(void)
{
    __prof;
    struct sys_window w = {
        .handle = (u64)win32_window_alloc()
    };
    return w;
}

void sys_window_release(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    win32_window_release(window);
}

void sys_window_register_event_callback(struct sys_window *sys_window, sys_window_event_callback_func *func)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;

    sys_mutex_lock(&window->event_callbacks_mutex);
    {
        if (window->event_callbacks_count + 1 > ARRAY_COUNT(window->event_callbacks)) {
            sys_panic(STR("Too many window event callbacks registered"));
        } else {
            window->event_callbacks[window->event_callbacks_count++] = func;
        }
    }
    sys_mutex_unlock(&window->event_callbacks_mutex);
}

void sys_window_unregister_event_callback(struct sys_window *sys_window, sys_window_event_callback_func *func)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;

    sys_mutex_lock(&window->event_callbacks_mutex);
    {
        u64 count = window->event_callbacks_count;
        sys_window_event_callback_func *last = count > 0 ? window->event_callbacks[count - 1] : NULL;

        for (u64 i = 0; i < window->event_callbacks_count; ++i) {
            if (window->event_callbacks[i] == func) {
                if (func != last) {
                    /* Swap with last element */
                    window->event_callbacks[i] = last;
                }
                --window->event_callbacks_count;
            }
        }
    }
    sys_mutex_unlock(&window->event_callbacks_mutex);
}

void sys_window_update_settings(struct sys_window *sys_window, struct sys_window_settings *settings)
{
    __prof;
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    sys_rw_mutex_lock_exclusive(&window->settings_rw_mutex);
    {
        win32_update_window_from_settings(window, settings);
    }
    sys_rw_mutex_unlock_exclusive(&window->settings_rw_mutex);
}

/* FIXME: Lock settings mutex for these functions */

struct sys_window_settings sys_window_get_settings(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    return window->settings;
}

void sys_window_show(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    HWND hwnd = window->hwnd;
    sys_rw_mutex_lock_exclusive(&window->settings_rw_mutex);
    {
        i32 show_cmd = SW_NORMAL;
        struct sys_window_settings *settings = &window->settings;
        if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MAXIMIZED) {
            show_cmd = SW_SHOWMAXIMIZED;
        } else if (settings->flags & SYS_WINDOW_SETTINGS_FLAG_MINIMIZED) {
            show_cmd = SW_MINIMIZE;
        }
        window->flags |= SYS_WINDOW_FLAG_SHOWING;
        ShowWindow(hwnd, show_cmd);
        BringWindowToTop(hwnd);
    }
    sys_rw_mutex_unlock_exclusive(&window->settings_rw_mutex);
}

struct v2 sys_window_get_size(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    return V2((f32)window->width, (f32)window->height);
}

struct v2 sys_window_get_monitor_size(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    return V2((f32)window->monitor_width, (f32)window->monitor_height);
}

u64 sys_window_get_internal_handle(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    return (u64)window->hwnd;
}

void sys_window_cursor_set_pos(struct sys_window *sys_window, struct v2 pos)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    window->cursor_set_position = pos;
    window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_POSITION;
    win32_window_wake(window);
}

void sys_window_cursor_show(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_SHOW;
    win32_window_wake(window);
}

void sys_window_cursor_hide(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_HIDE;
    win32_window_wake(window);
}

void sys_window_cursor_enable_clip(struct sys_window *sys_window, struct rect bounds)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    window->cursor_clip_bounds = bounds;
    window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_ENABLE_CLIP;
    win32_window_wake(window);
}

void sys_window_cursor_disable_clip(struct sys_window *sys_window)
{
    struct win32_window *window = (struct win32_window *)sys_window->handle;
    window->cursor_set_flags |= WIN32_WINDOW_CURSOR_SET_FLAG_DISABLE_CLIP;
    win32_window_wake(window);
}

/* ========================== *
 * Mutex
 * ========================== */

struct sys_mutex sys_mutex_alloc(void)
{
    __prof;
    SRWLOCK srwlock = SRWLOCK_INIT;
    struct sys_mutex mutex = {
        .handle = *(u64 *)&srwlock
    };
    return mutex;
}

void sys_mutex_release(struct sys_mutex *mutex)
{
    __prof;
    (UNUSED)mutex;
    /* Mutex must be unlocked */
    ASSERT(mutex->owner_tid == 0);
}

void sys_mutex_lock(struct sys_mutex *mutex)
{
    __prof;
    AcquireSRWLockExclusive((SRWLOCK *)&mutex->handle);
#if RTC
    mutex->owner_tid = (u64)GetCurrentThreadId();
#endif
}

void sys_mutex_unlock(struct sys_mutex *mutex)
{
    __prof;
#if RTC
    mutex->owner_tid = 0;
#endif
    ReleaseSRWLockExclusive((SRWLOCK *)&mutex->handle);
}

#if RTC
void sys_mutex_assert_locked(struct sys_mutex *mutex)
{
    ASSERT(mutex->owner_tid == (u64)GetCurrentThreadId());
}
#endif

/* ========================== *
 * RW Mutex
 * ========================== */

struct sys_rw_mutex sys_rw_mutex_alloc(void)
{
    __prof;
    SRWLOCK srwlock;
    InitializeSRWLock(&srwlock);
    struct sys_rw_mutex mutex = {
        .handle = *(u64 *)&srwlock
    };
    return mutex;
}

void sys_rw_mutex_release(struct sys_rw_mutex *mutex)
{
    __prof;
    (UNUSED)mutex;
    /* Mutex must be unlocked */
    ASSERT(mutex->owner_tid == 0);
    ASSERT(atomic_i64_eval(&mutex->num_shared) == 0);
}

void sys_rw_mutex_lock_exclusive(struct sys_rw_mutex *mutex)
{
    __prof;
    AcquireSRWLockExclusive((SRWLOCK *)&mutex->handle);
#if RTC
    mutex->owner_tid = (u64)GetCurrentThreadId();
    GetThreadDescription(GetCurrentThread(), &mutex->owner_name);
#endif
}

void sys_rw_mutex_unlock_exclusive(struct sys_rw_mutex *mutex)
{
    __prof;
#if RTC
    mutex->owner_name = L"None";
    mutex->owner_tid = 0;
#endif
    ReleaseSRWLockExclusive((SRWLOCK *)&mutex->handle);
}

void sys_rw_mutex_lock_shared(struct sys_rw_mutex *mutex)
{
    __prof;
    AcquireSRWLockShared((SRWLOCK *)&mutex->handle);
#if RTC
    atomic_i64_inc_eval(&mutex->num_shared);
#endif
}

void sys_rw_mutex_unlock_shared(struct sys_rw_mutex *mutex)
{
    __prof;
#if RTC
    atomic_i64_dec_eval(&mutex->num_shared);
#endif
    ReleaseSRWLockShared((SRWLOCK *)&mutex->handle);
}

#if RTC
void sys_rw_mutex_assert_locked_exclusive(struct sys_rw_mutex *mutex)
{
    ASSERT(mutex->owner_tid == (u64)GetCurrentThreadId());
}
#endif

/* ========================== *
 * Condition variable
 * ========================== */

INTERNAL struct win32_condition_variable *win32_condition_variable_alloc(void)
{
    __prof;
    struct win32_condition_variable *cv = NULL;
    {
        sys_mutex_lock(&G.condition_variables_mutex);
        {
            if (G.first_free_condition_variable) {
                cv = G.first_free_condition_variable;
                G.first_free_condition_variable = cv->next_free;
            } else {
                cv = arena_push_zero(&G.condition_variables_arena, struct win32_condition_variable);
            }
        }
        sys_mutex_unlock(&G.condition_variables_mutex);
    }

    MEMZERO_STRUCT(cv);
    InitializeConditionVariable(&cv->condition_variable);

    return cv;
}

INTERNAL void win32_condition_variable_release(struct win32_condition_variable *w32cv)
{
    __prof;
    sys_mutex_lock(&G.condition_variables_mutex);
    {
        w32cv->next_free = G.first_free_condition_variable;
        G.first_free_condition_variable = w32cv;
    }
    sys_mutex_unlock(&G.condition_variables_mutex);

}

struct sys_condition_variable sys_condition_variable_alloc(void)
{
    __prof;
    struct sys_condition_variable cv = {
        .handle = (u64)win32_condition_variable_alloc()
    };
    return cv;
}

void sys_condition_variable_release(struct sys_condition_variable *cv)
{
    __prof;
    /* Condition variable must not have any sleepers (signal before releasing) */
    ASSERT(atomic_i64_eval(&cv->num_waiters) == 0);
    win32_condition_variable_release((struct win32_condition_variable *)cv->handle);
}

void sys_condition_variable_wait(struct sys_condition_variable *cv, struct sys_mutex *mutex)
{
    __prof;
#if RTC
    atomic_i64_inc_eval(&cv->num_waiters);
    mutex->owner_tid = 0;
#endif
    SleepConditionVariableSRW((PCONDITION_VARIABLE)cv->handle, (SRWLOCK *)&mutex->handle, INFINITE, 0);
#if RTC
    mutex->owner_tid = (u64)GetCurrentThreadId();
    atomic_i64_dec_eval(&cv->num_waiters);
#endif
}

void sys_condition_variable_wait_time(struct sys_condition_variable *cv, struct sys_mutex *mutex, f64 seconds)
{
    __prof;
#if RTC
    atomic_i64_inc_eval(&cv->num_waiters);
    mutex->owner_tid = 0;
#endif
    u32 ms = (u32)math_round_to_int((f32)seconds * 1000.f);
    SleepConditionVariableSRW((PCONDITION_VARIABLE)cv->handle, (SRWLOCK *)&mutex->handle, ms, 0);
#if RTC
    mutex->owner_tid = (u64)GetCurrentThreadId();
    atomic_i64_dec_eval(&cv->num_waiters);
#endif
}

void sys_condition_variable_signal(struct sys_condition_variable *cv)
{
    __prof;
    WakeConditionVariable((PCONDITION_VARIABLE)cv->handle);
}

void sys_condition_variable_broadcast(struct sys_condition_variable *cv)
{
    __prof;
    WakeAllConditionVariable((PCONDITION_VARIABLE)cv->handle);
}

/* ========================== *
 * Thread local storage
 * ========================== */

struct win32_tls {
    HANDLE sleep_timer;
    struct thread_local_store store;
};

INTERNAL void win32_thread_set_tls(struct win32_tls *ctx)
{
    /* TODO: Fail if error */
    TlsSetValue(G.thread_tls_index, (LPVOID)ctx);
}

INTERNAL struct win32_tls *win32_thread_get_tls(void)
{
    /* TODO: Fail if error */
    return TlsGetValue(G.thread_tls_index);
}

INTERNAL struct win32_tls win32_tls_alloc(void)
{
    struct win32_tls tls = { 0 };
    tls.sleep_timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
    tls.store = thread_local_store_alloc();
    return tls;
}

INTERNAL void win32_tls_release(struct win32_tls *tls)
{
    thread_local_store_release(&tls->store);
    CloseHandle(tls->sleep_timer);
}

struct thread_local_store *sys_thread_get_thread_local_store(void)
{
    struct win32_tls *thread_ctx = (struct win32_tls *)win32_thread_get_tls();
    return &thread_ctx->store;
}

/* ========================== *
 * Threads
 * ========================== */

INTERNAL struct win32_thread *win32_thread_alloc_assume_locked(void)
{
    struct win32_thread *t = NULL;
    if (G.threads_first_free) {
        t = G.threads_first_free;
        G.threads_first_free = t->next;
        MEMZERO_STRUCT(t);
    } else {
        t = arena_push_zero(&G.threads_arena, struct win32_thread);
        t->gen = 1;
    }
    if (!G.threads_first) {
        G.threads_first = t;
    } else {
        G.threads_last->next = t;
    }
    t->prev = G.threads_last;
    G.threads_last = t;
    return t;
}

INTERNAL void win32_thread_release_assume_locked(struct win32_thread *t)
{
    if (t->prev) {
        t->prev->next = t->next;
    }
    if (t->next) {
        t->next->prev = t->prev;
    }
    if (G.threads_first == t) {
        G.threads_first = t->next;
    }
    if (G.threads_last == t) {
        G.threads_last = t->prev;
    }
    u64 new_gen = t->gen + 1;
    *t = (struct win32_thread) {
        .next = G.threads_first_free,
        .gen = new_gen
    };
}

INTERNAL struct win32_thread *win32_thread_from_sys_thread_assume_locked(struct sys_thread st)
{
    u64 gen = st.handle[0];
    struct win32_thread *t = (struct win32_thread *)st.handle[1];
    if (t->gen == gen) {
        return t;
    } else {
        return NULL;
    }
}

INTERNAL struct sys_thread sys_thread_from_win32_thread_assume_locked(struct win32_thread *t)
{
    return (struct sys_thread) {
        .handle[0] = t->gen,
        .handle[1] = (u64)t
    };
}

INTERNAL DWORD WINAPI win32_thread_proc(LPVOID vt)
{
    struct win32_thread *t = (struct win32_thread *)vt;

    /* Initialize COM */
    CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

    /* Initialize TLS */
    struct win32_tls tls = win32_tls_alloc();
    win32_thread_set_tls(&tls);

    /* Set thread name */
    struct string thread_name = string_from_cstr(t->thread_name_cstr);
    if (thread_name.len > 0) {
        struct temp_arena scratch = scratch_begin_no_conflict();
        wchar_t *wc_thread_name = wstr_from_string(scratch.arena, thread_name);
        SetThreadDescription(GetCurrentThread(), wc_thread_name);
        scratch_end(scratch);
    }

    logf_info("New thread \"%F\" created with ID %F", FMT_STR(thread_name), FMT_UINT(sys_thread_id()));

    /* Start thread */
    t->entry_point(t->thread_data);

    /* Release thread object */
    sys_rw_mutex_lock_exclusive(&G.threads_rw_mutex);
    {
        win32_thread_release_assume_locked(t);
    }
    sys_rw_mutex_unlock_exclusive(&G.threads_rw_mutex);

    /* Release TLS */
    win32_tls_release(&tls);

    /* Uninitialize COM */
    CoUninitialize();

    return 0;
}

struct sys_thread sys_thread_alloc(sys_thread_entry_point_func *entry_point, void *thread_data, struct string thread_name)
{
    ASSERT(entry_point != NULL);
    logf_info("Creating thread \"%F\"", FMT_STR(thread_name));

    struct sys_thread res = { 0 };
    sys_rw_mutex_lock_exclusive(&G.threads_rw_mutex);
    {
        /* Allocate thread object */
        struct win32_thread *t = win32_thread_alloc_assume_locked();
        t->entry_point = entry_point;
        t->thread_data = thread_data;

        /* Copy thread name to params */
        cstr_buff_from_string(BUFFER_FROM_ARRAY(t->thread_name_cstr), thread_name);

        t->handle = CreateThread(
            NULL,
            SYS_THREAD_STACK_SIZE,
            win32_thread_proc,
            t,
            0,
            NULL
        );

        if (!t->handle) {
            sys_panic(STR("Failed to create thread"));
        }

        res = sys_thread_from_win32_thread_assume_locked(t);
    }
    sys_rw_mutex_unlock_exclusive(&G.threads_rw_mutex);
    return res;
}

void sys_thread_wait_release(struct sys_thread *thread)
{
    HANDLE handle = 0;

    /* Lookup */
    sys_rw_mutex_lock_shared(&G.threads_rw_mutex);
    {
        struct win32_thread *t = win32_thread_from_sys_thread_assume_locked(*thread);
        if (t) {
            handle = t->handle;
        }
    }
    sys_rw_mutex_unlock_shared(&G.threads_rw_mutex);

    /* Wait */
    if (handle) {
        DWORD res = WaitForSingleObject(handle, INFINITE);
        CloseHandle(handle);

        ASSERT(res != WAIT_FAILED);
        (UNUSED)res;
    }
}

u32 sys_thread_id(void)
{
    return GetCurrentThreadId();
}

#if RTC
void sys_thread_assert(u32 tid)
{
    ASSERT(sys_thread_id() == tid);
}
#endif

/* ========================== *
 * Message box
 * ========================== */

void sys_message_box(enum sys_message_box_kind kind, struct string message)
{
    struct temp_arena scratch = scratch_begin_no_conflict();

    wchar_t *message_wstr = wstr_from_string(scratch.arena, message);
    const wchar_t *title = L"";
    UINT mbox_type = MB_SETFOREGROUND;

    switch (kind) {
        case SYS_MESSAGE_BOX_KIND_OK: {
            mbox_type |= MB_ICONINFORMATION;
        } break;

        case SYS_MESSAGE_BOX_KIND_WARNING: {
            title = L"Warning";
            mbox_type |= MB_ICONWARNING;
        } break;

        case SYS_MESSAGE_BOX_KIND_ERROR: {
            title = L"Error";
            mbox_type |= MB_ICONERROR;
        } break;

        case SYS_MESSAGE_BOX_KIND_FATAL: {
            title = L"Fatal error";
            mbox_type |= MB_ICONSTOP;
        } break;
    }

    logf_info("Showing message box: \"%F\"", FMT_STR(message));
    MessageBoxExW(NULL, message_wstr, title, mbox_type, 0);

    scratch_end(scratch);
}

/* ========================== *
 * Clipboard
 * ========================== */

void sys_set_clipboard_text(struct string str)
{
    if (OpenClipboard(0)) {
        struct temp_arena scratch = scratch_begin_no_conflict();
        struct string16 str16 = string16_from_string(scratch.arena, str);
        u64 str16_size_bytes = str16.len * 2;
        EmptyClipboard();
        HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, str16_size_bytes + 1);
        if (handle) {
            u16 *dest_wstr = (u16 *)GlobalLock(handle);
            MEMCPY(dest_wstr, str16.text, str16_size_bytes);
            dest_wstr[str16.len] = 0;
            GlobalUnlock(handle);
            SetClipboardData(CF_UNICODETEXT, handle);
        }
        CloseClipboard();
        scratch_end(scratch);
    }
}

struct string sys_get_clipboard_text(struct arena *arena)
{
    struct string res = { 0 };
    if (IsClipboardFormatAvailable(CF_UNICODETEXT) && OpenClipboard(0)) {
        HANDLE handle = GetClipboardData(CF_UNICODETEXT);
        if (handle) {
            u16 *src_wstr = (u16 *)GlobalLock(handle);
            res = string_from_string16(arena, string16_from_wstr(src_wstr));
            GlobalUnlock(handle);
        }
        CloseClipboard();
    }
    return res;
}

/* ========================== *
 * Util
 * ========================== */

u32 sys_num_logical_processors(void)
{
    return GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
}

void sys_exit(void)
{
    ExitProcess(1);
}

u32 sys_rand_u32(void)
{
    u32 v;
    RtlGenRandom(&v, sizeof(v));
    return v;
}

void sys_panic(struct string msg)
{
    ASSERT(false);
    if (atomic_i32_eval_compare_exchange(&G.panicking, 0, 1) == 0) {
        log_panic(msg);

        wchar_t *wstr = G.panic_wstr;
        u64 wstr_len = 0;

        wchar_t prefix[] = L"A fatal error has occured and the application needs to exit:\n\n";
        MEMCPY(wstr, prefix, min_u64(ARRAY_COUNT(G.panic_wstr), (ARRAY_COUNT(prefix) << 1)));
        wstr_len += ARRAY_COUNT(prefix) - 1;

        struct string str8 = msg;
        u64 pos8 = 0;
        while (pos8 < str8.len) {
            struct string str8_remaining = { .len = (str8.len - pos8), .text = str8.text + pos8 };
            struct uni_decode_utf8_result decoded = uni_decode_utf8(str8_remaining);
            struct uni_encode_utf16_result encoded = uni_encode_utf16(decoded.codepoint);

            u64 wstr_new_len = wstr_len + encoded.count16;
            if (wstr_new_len < (ARRAY_COUNT(G.panic_wstr) - 1)) {
                u16 *dest = wstr + wstr_len;
                MEMCPY(dest, encoded.chars16, (encoded.count16 << 1));
                wstr_len = wstr_new_len;
                pos8 += decoded.advance8;
            } else {
                break;
            }
        }

        wstr[wstr_len] = 0;

        WRITE_BARRIER();
        SetEvent(G.panic_event);

        /* Wait for thread to be terminated */
        if (GetCurrentThreadId() != G.main_thread_id) {
            Sleep(INFINITE);
        }
    }
}

/* ========================== *
 * Sleep
 * ========================== */

/* https://blog.bearcats.nl/perfect-sleep-function/ */

INTERNAL void win32_precise_sleep_timer(f64 seconds, HANDLE timer)
{
    __prof;

    /* TODO: Does the high frequency timer even require setting / scaling of
     * timeBeginPeriod/scheduler_period_ms? There isn't much documentation. */

    i64 qpc_per_second = G.timer_frequency.QuadPart;;
    i32 scheduler_period_ms = G.scheduler_period_ms;

    LARGE_INTEGER qpc;
    QueryPerformanceCounter(&qpc);
    INT64 target_qpc = (INT64)(qpc.QuadPart + seconds * qpc_per_second);

    /* TODO: Maybe increase tolerance for higher precision but more power usage */
    //const double tolerance = 0.001200 * scheduler_period_ms;
    const double tolerance = 0.000520 * scheduler_period_ms;

    INT64 max_ticks = (INT64)scheduler_period_ms * 9500;
    while (true) {
        __profscope(win32_sleep_part);
        /* Break sleep up into parts that are lower than scheduler period */
        double remaining_seconds = (double)(target_qpc - qpc.QuadPart) / (double)qpc_per_second;
        INT64 sleep_ticks = (INT64)((remaining_seconds - tolerance) * 10000000);
        if (sleep_ticks <= 0) {
            break;
        }
        LARGE_INTEGER due;
        due.QuadPart = -(sleep_ticks > max_ticks ? max_ticks : sleep_ticks);
        SetWaitableTimerEx(timer, &due, 0, NULL, NULL, NULL, 0);
        WaitForSingleObject(timer, INFINITE);
        QueryPerformanceCounter(&qpc);
    }

    /* Spin for any remaining time */
    {
        __profscope(sleep_spin);
        while (qpc.QuadPart < target_qpc) {
            YieldProcessor();
            QueryPerformanceCounter(&qpc);
        }
    }
}

INTERNAL void win32_precise_sleep_legacy(f64 seconds)
{
    __prof;
    i64 qpc_per_second = G.timer_frequency.QuadPart;
    i32 scheduler_period_ms = G.scheduler_period_ms;

    LARGE_INTEGER qpc;
    QueryPerformanceCounter(&qpc);
    i64 target_qpc = (i64)(qpc.QuadPart + seconds * qpc_per_second);

    /* TODO: Calculate tolerance */

    /* TODO: Maybe increase tolerance for higher precision but more power usage */
    //const double tolerance = 1.02;
    const double tolerance = 0.52 * scheduler_period_ms;

    /* Sleep */
    f64 sleep_ms = (seconds * 1000) - tolerance;
    i32 sleep_slices = (i32)(sleep_ms / scheduler_period_ms);
    if (sleep_slices > 0) {
        __profscope(win32_sleep);
        Sleep((DWORD)sleep_slices * scheduler_period_ms);
    }

    {
        __profscope(win32_qpc);
        QueryPerformanceCounter(&qpc);
    }

    /* Spin for any remaining time */
    {
        __profscope(sleep_spin);
        while (qpc.QuadPart < target_qpc) {
            YieldProcessor();
            QueryPerformanceCounter(&qpc);
        }
    }
}

void sys_sleep_precise(f64 seconds)
{
    __prof;
    HANDLE timer = win32_thread_get_tls()->sleep_timer;
    if (timer) {
        /* Use newer sleeping method */
        win32_precise_sleep_timer(seconds, timer);
    } else {
        /* Fall back to older sleep method if CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
         * is not available due to older windows version */
        win32_precise_sleep_legacy(seconds);
    }
}

void sys_sleep(f64 seconds)
{
    __prof;
    u32 ms = max_u32(1, math_round_to_int((f32)(seconds * 1000.0)));
    Sleep(ms);
}

/* ========================== *
 * Entry point
 * ========================== */

INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(app_thread_entry_point, arg)
{
    (UNUSED)arg;
    app_entry_point();
}

int CALLBACK wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev_instance, _In_ LPWSTR command_line, _In_ int show_code)
{
    (UNUSED)instance;
    (UNUSED)prev_instance;
    (UNUSED)command_line;
    (UNUSED)show_code;

    const wchar_t *error_msg = NULL;

    /* ========================== *
     * Win32 setup
     * ========================== */

    G.main_thread_id = GetCurrentThreadId();
    SetThreadDescription(GetCurrentThread(), L"Main thread");

    /* Set up panic event */
    G.panic_event = CreateEventW(NULL, true, false, NULL);

    /* Query system info */
    GetSystemInfo(&G.info);
    QueryPerformanceFrequency(&G.timer_frequency);
    QueryPerformanceCounter(&G.timer_start);
    {
        TIMECAPS caps;
        timeGetDevCaps(&caps, sizeof caps);
        G.scheduler_period_ms = (i32)caps.wPeriodMin;
    }

    /* Set up timing period */
    timeBeginPeriod(G.scheduler_period_ms);

    /* Set up condition variables */
    G.condition_variables_mutex = sys_mutex_alloc();
    G.condition_variables_arena = arena_alloc(GIGABYTE(64));

    /* Set up threads */
    G.threads_rw_mutex = sys_rw_mutex_alloc();
    G.threads_arena = arena_alloc(GIGABYTE(64));

    /* Set up windows */
    G.windows_mutex = sys_mutex_alloc();
    G.windows_arena = arena_alloc(GIGABYTE(64));

    /* Set up TLS index */
    G.thread_tls_index = TlsAlloc();
    if (G.thread_tls_index == TLS_OUT_OF_INDEXES) {
        /* TODO: GetLastError */
        error_msg = L"Platform initialization error: TLS_OUT_OF_INDEXES";
        goto abort;
    }

    /* Initialize vk table */
    win32_init_vk_btn_table();

    /* Create window class */
    {
        /* Register the window class */
        WNDCLASSEXW *wc = &G.window_class;
        wc->cbSize = sizeof(WNDCLASSEX);
        wc->lpszClassName = WINDOW_CLASS_NAME;
        wc->hCursor = LoadCursor(NULL, IDC_ARROW);
        wc->style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
        //wc->hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
        wc->lpfnWndProc = win32_window_proc;
        wc->hInstance = instance;

        /* Use first icon resource as window icon (same as explorer) */
        wchar_t path[4096] = { 0 };
        GetModuleFileNameW(instance, path, ARRAY_COUNT(path));
        ExtractIconExW(path, 0, &wc->hIcon, &wc->hIconSm, 1);

        if (!RegisterClassExW(wc)) {
            /* TODO: GetLastError */
            error_msg = L"Failed to register window class";
            goto abort;
        }
    }

    /* Register raw input */
    {
        RAWINPUTDEVICE rid = (RAWINPUTDEVICE) {
            .usUsagePage = 0x01,        /* HID_USAGE_PAGE_GENERIC */
            .usUsage = 0x02,            /* HID_USAGE_GENERIC_MOUSE */
            //.dwFlags = RIDEV_NOLEGACY   /* Adds mouse and also ignores legacy mouse messages */
        };

        if (!RegisterRawInputDevices(&rid, 1, sizeof(rid))) {
            /* TODO: GetLastError */
            error_msg = L"Failed to register raw input device";
            goto abort;
        }
    }

    /* ========================== *
     * App thread setup
     * ========================== */

    /* Initialize main thread context. */
    struct win32_tls main_thread_tls = win32_tls_alloc();
    win32_thread_set_tls(&main_thread_tls);

    /* Call app thread and wait for return */
    {
        /* Start app thread */
        struct sys_thread app_thread = sys_thread_alloc(&app_thread_entry_point, NULL, STR("[P9] App thread"));

        /* Get app thread handle */
        HANDLE app_thread_handle = 0;
        sys_rw_mutex_lock_shared(&G.threads_rw_mutex);
        {
            struct win32_thread *wt = win32_thread_from_sys_thread_assume_locked(app_thread);
            app_thread_handle = wt->handle;
        }
        sys_rw_mutex_unlock_shared(&G.threads_rw_mutex);

        /* Wait for either app thread exit or panic */
        if (app_thread_handle) {
            HANDLE wait_handles[] = {
                app_thread_handle,
                G.panic_event
            };
            DWORD res = WaitForMultipleObjects(ARRAY_COUNT(wait_handles), wait_handles, false, INFINITE);
            ASSERT(res != WAIT_FAILED);
            (UNUSED)res;
        }
    }

    /* Check if panicking */
    if (atomic_i32_eval(&G.panicking)) {
        /* Wait for panic message to be ready */
        WaitForSingleObject(G.panic_event, INFINITE);
        /* Force stop threads */
        for (struct win32_thread *t = G.threads_last; t; t = t->prev) {
            HANDLE handle = t->handle;
            TerminateThread(handle, 0);
            CloseHandle(handle);
        }
        /* Set error and abort */
        error_msg = G.panic_wstr;
        goto abort;
    }

    /* Release main thread context */
    win32_tls_release(&main_thread_tls);

    /* ========================== *
     * Abort
     * ========================== */

    abort:

    if (error_msg) {
        MessageBoxExW(NULL, error_msg,  L"Fatal error", MB_ICONSTOP | MB_SETFOREGROUND, 0);
        ASSERT(false);
        return 1;
    }

    return 0;
}

/* ========================== *
 * CRT Stub
 * ========================== */

#if !CRTLIB

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmissing-variable-declarations"
#pragma clang diagnostic ignored "-Wmissing-prototypes"

/* Enable floating point */
__attribute((used))
int _fltused;

__attribute((used))
void __stdcall wWinMainCRTStartup(void)
{
    int result = wWinMain(GetModuleHandle(0), 0, GetCommandLineW(), 0);
    ExitProcess(result);
}

#pragma clang diagnostic pop

#endif /* !CRTLIB */
