#include "twn_loop.h"
#include "twn_engine_context_c.h"
#include "twn_input_c.h"
#include "twn_util.h"
#include "twn_game_object_c.h"
#include "twn_audio_c.h"
#include "twn_textures_c.h"
#include "twn_rendering.h"

#include <SDL2/SDL.h>
#include <physfs.h>
#include <stb_ds.h>

#ifdef EMSCRIPTEN
#include <GLES2/gl2.h>
#else
#include <glad/glad.h>
#endif

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <tgmath.h>
#include <limits.h>


static void poll_events(void) {
    SDL_Event e;

    ctx.game.window_size_has_changed = false;

    while (SDL_PollEvent(&e)) {
        switch (e.type) {
        case SDL_QUIT:
            ctx.game.is_running = false;
            break;

        case SDL_WINDOWEVENT:
            if (e.window.windowID != ctx.window_id)
                break;

            switch (e.window.event) {
            case SDL_WINDOWEVENT_SIZE_CHANGED:
                ctx.game.window_size_has_changed = true;
                break;

            default:
                break;
            }

            break;

        default:
            break;
        }
    }
}


#ifndef EMSCRIPTEN

static void APIENTRY opengl_log(GLenum source,
                                GLenum type,
                                GLuint id,
                                GLenum severity,
                                GLsizei length,
                                const GLchar* message,
                                const void* userParam)
{
    (void)source;
    (void)type;
    (void)id;
    (void)severity;
    (void)userParam;

    log_info("OpenGL: %.*s\n", length, message);
}

#endif


static void main_loop(void) {
    /*
    if (!ctx.is_running) {
        end(ctx);
        clean_up(ctx);
        emscripten_cancel_main_loop();
    }
    */

    /* frame timer */
    int64_t current_frame_time = SDL_GetPerformanceCounter();
    int64_t delta_time = current_frame_time - ctx.prev_frame_time;
    ctx.prev_frame_time = current_frame_time;
    ctx.game.delta_time = delta_time;

    /* handle unexpected timer anomalies (overflow, extra slow frames, etc) */
    if (delta_time > ctx.desired_frametime * 8) { /* ignore extra-slow frames */
        delta_time = ctx.desired_frametime;
    }
    delta_time = MAX(0, delta_time);

    /* vsync time snapping */
    /* get the refresh rate of the current display (it might not always be the same one) */
    int display_framerate = 60; /* a reasonable guess */
    SDL_DisplayMode current_display_mode;
    int current_display_index = SDL_GetWindowDisplayIndex(ctx.window);
    if (SDL_GetCurrentDisplayMode(current_display_index, &current_display_mode) == 0)
        display_framerate = current_display_mode.refresh_rate;

    int64_t snap_hz = display_framerate;
    if (snap_hz <= 0)
        snap_hz = 60;

    /* these are to snap delta time to vsync values if it's close enough */
    int64_t vsync_maxerror = (int64_t)((double)ctx.clocks_per_second * 0.0002);
    size_t snap_frequency_count = 8;
    for (size_t i = 0; i < snap_frequency_count; ++i) {
        int64_t frequency = (ctx.clocks_per_second / snap_hz) * (i+1);

        if (llabs(delta_time - frequency) < vsync_maxerror) {
            delta_time = frequency;
            break;
        }
    }

    /* delta time averaging */
    size_t time_averager_count = SDL_arraysize(ctx.time_averager);

    for (size_t i = 0; i < time_averager_count - 1; ++i) {
        ctx.time_averager[i] = ctx.time_averager[i+1];
    }
    ctx.time_averager[time_averager_count - 1] = delta_time;
    int64_t averager_sum = 0;
    for (size_t i = 0; i < time_averager_count; ++i) {
        averager_sum += ctx.time_averager[i];
    }
    delta_time = averager_sum / time_averager_count;

    ctx.delta_averager_residual += averager_sum % time_averager_count;
    delta_time += ctx.delta_averager_residual / time_averager_count;
    ctx.delta_averager_residual %= time_averager_count;

    /* add to the accumulator */
    ctx.frame_accumulator += delta_time;

    /* spiral of death protection */
    if (ctx.frame_accumulator > ctx.desired_frametime * 8) {
        ctx.resync_flag = true;
    }

    /* timer resync if requested */
    if (ctx.resync_flag) {
        ctx.frame_accumulator = 0;
        delta_time = ctx.desired_frametime;
        ctx.resync_flag = false;
    }

    /* finally, let's get to work */
    int frames = 0;
    while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.game.update_multiplicity) {
        frames += 1;
        for (size_t i = 0; i < ctx.game.update_multiplicity; ++i) {
            /* TODO: disable rendering pushes on not-last ? */
            render_queue_clear();

            poll_events();

            if (ctx.game.window_size_has_changed) {
                Vec2i size;
                SDL_GetWindowSize(ctx.window, &size.x, &size.y);
                ctx.game.window_w = size.x;
                ctx.game.window_h = size.y;
            }

            input_state_update(&ctx.game.input);

            game_object_tick();

            ctx.frame_accumulator -= ctx.desired_frametime;
            ctx.game.tick_count = (ctx.game.tick_count % ULLONG_MAX) + 1;
            ctx.game.initialization_needed = false;

        }
    }

    /* TODO: in some cases machine might want to assume frames will be fed as much as possible */
    /*       which for now is broken as glBufferData with NULL is used all over right after render */
    if (frames != 0)
        render();
}


