#include "user.h"
#include "renderer.h"
#include "font.h"
#include "sprite.h"
#include "draw.h"
#include "intrinsics.h"
#include "app.h"
#include "game.h"
#include "asset_cache.h"
#include "string.h"
#include "scratch.h"
#include "math.h"
#include "sys.h"
#include "world.h"
#include "entity.h"
#include "mixer.h"
#include "atomic.h"
#include "app.h"

struct bind_state {
    b32 is_held;       /* Is this bind held down this frame */
    u32 num_presses;   /* How many times was this bind pressed since last frame */
    u32 num_releases;  /* How many times was this bind released since last frame */
};

struct blend_tick {
    struct blend_tick *next;
    struct blend_tick *prev;
    struct world world;
};

GLOBAL struct {
    struct atomic_i32 user_thread_shutdown;
    struct sys_thread user_thread;

    struct arena arena;

    struct sys_window *window;
    struct renderer_canvas *world_canvas;
    struct renderer_canvas *viewport_bg_canvas;
    struct renderer_canvas *viewport_canvas;
    struct xform world_view;

    struct blend_tick *head_free_blend_tick;
    struct blend_tick *head_blend_tick;
    struct world world;

    struct bind_state bind_states[USER_BIND_KIND_COUNT];

    b32 debug_camera;
    b32 debug_camera_panning;
    struct v2 debug_camera_pan_start;

    b32 debug_draw;

    /* User thread input */
    struct sys_mutex sys_events_mutex;
    struct arena sys_events_arena;

    /* Per-frame */
    f64 time;
    f64 dt;
    struct v2 screen_size;
    struct v2 screen_cursor;
    struct v2 viewport_screen_offset;
    struct v2 viewport_size;
    struct v2 viewport_center;
    struct v2 viewport_cursor;
    struct v2 world_cursor;
} G = { 0 }, DEBUG_ALIAS(G, G_user);

/* ========================== *
 * Bind state
 * ========================== */

/* TODO: Remove this */

GLOBAL READONLY enum user_bind_kind g_binds[SYS_BTN_COUNT] = {
    [SYS_BTN_W] = USER_BIND_KIND_MOVE_UP,
    [SYS_BTN_S] = USER_BIND_KIND_MOVE_DOWN,
    [SYS_BTN_A] = USER_BIND_KIND_MOVE_LEFT,
    [SYS_BTN_D] = USER_BIND_KIND_MOVE_RIGHT,

    /* Testing */

    [SYS_BTN_C]             = USER_BIND_KIND_DEBUG_CLEAR,
    [SYS_BTN_F6]            = USER_BIND_KIND_DEBUG_DRAW,
    [SYS_BTN_F7]            = USER_BIND_KIND_DEBUG_CAMERA,
    [SYS_BTN_F11]           = USER_BIND_KIND_FULLSCREEN,
    [SYS_BTN_MWHEELUP]      = USER_BIND_KIND_ZOOM_IN,
    [SYS_BTN_MWHEELDOWN]    = USER_BIND_KIND_ZOOM_OUT,
    [SYS_BTN_M3]            = USER_BIND_KIND_PAN,
    [SYS_BTN_CTRL]          = USER_BIND_KIND_CTRL_TEST
};

/* ========================== *
 * Startup
 * ========================== */

INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown);
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg);
INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event);

struct user_startup_receipt user_startup(struct work_startup_receipt *work_sr,
                                         struct renderer_startup_receipt *renderer_sr,
                                         struct font_startup_receipt *font_sr,
                                         struct sprite_startup_receipt *sprite_sr,
                                         struct draw_startup_receipt *draw_sr,
                                         struct game_startup_receipt *game_sr,
                                         struct asset_cache_startup_receipt *asset_cache_sr,
                                         struct mixer_startup_receipt *mixer_sr,
                                         struct sys_window *window)
{
    (UNUSED)work_sr;
    (UNUSED)renderer_sr;
    (UNUSED)font_sr;
    (UNUSED)sprite_sr;
    (UNUSED)draw_sr;
    (UNUSED)game_sr;
    (UNUSED)asset_cache_sr;
    (UNUSED)mixer_sr;

    G.arena = arena_alloc(GIGABYTE(64));
    G.sys_events_mutex = sys_mutex_alloc();
    G.sys_events_arena = arena_alloc(GIGABYTE(64));
    world_alloc(&G.world);
    G.world_canvas = renderer_canvas_alloc();
    G.world_view = XFORM_TRS(.t = V2(0, 0), .r = 0, .s = V2(PIXELS_PER_UNIT, PIXELS_PER_UNIT));
    G.viewport_bg_canvas = renderer_canvas_alloc();
    G.viewport_canvas = renderer_canvas_alloc();
    G.window = window;
    sys_window_register_event_callback(G.window, &window_event_callback);

    G.user_thread = sys_thread_alloc(&user_thread_entry_point, NULL, STR("[P1] User thread"));
    app_register_exit_callback(&user_shutdown);

    return (struct user_startup_receipt) { 0 };
}

INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(user_shutdown)
{
    __prof;
    atomic_i32_eval_exchange(&G.user_thread_shutdown, true);
    sys_thread_wait_release(&G.user_thread);
}

/* ========================== *
 * Window -> user communication
 * ========================== */

INTERNAL struct sys_event_array pop_sys_events(struct arena *arena)
{
    struct sys_event_array array = { 0 };
    sys_mutex_lock(&G.sys_events_mutex);
    {
        struct buffer events_buff = arena_to_buffer(&G.sys_events_arena);
        arena_align(arena, alignof(struct sys_event));
        array.events = (struct sys_event *)arena_push_array(arena, u8, events_buff.size);
        array.count = events_buff.size / sizeof(struct sys_event);
        MEMCPY(array.events, events_buff.data, events_buff.size);
        arena_reset(&G.sys_events_arena);
    }
    sys_mutex_unlock(&G.sys_events_mutex);
    return array;
}

INTERNAL SYS_WINDOW_EVENT_CALLBACK_FUNC_DEF(window_event_callback, event)
{
    sys_mutex_lock(&G.sys_events_mutex);
    {
        *arena_push(&G.sys_events_arena, struct sys_event) = event;
    }
    sys_mutex_unlock(&G.sys_events_mutex);
}

/* ========================== *
 * Game -> user communication
 * ========================== */

INTERNAL struct blend_tick *blend_tick_alloc(void)
{
    struct blend_tick *bt = NULL;
    if (G.head_free_blend_tick) {
        bt = G.head_free_blend_tick;
        G.head_free_blend_tick = bt->next;
        *bt = (struct blend_tick) {
            .world = bt->world
        };
    } else {
        bt = arena_push_zero(&G.arena, struct blend_tick);
        world_alloc(&bt->world);
    }
    if (G.head_blend_tick) {
        bt->next = G.head_blend_tick;
        G.head_blend_tick->prev = bt;
    }
    G.head_blend_tick = bt;
    return bt;
}

INTERNAL void blend_tick_release(struct blend_tick *bt)
{
    struct blend_tick *next = bt->next;
    struct blend_tick *prev = bt->prev;

    /* Remove from list */
    if (next) {
        next->prev = prev;
    }
    if (prev) {
        prev->next = next;
    }
    if (bt == G.head_blend_tick) {
        G.head_blend_tick = next;
    }

    /* Add to free list */
    bt->next = G.head_free_blend_tick;
    bt->prev = NULL;
    G.head_free_blend_tick = bt;
}

struct interp_ticks {
    struct world *from_tick;
    struct world *to_tick;
};

INTERNAL struct interp_ticks pull_ticks(f64 blend_time)
{
    __prof;

    /* Find newest stored tick */
    struct world *newest_tick = NULL;
    for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) {
        if (!newest_tick || bt->world.tick_id > newest_tick->tick_id) {
            newest_tick = &bt->world;
        }
    }

    /* Pull new tick from game thread if necessary */
    if (!newest_tick || game_get_latest_tick_id() > newest_tick->tick_id) {
        struct blend_tick *latest_bt = blend_tick_alloc();
        newest_tick = &latest_bt->world;
        game_get_latest_tick(newest_tick);
    }

    /* Find oldest tick */
    struct world *oldest_tick = NULL;
    for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) {
        if (!oldest_tick || bt->world.tick_id < oldest_tick->tick_id) {
            oldest_tick = &bt->world;
        }
    }

    /* Find closest ticks to blend time */
    struct world *from_tick = oldest_tick;
    struct world *to_tick = newest_tick;
    for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) {
        f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts);

        if (bt_time < blend_time && bt_time > sys_timestamp_seconds(from_tick->tick_ts)) {
            from_tick = &bt->world;
        }

        if (bt_time > blend_time && bt_time < sys_timestamp_seconds(to_tick->tick_ts)) {
            to_tick = &bt->world;
        }
    }

    /* Free any unused old ticks */
    {
        struct temp_arena scratch = scratch_begin_no_conflict();
        struct blend_tick **bts_to_free = arena_dry_push(scratch.arena, struct blend_tick *);
        u64 bts_to_free_count = 0;

        for (struct blend_tick *bt = G.head_blend_tick; bt; bt = bt->next) {
            f64 bt_time = sys_timestamp_seconds(bt->world.tick_ts);
            if (bt_time < sys_timestamp_seconds(from_tick->tick_ts)) {
                *arena_push(scratch.arena, struct blend_tick *) = bt;
                ++bts_to_free_count;
            }
        }

        for (u64 i = 0; i < bts_to_free_count; ++i) {
            blend_tick_release(bts_to_free[i]);
        }

        scratch_end(scratch);
    }

    return (struct interp_ticks) {
        .from_tick = from_tick,
        .to_tick = to_tick
    };
}

/* ========================== *
 * User -> game communication
 * ========================== */

struct game_cmd_node {
    struct game_cmd cmd;
    struct game_cmd_node *next;
};

struct game_cmd_list {
    struct arena *arena;
    struct game_cmd_node *first;
    struct game_cmd_node *last;
};

