#include "game_session.h"

#include <stddef.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>

#include "shared/squeue.h"

#define INPUT_QUEUE_CAPACITY 8

Game_Session *g_current_session = NULL;

void game_session_create(bool is_singleplayer, bool is_host, Game_Session_Settings settings) {
    assert(g_current_session == NULL);

    Game_Session *session = (Game_Session *)calloc(1, sizeof(Game_Session));
    session->is_singleplayer = is_singleplayer;
    session->is_host = is_host;
    session->settings = settings;

    session->simulation_world = simulation_create_world(
        session->settings.seed,
        session->settings.max_players,
        session->settings.level_width,
        session->settings.level_height
    );

    session->players        = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player));
    session->players_prev   = (Game_Session_Player *)calloc(session->settings.max_players, sizeof(Game_Session_Player));

    for(uint16_t i = 0; i < session->settings.max_players; i++) {
        assert(squeue_init(
            &session->players[i].input_queue,
            INPUT_QUEUE_CAPACITY,
            sizeof(Simulation_Game_Input)
        ));

        // Intentionally not initializing players_prev's input_queue.
    }
    
    { // TEMP
        session->local_player_index = 0;
        assert(game_session_create_player(session, &session->local_player_index));
    }

    session->simulation_accumulator = 0.0f;

    g_current_session = session;

    printf("New Game_Session created.\n");
}

void game_session_destroy() {
    Game_Session *session = g_current_session;

    if(session == NULL) {
        return;
    }

    simulation_destroy_world(&session->simulation_world);

    for(uint16_t i = 0; i < session->settings.max_players; i++) {
        squeue_free(&session->players[i].input_queue);

        // Intentionally not freeing players_prev's input_queue.
    }

    free(session->players);
    session->players = NULL;
    free(session->players_prev);
    session->players_prev = NULL;

    free(session);
    g_current_session = NULL;
}

void game_session_init_default_settings(bool is_singleplayer, Game_Session_Settings *out_settings) {
    out_settings->seed         = 1337; // TODO: SS - Randomize.
    out_settings->tickrate     = 10.0;
    out_settings->level_width  = 32;
    out_settings->level_height = 32;
    out_settings->max_players  = is_singleplayer ? 1 : 8;
}

static void game_session_tick(Game_Session *session) {
    // Update input.
    for(uint16_t i = 0; i < session->settings.max_players; i++) {
        Game_Session_Player *session_player = &session->players[i];

        { // Check if any players have joined/disconnected by comparing against the previous session_player.
            Game_Session_Player *session_player_prev = &session->players_prev[i];
            
            bool was_active = session_player_prev->active;
            bool is_active  = session_player->active;
            
            if (!was_active && is_active) {
                simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
                    .type = Simulation_Command_Type_Add_Player,
                    .player_id = i
                });
            } else if (was_active && !is_active) {
                simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
                    .type = Simulation_Command_Type_Remove_Player,
                    .player_id = i
                });
            }
        }
            
        if(!session_player->active) {
            continue;
        }

        Simulation_Game_Input input = {0};
        if(squeue_pop(&session_player->input_queue, &input)) {
            // We got an input.

            simulation_world_enqueue_command(&session->simulation_world, (Simulation_Command) {
                .type = Simulation_Command_Type_Set_Player_Input,
                .player_id = i,
                .player_input = input,
            });
        }
    }
    
    // Tick.
    simulation_world_tick(&session->simulation_world);

    // Copy 'players' from session to 'players_prev'.
    memcpy(session->players_prev, session->players, sizeof(Game_Session_Player) * session->settings.max_players);
}

void game_session_update(Game_Session *session, const double delta_time) {
    session->simulation_accumulator += delta_time;
    
    const double sim_dt = 1.0 / session->settings.tickrate;

    while (session->simulation_accumulator >= sim_dt) {
        game_session_tick(session);
        session->simulation_accumulator -= sim_dt;
    }
}

static Game_Session_Player *game_session_get_player_from_id(Game_Session *session, uint16_t player_index) {
    assert(session != NULL);
    if(player_index >= session->settings.max_players) {
        return NULL;
    }
    
    return &session->players[player_index];
}

Game_Session_Player *game_session_get_local_player(Game_Session *session) {
    assert(session != NULL);
    return game_session_get_player_from_id(session, session->local_player_index);
}


bool game_session_create_player(Game_Session *session, uint16_t *out_player_index) {
    assert(session != NULL);

    int32_t found_index = -1;
    for(uint16_t i = 0; i < session->settings.max_players; i++) {
        Game_Session_Player *session_player = &session->players[i];
        assert(session_player != NULL);
        if(!session_player->active) {
            found_index = i;

            session_player->active = true;
            break;
        }
    }

    if(found_index < 0) {
        return false;
    }

    *out_player_index = (uint16_t)found_index;
    return true;
}

void game_session_destroy_player(Game_Session *session, uint16_t player_index) {
    assert(session != NULL);
    assert(player_index < session->settings.max_players);
    
    Game_Session_Player *player = &session->players[player_index];
    assert(player != NULL);
    assert(player->active);

    memset(player, 0, sizeof(Game_Session_Player));
}

bool game_session_enqueue_player_input(Game_Session *session, uint16_t player_index, Simulation_Game_Input input) {
    Game_Session_Player *player = game_session_get_player_from_id(session, player_index);
    assert(player != NULL);

    if(simulation_input_equal(player->most_recent_input, input)) {
        // Ignore 'input' if it's equal to the most recent input. This way we avoid duplicates.
        return false;
    }

    // TODO: SS - Check if 'input' is a valid "snake-move".
    // Inputting right when going left will sadly be "okay" here which will make the game
    // feel less responsive.

    if(!squeue_push(&player->input_queue, (void *)&input)) {
        return false;
    }

    player->most_recent_input = input;
    return true;
}