#include "app.h"
#include "arena.h"
#include "string.h"
#include "scratch.h"
#include "sys.h"
#include "work.h"
#include "user.h"
#include "game.h"
#include "playback.h"
#include "log.h"
#include "resource.h"
#include "asset_cache.h"
#include "font.h"
#include "sprite.h"
#include "ttf.h"
#include "mixer.h"
#include "sound.h"
#include "util.h"
#include "settings.h"
#include "draw.h"
#include "math.h"
#include "renderer.h"

struct exit_callback {
    app_exit_callback_func *func;
    struct exit_callback *next;
    struct sys_thread thread;
};

GLOBAL struct {
    struct arena arena;
    struct string write_path;
    struct sync_flag exit_sf;

    /* Exit callbacks */
    struct sys_mutex exit_callbacks_mutex;
    struct arena exit_callbacks_arena;
    struct exit_callback *exit_callbacks_head;
} G = { 0 }, DEBUG_ALIAS(G, G_app);

/* ========================== *
 * Write directory
 * ========================== */

INTERNAL struct string initialize_write_directory(struct arena *arena, struct string write_dir)
{
    struct temp_arena scratch = scratch_begin(arena);

    /* Create write path */
    struct string base_write_dir = sys_get_write_path(scratch.arena);
    struct string write_path_fmt = base_write_dir.len > 0 ? STR("%F/%F/") : STR("%F%F/");
    struct string write_path = string_format(
        arena,
        write_path_fmt,
        FMT_STR(base_write_dir),
        FMT_STR(write_dir)
    );

    /* Create write dir if not present */
    if (!sys_is_dir(write_path)) {
        sys_mkdir(write_path);
        /* TODO: handle failure */
    }

    scratch_end(scratch);

    return write_path;
}

struct string app_write_path_cat(struct arena *arena, struct string filename)
{
    return string_cat(arena, G.write_path, filename);
}

/* ========================== *
 * Default settings
 * ========================== */

INTERNAL struct sys_window_settings default_window_settings(struct sys_window *window)
{
    __prof;

    struct v2 monitor_size = sys_window_get_monitor_size(window);

    i32 width = 1280;
    i32 height = math_round_to_int(width / (f32)(DEFAULT_CAMERA_WIDTH / DEFAULT_CAMERA_HEIGHT));
    i32 x = math_round_to_int(monitor_size.x / 2.f - width / 2);
    i32 y = math_round_to_int(monitor_size.y / 2.f - height / 2);

    return (struct sys_window_settings) {
        .title = WINDOW_TITLE,
        .floating_x = x,
        .floating_y = y,
        .floating_width = width,
        .floating_height = height
    };
}

/* ========================== *
 * Exit callbacks
 * ========================== */

INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(exit_callback_thread_entry_point, vcallback)
{
    struct exit_callback *callback = (struct exit_callback *)vcallback;
    callback->func();
}

void app_register_exit_callback(app_exit_callback_func *func)
{
    sys_mutex_lock(&G.exit_callbacks_mutex);
    {
        struct exit_callback *callback = arena_push_zero(&G.exit_callbacks_arena, struct exit_callback);
        callback->func = func;
        callback->next = G.exit_callbacks_head;
        G.exit_callbacks_head = callback;
    }
    sys_mutex_unlock(&G.exit_callbacks_mutex);
}

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