static bool initialize(void) {
    if (SDL_Init(SDL_INIT_EVERYTHING & ~SDL_INIT_HAPTIC) == -1) {
        CRY_SDL("SDL initialization failed.");
        return false;
    }

    /* TODO: recognize cli parameter to turn it on on release */
    /* debug mode _defaults_ to being enabled on debug builds. */
    /* you should be able to enable it at runtime on any build */
#ifndef NDEBUG
    ctx.game.debug = true;
#else
    ctx.game.debug = false;
#endif

#ifdef EMSCRIPTEN
    /* emscripten interpretes those as GL ES version against WebGL */
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);

    if (ctx.game.debug)
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
    else
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_NO_ERROR);
#endif

    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);

    /* init got far enough to create a window */
    ctx.window = SDL_CreateWindow("townengine",
                                   SDL_WINDOWPOS_CENTERED,
                                   SDL_WINDOWPOS_CENTERED,
                                   RENDER_BASE_WIDTH,
                                   RENDER_BASE_HEIGHT,
                                   // SDL_WINDOW_ALLOW_HIGHDPI |
                                        SDL_WINDOW_RESIZABLE |
                                        SDL_WINDOW_OPENGL);
    if (ctx.window == NULL) {
        CRY_SDL("Window creation failed.");
        goto fail;
    }

    ctx.gl_context = SDL_GL_CreateContext(ctx.window);
    if (!ctx.gl_context) {
        CRY_SDL("GL context creation failed.");
        goto fail;
    }

    if (SDL_GL_MakeCurrent(ctx.window, ctx.gl_context)) {
        CRY_SDL("GL context binding failed.");
        goto fail;
    }

    if (SDL_GL_SetSwapInterval(-1))
        SDL_GL_SetSwapInterval(1);

#ifndef EMSCRIPTEN
    if (gladLoadGL() == 0) {
        CRY("Init", "GLAD failed");
        goto fail;
    }
#endif

    log_info("OpenGL context: %s\n", glGetString(GL_VERSION));

#ifndef EMSCRIPTEN
    glHint(GL_TEXTURE_COMPRESSION_HINT, GL_NICEST);
    glHint(GL_GENERATE_MIPMAP_HINT, GL_NICEST);
    glHint(GL_FOG_HINT, GL_FASTEST);