INTERNAL void queue_game_cmd(struct game_cmd_list *list, struct game_cmd cmd)
{
    struct game_cmd_node *node = arena_push_zero(list->arena, struct game_cmd_node);
    node->cmd = cmd;
    if (list->first) {
        list->last->next = node;
    } else {
        list->first = node;
    }
    list->last = node;
}

INTERNAL void pubilsh_game_cmds(struct game_cmd_list *list)
{
    struct temp_arena scratch = scratch_begin(list->arena);

    /* Construct array */
    struct game_cmd_array array = { .cmds = arena_dry_push(scratch.arena, struct game_cmd) };
    for (struct game_cmd_node *node = list->first; node; node = node->next) {
        struct game_cmd *cmd = arena_push(scratch.arena, struct game_cmd);
        *cmd = node->cmd;
        ++array.count;
    }

    /* Push array to game thread */
    if (array.count > 0) {
        game_push_cmds(array);
    }

    scratch_end(scratch);
}

/* ========================== *
 * Update
 * ========================== */

/* TODO: remove this (testing) */
INTERNAL void debug_draw_xform(struct xform xf)
{
    f32 thickness = 2.f;
    f32 arrowhead_len = 15.f;
    u32 color   = RGBA_32_F(0, 1, 1, 0.3);
    u32 color_x = RGBA_32_F(1, 0, 0, 0.3);
    u32 color_y = RGBA_32_F(0, 1, 0, 0.3);

    struct v2 pos   = xform_mul_v2(G.world_view, xf.og);
    struct v2 x_ray = xform_basis_mul_v2(G.world_view, xform_get_right(xf));
    struct v2 y_ray = xform_basis_mul_v2(G.world_view, xform_get_up(xf));

    struct quad quad = quad_from_rect(RECT(0, 0, 1, -1));
    quad = quad_mul_xform(quad_scale(quad, 0.075f), xf);

    draw_solid_arrow_ray(G.viewport_canvas, pos, x_ray, thickness, arrowhead_len, color_x);
    draw_solid_arrow_ray(G.viewport_canvas, pos, y_ray, thickness, arrowhead_len, color_y);
    draw_solid_quad(G.viewport_canvas, quad, color);
}

/* TODO: remove this (testing) */
INTERNAL void debug_draw_movement(struct entity *ent)
{
    f32 thickness = 2.f;
    f32 arrow_len = 15.f;

    u32 color_vel = RGBA_32_F(1, 0.5, 0, 1);
    u32 color_acc = RGBA_32_F(1, 1, 0.5, 1);

    struct v2 pos       = xform_mul_v2(G.world_view, ent->world_xform.og);
    struct v2 vel_ray   = xform_basis_mul_v2(G.world_view, ent->velocity);
    struct v2 acc_ray   = xform_basis_mul_v2(G.world_view, ent->acceleration);

    draw_solid_arrow_ray(G.viewport_canvas, pos, vel_ray, thickness, arrow_len, color_vel);
    draw_solid_arrow_ray(G.viewport_canvas, pos, acc_ray, thickness, arrow_len, color_acc);
}