void app_entry_point(void)
{
    G.exit_sf = sync_flag_alloc();
    G.exit_callbacks_mutex = sys_mutex_alloc();
    G.exit_callbacks_arena = arena_alloc(GIGABYTE(64));

    u32 worker_count = 4;
    {
        /* FIXME: Switch this on to utilize all cores. Only decreasing worker count for testing purposes. */
#if !PROFILING && !RTC
        /* 1. User thread, Input thread
         * 2. Game thread
         * 3. Playback thread
         */
        u32 num_reserved_cores = 3;

        i32 min_worker_count = 2;
        i32 max_worker_count = 512;
        i32 target_worker_count = (i32)sys_num_logical_processors() - num_reserved_cores;
        worker_count = (u32)clamp_i32(target_worker_count, min_worker_count, max_worker_count);
#endif
    }

    G.arena = arena_alloc(GIGABYTE(64));
    G.write_path = initialize_write_directory(&G.arena, STR(WRITE_DIR));

    /* Startup logging */
    {
        struct temp_arena scratch = scratch_begin_no_conflict();
        struct string logfile_path = app_write_path_cat(scratch.arena, STR("log.txt"));
        struct log_startup_receipt log_sr = log_startup(logfile_path);
        (UNUSED)log_sr;
        scratch_end(scratch);
        logf_info("Start of logs");
    }

    /* Create window */
    struct sys_window window = sys_window_alloc();

    /* Read window settings from file */
    {
        struct temp_arena scratch = scratch_begin_no_conflict();

        struct sys_window_settings window_settings = { 0 };
        struct string settings_path = app_write_path_cat(scratch.arena, STR(SETTINGS_FILENAME));
        logf_info("Looking for settings file \"%F\"", FMT_STR(settings_path));
        if (sys_is_file(settings_path)) {
            logf_info("Settings file found");
            struct sys_file settings_file = sys_file_open_read(settings_path);
            struct buffer file_data = sys_file_read_all(scratch.arena, settings_file);
            sys_file_close(settings_file);
            logf_info("Deserializing settings file data: %F", FMT_STR(STRING_FROM_BUFFER(file_data)));
            struct string error = { 0 };
            struct sys_window_settings *res = settings_deserialize(scratch.arena, file_data, &error);
            if (error.len > 0) {
                logf_info("Failed to load settings file with error - %F", FMT_STR(error));
                struct string msg = string_format(scratch.arena,
                                                  STR(
                                                      "Failed to loading settings file \"%F\":\n"
                                                      "------------\n"
                                                      "%F\n"
                                                      "------------\n"
                                                      "To stop this error from appearing, either Fix the issue above or delete the file from the system."
                                                  ),
                                                  FMT_STR(settings_path),
                                                  FMT_STR(error));
                sys_panic(msg);
            }
            logf_info("Settings file loaded successfully");
            window_settings = *res;
        } else {
            logf_info("Settings file not found, loading default");
            window_settings = default_window_settings(&window);
        }
        string_copy_buff(BUFFER_FROM_ARRAY(window_settings.title), STR(WINDOW_TITLE));
        sys_window_update_settings(&window, &window_settings);

        scratch_end(scratch);
    }

    /* Startup systems */
    struct renderer_startup_receipt renderer_sr = renderer_startup(&window);
    struct work_startup_receipt work_sr = work_startup(worker_count);
    struct resource_startup_receipt resource_sr = resource_startup();
    struct asset_cache_startup_receipt asset_cache_sr = asset_cache_startup(&work_sr);
    struct ttf_startup_receipt ttf_sr = ttf_startup();
    struct font_startup_receipt font_sr = font_startup(&work_sr, &renderer_sr, &asset_cache_sr, &ttf_sr, &resource_sr);
    struct sprite_startup_receipt sprite_sr = sprite_startup(&renderer_sr, &resource_sr);
    struct mixer_startup_receipt mixer_sr = mixer_startup();
    struct sound_startup_receipt sound_sr = sound_startup(&work_sr, &asset_cache_sr, &resource_sr);
    struct draw_startup_receipt draw_sr = draw_startup(&renderer_sr, &font_sr);
    struct game_startup_receipt game_sr = game_startup(&mixer_sr, &sprite_sr, &sound_sr);
    struct user_startup_receipt user_sr = user_startup(&work_sr, &renderer_sr, &font_sr, &sprite_sr, &draw_sr, &game_sr, &asset_cache_sr, &mixer_sr, &window);
    struct playback_startup_receipt playback_sr = playback_startup(&mixer_sr);

    (UNUSED)user_sr;
    (UNUSED)playback_sr;

    /* Show window */
    sys_window_show(&window);

    /* Wait for app_exit() */
    sync_flag_wait(&G.exit_sf);

    /* Call exit callbacks */
    /* FIXME: Only wait on threads for a certain period of time before
     * forcing process exit (to prevent process hanging in the background
     * when a thread gets stuck) */
    sys_mutex_lock(&G.exit_callbacks_mutex);
    {
        /* Start callback threads */
        for (struct exit_callback *callback = G.exit_callbacks_head; callback; callback = callback->next) {
            callback->thread = sys_thread_alloc(&exit_callback_thread_entry_point, callback, STR("[P4] Exit callback thread"));
        }
        /* Wait on callback threads */
        for (struct exit_callback *callback = G.exit_callbacks_head; callback; callback = callback->next) {
            sys_thread_wait_release(&callback->thread);
        }
    }
    sys_mutex_unlock(&G.exit_callbacks_mutex);

    /* Write window settings to file */
    {
        struct temp_arena scratch = scratch_begin_no_conflict();

        struct string window_settings_path = app_write_path_cat(scratch.arena, STR(SETTINGS_FILENAME));

        struct sys_window_settings settings = sys_window_get_settings(&window);
        struct buffer buff = settings_serialize(scratch.arena, &settings);
        logf_info("Serialized window settings: %F", FMT_STR(STRING_FROM_BUFFER(buff)));

        logf_info("Writing settings file to path \"%F\"", FMT_STR(window_settings_path));
        struct sys_file settings_file = sys_file_open_write(window_settings_path);
        sys_file_write(settings_file, buff);
        sys_file_close(settings_file);
        logf_info("Finished writing settings file");

        scratch_end(scratch);
    }


    logf_info("Program exited normally");
}

void app_exit(void)
{
    sync_flag_set(&G.exit_sf);
}