#endif

    /* might need this to have multiple windows */
    ctx.window_id = SDL_GetWindowID(ctx.window);

    glViewport(0, 0, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);

    /* TODO: */
    // SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
    ctx.game.window_w = RENDER_BASE_WIDTH;
    ctx.game.window_h = RENDER_BASE_HEIGHT;

    /* audio initialization */
    {

    SDL_AudioSpec request, got;
    SDL_zero(request);

    request.freq = AUDIO_FREQUENCY;
    request.format = AUDIO_S16;
    request.channels = AUDIO_N_CHANNELS;
    request.callback = audio_callback;
    /* TODO: check for errors */
    ctx.audio_device = SDL_OpenAudioDevice(NULL, 0, &request, &got, 0);
    ctx.audio_stream_format = got.format;
    ctx.audio_stream_frequency = got.freq;
    ctx.audio_stream_channel_count = got.channels;

    SDL_PauseAudioDevice(ctx.audio_device, 0);

    }

    /* filesystem time */
    /* TODO: ANDROID: see the warning in physicsfs PHYSFS_init docs/header */
    if (!PHYSFS_init(ctx.argv[0]) ||
        !PHYSFS_setSaneConfig(ORGANIZATION_NAME, APP_NAME, PACKAGE_EXTENSION, false, true))
        {
            CRY_PHYSFS("Filesystem initialization failed.");
            goto fail;
        }

    /* you could change this at runtime if you wanted */
    ctx.game.update_multiplicity = 1;

#ifndef EMSCRIPTEN
    /* hook up opengl debugging callback */
    if (ctx.game.debug) {
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(opengl_log, NULL);
    }
#endif

    /* random seeding */
    /* SDL_GetPerformanceCounter returns some platform-dependent number. */
    /* it should vary between game instances. i checked! random enough for me. */
    ctx.game.random_seed = SDL_GetPerformanceCounter();
    srand((unsigned int)ctx.game.random_seed);
    stbds_rand_seed(ctx.game.random_seed);

    /* main loop machinery */
    ctx.game.is_running = true;
    ctx.resync_flag = true;
    ctx.clocks_per_second = SDL_GetPerformanceFrequency();
    ctx.prev_frame_time = SDL_GetPerformanceCounter();
    ctx.desired_frametime = ctx.clocks_per_second / TICKS_PER_SECOND;
    ctx.frame_accumulator = 0;
    ctx.game.tick_count = 0;

    /* delta time averaging */
    ctx.delta_averager_residual = 0;
    for (size_t i = 0; i < SDL_arraysize(ctx.time_averager); ++i) {
        ctx.time_averager[i] = ctx.desired_frametime;
    }

    /* rendering */
    /* these are dynamic arrays and will be allocated lazily by stb_ds */
    ctx.render_queue_2d = NULL;
    ctx.uncolored_mesh_batches = NULL;

    textures_cache_init(&ctx.texture_cache, ctx.window);
    text_cache_init(&ctx.text_cache);

    /* input */
    input_state_init(&ctx.game.input);

    /* scripting */
    /*
    if (!scripting_init(ctx)) {
        goto fail;
    }
    */

    return true;

fail:
    SDL_Quit();
    return false;
}


/* will not be called on an abnormal exit */
static void clean_up(void) {
    /*
    scripting_deinit(ctx);
    */

    input_state_deinit(&ctx.game.input);

    /* if you're gonna remove this, it's also being done in rendering.c */
    for (size_t i = 0; i < arrlenu(ctx.render_queue_2d); ++i) {
        if (ctx.render_queue_2d[i].type == PRIMITIVE_2D_TEXT) {
            free(ctx.render_queue_2d[i].text.text);
        }
    }
    arrfree(ctx.render_queue_2d);

    text_cache_deinit(&ctx.text_cache);
    textures_cache_deinit(&ctx.texture_cache);

    PHYSFS_deinit();
    SDL_Quit();
}


static void reset_state(void) {
    input_reset_state(&ctx.game.input);
    textures_reset_state();
}


int enter_loop(int argc, char **argv) {
    ctx.argc = argc;
    ctx.argv = argv;


    if (!initialize())
        return EXIT_FAILURE;

    for (int i = 1; i < (argc - 1); ++i) {
        if (strcmp(argv[i], "--data-dir") == 0) {
            if (!PHYSFS_mount(argv[i+1], NULL, true)) {
                CRY_PHYSFS("Data dir mount override failed.");
                return EXIT_FAILURE;
            }
        }
    }

    game_object_load();

    ctx.was_successful = true;
    ctx.game.initialization_needed = true;

    while (ctx.game.is_running) {
        if (game_object_try_reloading()) {
            ctx.game.initialization_needed = true;
            reset_state();
        }

        main_loop();
    }

    game_object_unload();

    clean_up();

    return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE;
}