INTERNAL void user_update(void)
{
    struct temp_arena scratch = scratch_begin_no_conflict();
    struct game_cmd_list cmd_list = {
        .arena = scratch.arena
    };

    /* Get time */
    f64 cur_time = sys_timestamp_seconds(sys_timestamp());
    G.dt = max_f64(0.0, cur_time - G.time);
    G.time += G.dt;
    G.screen_size = sys_window_get_size(G.window);

    /* ========================== *
     * Begin frame cache scopes
     * ========================== */

    /* TODO: Remove texture scope once renderer is rewritten to work with tags */
    struct sprite_scope *sprite_frame_scope = sprite_scope_begin();

    /* ========================== *
     * Produce interpolated tick
     * ========================== */

    b32 tick_is_first_frame = false;
    {
        __profscope(produce_interpolated_tick);

#if USER_INTERP_ENABLED
        f64 blend_time_offset = (1.0 / GAME_FPS) * USER_INTERP_OFFSET_TICK_RATIO;
        f64 blend_time = G.time > blend_time_offset ? G.time - blend_time_offset : 0;

        /* Pull ticks */
        struct interp_ticks interp_ticks = pull_ticks(blend_time);
        struct world *t0 = interp_ticks.from_tick;
        struct world *t1 = interp_ticks.to_tick;

        tick_is_first_frame = (t0->tick_id == 0 || t1->tick_id == 0);

        f32 tick_blend = 0;
        {
            f64 t0_time = sys_timestamp_seconds(t0->tick_ts);
            f64 t1_time = sys_timestamp_seconds(t1->tick_ts);
            if (t1_time > t0_time) {
                tick_blend = (f32)((blend_time - t0_time) / (t1_time - t0_time));
            }
            tick_blend = clamp_f32(tick_blend, 0.0f, 1.0f);
        }

        world_copy_replace(&G.world, t1);

        /* Blend time */
        G.world.time = math_lerp64(t0->time, t1->time, (f64)tick_blend);

        /* Blend entities */
        struct entity_array t0_entities =  entity_store_as_array(&t0->entity_store);
        struct entity_array t1_entities = entity_store_as_array(&t1->entity_store);
        struct entity_array world_entities = entity_store_as_array(&G.world.entity_store);

        u64 num_entities = min_u64(t0_entities.count, t1_entities.count);
        for (u64 i = 0; i < num_entities; ++i) {
            struct entity *e0 = &t0_entities.entities[i];
            struct entity *e1 = &t1_entities.entities[i];
            struct entity *e = &world_entities.entities[i];
            if (e0->handle.gen == e1->handle.gen && e0->continuity_gen == e1->continuity_gen) {
                e->rel_xform = xform_lerp(e0->rel_xform, e1->rel_xform, tick_blend);
                e->world_xform = xform_lerp(e0->world_xform, e1->world_xform, tick_blend);

                e->acceleration = v2_lerp(e0->acceleration, e1->acceleration, tick_blend);
                e->velocity = v2_lerp(e0->velocity, e1->velocity, tick_blend);
                e->player_acceleration = math_lerp(e0->player_acceleration, e1->player_acceleration, tick_blend);
                e->player_aim = v2_lerp(e0->player_aim, e1->player_aim, tick_blend);

                e->sprite_quad_xform = xform_lerp(e0->sprite_quad_xform, e1->sprite_quad_xform, tick_blend);
                e->animation_time_in_frame = math_lerp64(e0->animation_time_in_frame, e1->animation_time_in_frame, (f64)tick_blend);

                e->camera_quad_xform = xform_lerp(e0->camera_quad_xform, e1->camera_quad_xform, tick_blend);
                e->camera_rel_xform_target = xform_lerp(e0->camera_rel_xform_target, e1->camera_rel_xform_target, tick_blend);
            }
        }
#else
        struct interp_ticks interp_ticks = pull_ticks(G.time);
        world_copy_replace(&G.world, interp_ticks.to_tick);
        tick_is_first_frame = G.world.tick_id == 0;
#endif
    }
    struct entity_array entities_array = entity_store_as_array(&G.world.entity_store);

    /* ========================== *
     * Find important entities
     * ========================== */

    struct entity *player = entity_nil();
    struct entity *active_camera = entity_nil();

    for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) {
        struct entity *ent = &entities_array.entities[entity_index];
        if (!ent->valid) continue;

        /* Player */
        if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
            player = ent;
        }

        /* Active camera */
        if (entity_has_prop(ent, ENTITY_PROP_CAMERA) && entity_has_prop(ent, ENTITY_PROP_CAMERA_ACTIVE)) {
            active_camera = ent;
        }
    }

    /* ========================== *
     * Read sys events
     * ========================== */

    struct sys_event_array events = pop_sys_events(scratch.arena);

    /* Reset bind states "was_pressed" */
    for (u32 i = 0; i < ARRAY_COUNT(G.bind_states); ++i) {
        G.bind_states[i] = (struct bind_state) {
            .is_held = G.bind_states[i].is_held
        };
    }

    for (u64 entity_index = 0; entity_index < events.count; ++entity_index) {
        struct sys_event *event = &events.events[entity_index];

        if (event->kind == SYS_EVENT_KIND_QUIT) {
            app_exit();
        }

        if (event->kind == SYS_EVENT_KIND_BUTTON_UP) {
#if DEVELOPER
            /* Escape quit */
            if (event->button == SYS_BTN_ESC) {
                app_exit();
            }
#endif
        }

        /* Update mouse pos */
        if (event->kind == SYS_EVENT_KIND_CURSOR_MOVE) {
            G.screen_cursor = event->cursor_position;
        }

        /* Update bind states */
        if ((event->kind == SYS_EVENT_KIND_BUTTON_DOWN || event->kind == SYS_EVENT_KIND_BUTTON_UP) && !event->is_repeat) {
            enum sys_btn button = event->button;
            button = button >= SYS_BTN_COUNT ? SYS_BTN_NONE : button;
            enum user_bind_kind bind = g_binds[button];
            if (bind) {
                b32 pressed = event->kind == SYS_EVENT_KIND_BUTTON_DOWN;
                b32 out_of_bounds = button >= SYS_BTN_M1 && button <= SYS_BTN_M5 &&
                                    (G.viewport_cursor.x < 0 ||
                                     G.viewport_cursor.y < 0 ||
                                     G.viewport_cursor.x > G.viewport_size.x ||
                                     G.viewport_cursor.y > G.viewport_size.y);
                G.bind_states[bind].is_held = pressed && !out_of_bounds;
                if (pressed) {
                    if (!out_of_bounds) {
                        ++G.bind_states[bind].num_presses;
                    }
                } else {
                    ++G.bind_states[bind].num_releases;
                }
            }
        }
    }

    /* ========================== *
     * Debug commands
     * ========================== */

    /* Test fullscreen */
    {
        struct bind_state state = G.bind_states[USER_BIND_KIND_FULLSCREEN];
        if (state.num_presses) {
            struct sys_window_settings settings = sys_window_get_settings(G.window);
            settings.flags ^= SYS_WINDOW_SETTINGS_FLAG_FULLSCREEN;
            sys_window_update_settings(G.window, &settings);
        }
    }

    /* Test clear world */
    {
        struct bind_state state = G.bind_states[USER_BIND_KIND_DEBUG_CLEAR];
        if (state.num_presses || state.is_held) {
            queue_game_cmd(&cmd_list, (struct game_cmd) {
                .kind = GAME_CMD_KIND_CLEAR_ALL
            });
        }
    }

    if (G.bind_states[USER_BIND_KIND_DEBUG_DRAW].num_presses > 0) {
        G.debug_draw = !G.debug_draw;
    }

    if (G.bind_states[USER_BIND_KIND_DEBUG_CAMERA].num_presses > 0) {
        G.debug_camera = !G.debug_camera;
    }


    /* ========================== *
     * Update viewport
     * ========================== */

    /* Calculate screen viewport dimensions */
    if (G.debug_camera) {
        G.viewport_size = G.screen_size;
        G.viewport_screen_offset = V2(0, 0);
    } else {

        /* Determine viewport size by camera & window dimensions */
        f32 aspect_ratio = 1.0;
        {
            struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform);
            struct v2 camera_size = xform_get_scale(quad_xf);
            if (!v2_eq(camera_size, V2(0, 0))) {
                aspect_ratio = camera_size.x / camera_size.y;
            }
        }
        f32 width = G.screen_size.x;
        f32 height = G.screen_size.y;
        if (width / height > aspect_ratio) {
            width = height * aspect_ratio;
        } else {
            height = math_ceil(width / aspect_ratio);
        }
        G.viewport_size = V2(width, height);

        /* Center viewport in window */
        f32 x = 0;
        f32 y = 0;
        x = math_round(G.screen_size.x / 2 - width / 2);
        y = math_round(G.screen_size.y / 2 - height / 2);
        G.viewport_screen_offset = V2(x, y);
    }

    G.viewport_center = v2_mul(G.viewport_size, 0.5);
    G.viewport_cursor = v2_sub(G.screen_cursor, G.viewport_screen_offset);

    /* ========================== *
     * Update view
     * ========================== */

    if (G.debug_camera) {
        G.world_view = xform_with_rotation(G.world_view, 0);

        /* Pan view */
        if (G.bind_states[USER_BIND_KIND_PAN].is_held) {
            if (!G.debug_camera_panning) {
                G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_cursor);
            }
            G.debug_camera_panning = true;
            struct v2 offset = v2_sub(G.debug_camera_pan_start, xform_invert_mul_v2(G.world_view, G.viewport_cursor));
            G.world_view = xform_translate(G.world_view, v2_neg(offset));
            G.debug_camera_pan_start = xform_invert_mul_v2(G.world_view, G.viewport_cursor);
        } else {
            G.debug_camera_panning = false;
        }

        /* Zoom view */
        i32 input_zooms = G.bind_states[USER_BIND_KIND_ZOOM_IN].num_presses - G.bind_states[USER_BIND_KIND_ZOOM_OUT].num_presses;
        if (input_zooms != 0) {
            /* Zoom to cursor */
            f32 zoom_rate = 2;
            f32 zoom = math_pow(zoom_rate, input_zooms);
            struct v2 world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor);
            G.world_view = xform_translate(G.world_view, world_cursor);
            G.world_view = xform_scale(G.world_view, V2(zoom, zoom));
            G.world_view = xform_translate(G.world_view, v2_neg(world_cursor));
        }
    } else {
        struct v2 center = active_camera->world_xform.og;
        f32 rot = xform_get_rotation(active_camera->world_xform);

        /* Scale view into viewport based on camera size */
        struct v2 size = G.viewport_size;
        {
            struct xform quad_xf = xform_mul(active_camera->world_xform, active_camera->camera_quad_xform);
            struct v2 camera_size = xform_get_scale(quad_xf);
            if (!v2_eq(camera_size, V2(0, 0))) {
                size = v2_div_v2(size, camera_size);
            }
        }
        f32 scale = min_f32(size.x, size.y);

        struct trs trs = TRS(
            .t = v2_sub(G.viewport_center, center),
            .r = rot,
            .s = V2(scale, scale)
        );

        struct v2 pivot = center;
        G.world_view = XFORM_IDENT;
        G.world_view = xform_translate(G.world_view, pivot);
        G.world_view = xform_trs_pivot_rs(G.world_view, trs, pivot);
    }
    G.world_cursor = xform_invert_mul_v2(G.world_view, G.viewport_cursor);

    /* ========================== *
     * Update listener
     * ========================== */

    {
        struct v2 up = V2(0, -1);
        struct v2 listener_pos = xform_invert_mul_v2(G.world_view, G.viewport_center);
        struct v2 listener_dir = v2_norm(xform_basis_invert_mul_v2(G.world_view, up));
        mixer_set_listener(listener_pos, listener_dir);
    }

    /* ========================== *
     * Draw test BG
     * ========================== */

    {
        u32 color = RGBA_32_F(0.2f, 0.2f, 0.2f, 1.f);
        draw_solid_rect(G.viewport_bg_canvas, RECT(0, 0, G.viewport_size.x, G.viewport_size.y), color);
    }

    /* ========================== *
     * Draw test grid
     * ========================== */

    {
        f32 thickness = 3.f;
        u32 color = RGBA_32(0x3f, 0x3f, 0x3f, 0xFF);
        u32 x_color = RGBA_32(0x3f, 0, 0, 0xFF);
        u32 y_color = RGBA_32(0, 0x3f, 0, 0xFF);

        i64 startx = -10;
        i64 starty = -10;
        i64 rows = 20;
        i64 cols = 20;

        /* Draw column lines */
        struct v2 col_ray = xform_basis_mul_v2(G.world_view, V2(0, rows));
        for (i64 col = starty; col <= (starty + cols); ++col) {
            u32 line_color = color;
            if (col == 0) {
                line_color = y_color;
            }

            struct v2 pos = xform_mul_v2(G.world_view, V2(col, starty));
            draw_solid_ray(G.viewport_bg_canvas, pos, col_ray, thickness, line_color);
        }

        struct v2 row_ray = xform_basis_mul_v2(G.world_view, V2(cols, 0));
        for (i64 row = startx; row <= (startx + rows); ++row) {
            u32 line_color = color;
            if (row == 0) {
                line_color = x_color;
            }
            struct v2 pos = xform_mul_v2(G.world_view, V2(startx, row));
            draw_solid_ray(G.viewport_bg_canvas, pos, row_ray, thickness, line_color);
        }
    }

    /* ---------------------------------------------------------------------- */
    /* ---------------------------------------------------------------------- */

    /* ========================== *
     * Iterate entities
     * ========================== */

    /* Iterate entities */
    for (u64 entity_index = 0; entity_index < entities_array.count; ++entity_index) {
        __profscope(user_entity_iter);

        struct entity *ent = &entities_array.entities[entity_index];
        if (!ent->valid) continue;

        b32 skip_debug_draw = !G.debug_camera && ent == active_camera;
        b32 skip_debug_draw_transform = ent == active_camera;

        /* Draw sprite */
        if (!sprite_tag_is_nil(ent->sprite)) {
            /* Draw texture */
            struct sprite_tag sprite = ent->sprite;
            /* TODO: Remove this once renderer is rewritten to work with tags */
            struct sprite_texture *texture = sprite_texture_from_tag_async(sprite_frame_scope, sprite);

            struct xform xf = xform_mul(ent->world_xform, ent->sprite_quad_xform);
            struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf);

            u32 tint = ent->sprite_tint;

            struct sprite_sheet *sheet = sprite_sheet_from_tag_async(sprite_frame_scope, sprite);
            struct sprite_sheet_frame frame = { 0 };
            {
                struct sprite_sheet_span span = sprite_sheet_get_span(sheet, ent->sprite_span_name);
                u64 frame_index = span.start + ent->animation_frame;
                frame = sprite_sheet_get_frame(sheet, frame_index);
                if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) {
                    f64 time_in_frame = ent->animation_time_in_frame;
                    while (time_in_frame > frame.duration) {
                        time_in_frame -= frame.duration;
                        ++frame_index;
                        if (frame_index > span.end) {
                            /* Loop animation */
                            frame_index = span.start;
                        }
                        frame = sprite_sheet_get_frame(sheet, frame_index);
                    }
                }
            }

            struct draw_sprite_params params = DRAW_SPRITE_PARAMS(.sprite = sprite, .tint = tint, .clip = frame.clip);

            /* TODO: Check for texture loading as well */
            if (sheet->loaded && texture->loaded) {
                /* TODO: Fade in a placeholder or something instead of drawing nothing. */
                draw_sprite_quad(G.world_canvas, params, quad);
            }

#if 0
            if (G.debug_draw && !skip_debug_draw) {
                /* Debug draw sprite quad */
                {
                    f32 thickness = 2.f;
                    u32 color = RGBA_32_F(1, 1, 0, 0.25);
                    draw_solid_quad_line(G.world_canvas, quad, (thickness / PIXELS_PER_UNIT / G.world_view.zoom), color);
                }

                /* Debug draw sprite transform */
                {
                    debug_draw_xform(xf);
                }

                /* Debug draw sprite pivot */
                {
                    u32 color = RGBA_32_F(1, 0, 0, 1);
                    draw_solid_circle(G.world_canvas, ent->world_xform.og, 0.02, color, 20);
                }
            }
#endif
        }

        /* Debug draw info */
        if (G.debug_draw && !skip_debug_draw) {
            struct temp_arena temp = arena_temp_begin(scratch.arena);

#if 0
            struct font *disp_font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f);
            if (disp_font) {
                struct xform xf = ent->world_xform;
                struct trs trs = trs_from_xform(xf);
                struct v2 velocity = ent->velocity;
                struct v2 acceleration = ent->acceleration;

                f32 offset = 1;
                struct v2 pos = v2_add(xf.og, v2_mul(V2(0, -1), offset));
                pos = xform_mul_v2(G.world_view, pos);
                pos = v2_round(pos);

                struct string disp_name = ent->sprite_name;

                struct string fmt = STR(
                    "sprite name: \"%F\"\n"
                    "pos: (%F, %F)\n"
                    "scale: (%F, %F)\n"
                    "rot: %F\n"
                    "velocity: (%F, %F)\n"
                    "acceleration: (%F, %F)\n"
                );
                struct string text = string_format(temp.arena, fmt,
                                                   FMT_STR(disp_name),
                                                   FMT_FLOAT((f64)trs.t.x), FMT_FLOAT((f64)trs.t.y),
                                                   FMT_FLOAT((f64)trs.s.x), FMT_FLOAT((f64)trs.s.y),
                                                   FMT_FLOAT((f64)trs.r),
                                                   FMT_FLOAT((f64)velocity.x), FMT_FLOAT((f64)velocity.y),
                                                   FMT_FLOAT((f64)acceleration.x), FMT_FLOAT((f64)acceleration.y)
                                                   );


                draw_text(G.viewport_canvas, disp_font, pos, text);
            }
#endif

            debug_draw_movement(ent);

            if (!skip_debug_draw_transform) {
                debug_draw_xform(ent->world_xform);
            }

            /* Draw hierarchy */
            struct entity *parent = entity_from_handle(&G.world.entity_store, ent->parent);
            if (parent->valid) {
                u32 color = RGBA_32_F(0.6, 0.6, 1, 0.75);
                f32 thickness = 5;
                f32 arrow_height = 15;

                struct v2 start = xform_mul_v2(G.world_view, ent->world_xform.og);
                struct v2 end = xform_mul_v2(G.world_view, parent->world_xform.og);
                draw_solid_arrow_line(G.viewport_canvas, start, end, thickness, arrow_height, color);
            }

            /* Draw aim */
            if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
                u32 color = RGBA_32_F(0.75, 0, 0.75, 0.5);
                f32 thickness = 3;
                f32 arrow_height = 10;
                struct v2 pos = xform_mul_v2(G.world_view, ent->world_xform.og);
                struct v2 aim_ray = xform_basis_mul_v2(G.world_view, ent->player_aim);
                draw_solid_arrow_ray(G.viewport_canvas, pos, aim_ray, thickness, arrow_height, color);
            }

            /* Draw camera rect */
            if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) {
                u32 color = ent == active_camera ? RGBA_32_F(1, 1, 1, 0.5) : RGBA_32_F(0, 0.75, 0, 0.5);
                f32 thickness = 3;


                struct xform quad_xf = xform_mul(ent->world_xform, ent->camera_quad_xform);
                struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, quad_xf);
                quad = quad_mul_xform(quad, G.world_view);

                draw_solid_quad_line(G.viewport_canvas, quad, thickness, color);
            }

            arena_temp_end(temp);
        }
    }

    /* Draw crosshair or show cursor */
    if (!G.debug_camera) {
        struct v2 crosshair_pos = G.viewport_cursor;
        u32 tint = RGBA_32_F(1, 1, 1, 1);

        struct sprite_tag crosshair_tag = sprite_tag_from_path(STR("res/graphics/crosshair.ase"));
        struct sprite_texture *t = sprite_texture_from_tag_async(sprite_frame_scope, crosshair_tag);

        struct v2 size = V2(t->width, t->height);
        struct xform xf = XFORM_TRS(.t = crosshair_pos, .s = size);
        struct quad quad = quad_mul_xform(QUAD_UNIT_SQUARE_CENTERED, xf);
        draw_sprite_quad(G.viewport_canvas, DRAW_SPRITE_PARAMS(.sprite = crosshair_tag, .tint = tint), quad);

        struct rect cursor_clip = RECT_FROM_V2(G.viewport_screen_offset, G.viewport_size);
        cursor_clip.pos = v2_add(cursor_clip.pos, v2_mul(size, 0.5f));
        cursor_clip.pos = v2_add(cursor_clip.pos, V2(1, 1));
        cursor_clip.size = v2_sub(cursor_clip.size, size);
        sys_window_cursor_hide(G.window);
        sys_window_cursor_enable_clip(G.window, cursor_clip);
    } else {
        sys_window_cursor_disable_clip(G.window);
        sys_window_cursor_show(G.window);
    }

    /* ========================== *
     * Construct movement input
     * ========================== */

    /* Movement */
    struct v2 input_move_dir = { 0 };
    {
        for (enum user_bind_kind bind = 0; bind < (i32)ARRAY_COUNT(G.bind_states); ++bind) {
            struct bind_state state = G.bind_states[bind];

            if (!state.is_held && state.num_presses <= 0) {
                continue;
            }

            switch (bind) {
                /* Movement */
                case USER_BIND_KIND_MOVE_UP: {
                    input_move_dir.y -= 1;
                } break;

                case USER_BIND_KIND_MOVE_DOWN: {
                    input_move_dir.y += 1;
                } break;

                case USER_BIND_KIND_MOVE_LEFT: {
                    input_move_dir.x -= 1;
                } break;

                case USER_BIND_KIND_MOVE_RIGHT: {
                    input_move_dir.x += 1;
                } break;

                default: break;
            }
        }

        input_move_dir = xform_basis_invert_mul_v2(G.world_view, input_move_dir);  /* Make move dir relative to world view */
        input_move_dir = v2_norm(input_move_dir);
    }

    /* Aim */
    struct v2 input_aim = player->player_aim;
    if (!G.debug_camera) {
        input_aim = v2_sub(G.world_cursor, player->world_xform.og);
    }

    /* Queue cmd */
    queue_game_cmd(&cmd_list, (struct game_cmd) {
        .kind = GAME_CMD_KIND_PLAYER_MOVE,
        .move_dir = input_move_dir,
        .aim = input_aim
    });

    /* ---------------------------------------------------------------------- */
    /* ---------------------------------------------------------------------- */

    /* Debug draw info */
    if (G.debug_draw) {

        f32 spacing = 20;
        struct v2 pos = V2(10, 8);
        struct font *font = font_load_async(STR("res/fonts/fixedsys.ttf"), 12.0f);
        if (font) {
            struct temp_arena temp = arena_temp_begin(scratch.arena);

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("time: %F"), FMT_FLOAT((f64)G.time)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("screen_size: (%F, %F)"), FMT_FLOAT((f64)G.screen_size.x), FMT_FLOAT((f64)G.screen_size.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("screen_cursor: (%F, %F)"), FMT_FLOAT((f64)G.screen_cursor.x), FMT_FLOAT((f64)G.screen_cursor.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_screen_offset: (%F, %F)"), FMT_FLOAT((f64)G.viewport_screen_offset.x), FMT_FLOAT((f64)G.viewport_screen_offset.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_size: (%F, %F)"), FMT_FLOAT((f64)G.viewport_size.x), FMT_FLOAT((f64)G.viewport_size.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_center: (%F, %F)"), FMT_FLOAT((f64)G.viewport_center.x), FMT_FLOAT((f64)G.viewport_center.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("viewport_cursor: (%F, %F)"), FMT_FLOAT((f64)G.viewport_cursor.x), FMT_FLOAT((f64)G.viewport_cursor.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view.og: (%F, %F)"), FMT_FLOAT((f64)G.world_view.og.x), FMT_FLOAT((f64)G.world_view.og.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view rotation: %F"), FMT_FLOAT((f64)xform_get_rotation(G.world_view))));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_view scale: (%F, %F)"), FMT_FLOAT((f64)xform_get_scale(G.world_view).x), FMT_FLOAT((f64)xform_get_scale(G.world_view).x)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("world_cursor: (%F, %F)"), FMT_FLOAT((f64)G.world_cursor.x), FMT_FLOAT((f64)G.world_cursor.y)));
            pos.y += spacing;

            draw_text(G.viewport_canvas, font, pos, string_format(temp.arena, STR("debug_camera: %F"), FMT_STR(G.debug_camera ? STR("true") : STR("false"))));
            pos.y += spacing;

            arena_temp_end(temp);
        }

    }

    /* Push game cmds */
    pubilsh_game_cmds(&cmd_list);

    /* ========================== *
     * Present
     * ========================== */

    /* Send canvases to GPU */
    renderer_canvas_send_to_gpu(G.viewport_bg_canvas);
    renderer_canvas_send_to_gpu(G.world_canvas);
    renderer_canvas_send_to_gpu(G.viewport_canvas);

    /* Set canvas views before presenting */
    renderer_canvas_set_view(G.viewport_bg_canvas, XFORM_IDENT);
    renderer_canvas_set_view(G.world_canvas, G.world_view);
    renderer_canvas_set_view(G.viewport_canvas, XFORM_IDENT);

    /* Present */
    i32 vsync = VSYNC_ENABLED;

    struct renderer_canvas **canvases = arena_dry_push(scratch.arena, struct renderer_canvas *);
    u64 canvases_count = 0;
    {
        /* Viewport background canvas */
        *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_bg_canvas;
        ++canvases_count;

        /* World canvas */
        if (!tick_is_first_frame) {
            /* Only render world if not on first frame */
            *arena_push(scratch.arena, struct renderer_canvas *) = G.world_canvas;
            ++canvases_count;
        }

        /* Viewport canvas */
        *arena_push(scratch.arena, struct renderer_canvas *) = G.viewport_canvas;
        ++canvases_count;
    }

    renderer_canvas_present(canvases, canvases_count, G.screen_size, RECT_FROM_V2(G.viewport_screen_offset, G.viewport_size), vsync);

    /* ========================== *
     * End frame cache scopes
     * ========================== */

    sprite_scope_end(sprite_frame_scope);

    scratch_end(scratch);
}

/* ========================== *
 * User thread entry point
 * ========================== */

INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(user_thread_entry_point, arg)
{
    (UNUSED)arg;

    sys_timestamp_t last_frame_ts = 0;
    f64 target_dt = USER_FRAME_LIMIT > (0) ? (1.0 / USER_FRAME_LIMIT) : 0;

    while (!atomic_i32_eval(&G.user_thread_shutdown)) {
        __profscope(user_update_w_sleep);
        sleep_frame(last_frame_ts, target_dt);
        last_frame_ts = sys_timestamp();
        user_update();
    }
}
