#include "state_ingame.h"

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

#include "third_party/raylib.h"
#include "third_party/raygui.h"

#include "session/game_session.h"

#include "shared/wang_hash.h"

#define GRID_CELL_SIZE 8
#define ENTITY_PRESENTATION_Y_OFFSET -2

static void state_enter(Presentation_State *state) {
    Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
    (void)ctx;

    printf("Entered ingame.\n");
    
    printf("Setting up context ...\n");
    ctx->main_camera = (Camera2D) {
        .offset     = (Vector2) { 0, 0 },
        .target     = (Vector2) { 0, 0 },
        .rotation   = 0.0f,
        .zoom       = 2.0f
    };

    // TODO: SS - Maybe put the textures in an array and index them using an enum?
    // These should probably be loaded at start-up instead of here.
    ctx->texture_shadow_basic = LoadTexture("game/assets/sprites/spr_shadow_basic.png");
    assert(IsTextureValid(ctx->texture_shadow_basic));
    ctx->texture_grass = LoadTexture("game/assets/sprites/spr_floor_grass.png");
    assert(IsTextureValid(ctx->texture_grass));
    ctx->texture_apple = LoadTexture("game/assets/sprites/spr_food_apple.png");
    assert(IsTextureValid(ctx->texture_apple));
    ctx->texture_snake_head = LoadTexture("game/assets/sprites/spr_snake_head.png");
    assert(IsTextureValid(ctx->texture_snake_head));
    ctx->texture_snake_body = LoadTexture("game/assets/sprites/spr_snake_body.png");
    assert(IsTextureValid(ctx->texture_snake_body));
}

static Simulation_Game_Input gather_input_for_local_player(uint8_t local_player_index) {
    switch(local_player_index) {
        case 0: {
            return (Simulation_Game_Input) {
                .right  = IsKeyPressed(KEY_D),
                .left   = IsKeyPressed(KEY_A)
            };
        }
        case 1: {
            return (Simulation_Game_Input) {
                .right  = IsKeyPressed(KEY_RIGHT),
                .left   = IsKeyPressed(KEY_LEFT)
            };
        }
        case 2: {
            return (Simulation_Game_Input) {
                .right  = IsKeyPressed(KEY_L),
                .left   = IsKeyPressed(KEY_J)
            };
        }
        case 3: {
            return (Simulation_Game_Input) {
                .right  = IsKeyPressed(KEY_H),
                .left   = IsKeyPressed(KEY_F)
            };
        }
        default: {
            assert(false);
            return (Simulation_Game_Input) {0};
        }
    }
}

static void state_tick(Presentation_State *state) {
    Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
    (void)ctx;
    
    Game_Instance *instance = ctx->game_instance;
    assert(instance != NULL);
    Game_Session *session = instance->game_session;
    assert(session != NULL);

    { // Push local input to queue.
        if(instance->amount_of_local_players == 1) { // If playing alone (either singleplayer or online-multiplayer), more inputs are allowed.
            Simulation_Game_Input input = (Simulation_Game_Input) {
                .right  = IsKeyPressed(KEY_RIGHT)  || IsKeyPressed(KEY_D),
                .left   = IsKeyPressed(KEY_LEFT)   || IsKeyPressed(KEY_A)
            };

            if(!simulation_input_empty(input)) {
                game_instance_push_local_input(instance, 0, input);
            }
        }
        else if(instance->amount_of_local_players > 1) {
            for(uint8_t i = 0; i < instance->amount_of_local_players; i++) {
                Simulation_Game_Input input = gather_input_for_local_player(i);
                if(!simulation_input_empty(input)) {
                    game_instance_push_local_input(instance, i, input);
                }
            }
        }
    }

    const double delta_time = GetFrameTime();
    ctx->simulation_accumulator += delta_time;

    const double sim_dt = 1.0f / session->settings.tickrate;

    while (ctx->simulation_accumulator >= sim_dt) {
        for(uint16_t i = 0; i < instance->amount_of_local_players; i++) {
            // Pop input from local-player's input_queue and set the player's input to it, if we have one.
            Simulation_Game_Input input = {0};
            game_instance_pop_local_input(instance, i, &input);
            game_session_set_player_input(session, i, input);
        }

        game_session_tick(session);
        ctx->simulation_accumulator -= sim_dt;
    }

    // TODO: SS - I believe that this is where we should send game-messages to the clients and the host.

    { // TEMP: SS
        if(IsKeyPressed(KEY_TAB)) {
            ctx->debug_draw_session_details = !ctx->debug_draw_session_details;
        }

        if(IsKeyPressed(KEY_ESCAPE)) {
            // TODO: SS - Show pause-menu instead.
            presentation_state_machine_go_to(&presentation_state_main_menu);
        }
    }
}

uint32_t floor_texture_variant(uint32_t x, uint32_t y, uint32_t seed, uint32_t num_variants) {
    return (wang_hash(x * 73856093u ^ y * 19349663u ^ seed)) % num_variants;
}

Color get_tint_for_player_id(uint16_t player_id) { // TODO: SS - Could get values from 'the active color-palette'.
    switch(player_id) {
        case 0: {
            return (Color) { 255, 135, 102, 255 };
        }
        case 1: {
            return (Color) { 230, 204, 138, 255 };
        }
        case 3: {
            return (Color) { 240, 236, 226, 255 };
        }
        case 4: {
            return (Color) { 77, 106, 148, 255 };
        }
        case 5: {
            return (Color) { 36, 43, 74, 255 };
        }
        case 6: {
            return (Color) { 153, 92, 149, 255 };
        }
        case 7: {
            return (Color) { 245, 79, 79, 255 };
        }
        case 8: {
            return (Color) { 53, 121, 133, 255 };
        }
        default: {
            return (Color) { 82, 33, 110, 255 };
        }
    }
}

#define SHADOW_ALPHA 64

static void state_render(Presentation_State *state) {
    Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
    (void)ctx;

    ClearBackground((Color) { 240, 236, 226, 255 });

    Game_Instance *instance = ctx->game_instance;
    Game_Session *session = instance->game_session;
    Simulation_World *sim_world = &session->simulation_world;
    assert(sim_world != NULL);

    Game_World *world = sim_world->game_world;
    assert(world != NULL);
    Grid *grid = &world->grid;

    uint32_t grid_total_size = grid->width * grid->height;

    BeginMode2D(ctx->main_camera); {
        // Render the level.
        for(uint32_t i = 0; i < grid_total_size; i++) {
            uint32_t x = i % grid->width;
            uint32_t y = i / grid->width;

            uint32_t pres_x = x * GRID_CELL_SIZE;
            uint32_t pres_y = y * GRID_CELL_SIZE;

            uint32_t random_floor_index = floor_texture_variant(x, y, world->seed, 4);

            DrawTextureRec(
                ctx->texture_grass,
                (Rectangle) { GRID_CELL_SIZE * random_floor_index, 0, GRID_CELL_SIZE, GRID_CELL_SIZE },
                (Vector2) { pres_x, pres_y },
                WHITE
            );
            DrawRectangleLines(pres_x, pres_y, GRID_CELL_SIZE, GRID_CELL_SIZE, (Color) { 0, 0, 0, 2 }); // TODO: SS - Let the user customize the alpha locally.
        }
        
        for(uint32_t i = 0; i < grid_total_size; i++) {
            uint32_t x = i % grid->width;
            uint32_t y = i / grid->width;

            uint32_t pres_x = x * GRID_CELL_SIZE;
            uint32_t pres_y = y * GRID_CELL_SIZE;

            Grid_Cell *cell = &grid->cells[i];
            if(cell->entity != NULL) {
                Entity *entity = cell->entity;

                switch(entity->type) {
                    case Entity_Type_Snake_Head: {

                        Color tint = (Color) { 255, 255, 255, 255 };
                        // Set the snake's tint based on player-index.
                        for(uint16_t i = 0; i < world->max_players; i++) {
                            Game_World_Player *player = &world->players[i];
                            if(!player->active) {
                                continue;
                            }

                            if(player->entity_id == entity->id) {
                                tint = get_tint_for_player_id(i); // NOTE: SS - It's more like player-index. This needs to change if they're not sorted (but they are at the moment and I don't really plan on switching that up.)
                                break;
                            }
                        }

                        Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 };

                        Entity *e = entity;
                        uint16_t i = 0;
                        while(e != NULL) { // Loop over all the body-parts of the snake, including the head.
                            float sin_frequency = 0.0f;
                            float sin_amplitude = 0.0f;

                            uint32_t pres_x = e->x * GRID_CELL_SIZE;
                            uint32_t pres_y = e->y * GRID_CELL_SIZE;
        
                            Texture2D *texture = NULL;
                            if(e->type == Entity_Type_Snake_Head) {
                                texture = &ctx->texture_snake_head;
                            }
                            else {
                                texture = &ctx->texture_snake_body;
                                sin_frequency = 4.0f;
                                sin_amplitude = 1.0f;
                            }
        
                            // Draw shadow.
                            DrawTextureEx(
                                ctx->texture_shadow_basic,
                                (Vector2) { pres_x, pres_y },
                                0.0f,
                                1.0f,
                                (Color) { 0, 0, 0, SHADOW_ALPHA }
                            );
                            
                            float rotation = 0.0f;
                            switch(e->move_direction){
                                case Entity_Movement_Direction_None: {
                                    rotation = 0.0f;
                                    break;
                                }
                                case Entity_Movement_Direction_Up: {
                                    rotation = -90.0f;
                                    break;
                                }
                                case Entity_Movement_Direction_Down: {
                                    rotation = 90.0f;
                                    break;
                                }
                                case Entity_Movement_Direction_Right: {
                                    rotation = 0.0f;
                                    break;
                                }
                                case Entity_Movement_Direction_Left: {
                                    rotation = 180.0f;
                                    break;
                                }
                            }
                            
                            DrawTexturePro(
                                *texture,
                                (Rectangle) { // Source.
                                    0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE
                                },
                                (Rectangle) { // Destination.
                                    pres_x + origin.x,
                                    pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - (sin((GetTime() + (float)(i) / 4.0) * sin_frequency)) * sin_amplitude,
                                    GRID_CELL_SIZE,
                                    GRID_CELL_SIZE
                                },
                                origin, // Origin.
                                rotation, // Rotation.
                                tint // Tint.
                            );
                            
                            e = game_world_try_get_entity_by_id(world, e->child);
                            i += 1;
                        }

                        // TODO: SS - Don't draw player-name if playing by yourself.
                        // TODO: SS - Don't draw your own player-name, only others.
                        { // Draw player-name.
                            const char *player_name = "player"; // NOTE: SS - Hardcoded.
                            const uint32_t font_size = 8;
                            int text_width = MeasureText(player_name, font_size);
                            DrawText(player_name, pres_x - (float)text_width/2.0f + 4, pres_y - 16, font_size, (Color) { 255, 255, 255, 128 });
                        }
                        
                        break;
                    }
                    case Entity_Type_Snake_Body: {
                        break;
                    }
                    case Entity_Type_Food: {
                        uint32_t flash = ((sin(GetTime() * 8) + 1)/2) * 32;
                        Color tint = (Color) { 255-flash, 255-flash, 255-flash, 255 };
    
                        Vector2 origin = (Vector2) { GRID_CELL_SIZE/2, GRID_CELL_SIZE/2 };
    
                        // Draw shadow.
                        DrawTextureEx(
                            ctx->texture_shadow_basic,
                            (Vector2) { pres_x, pres_y },
                            0.0f,
                            1.0f,
                            (Color) { 0, 0, 0, SHADOW_ALPHA }
                        );
    
                        DrawTexturePro(
                            ctx->texture_apple,
                            (Rectangle) { // Source.
                                0, 0, GRID_CELL_SIZE, GRID_CELL_SIZE
                            },
                            (Rectangle) { // Destination.
                                pres_x + origin.x,
                                pres_y + origin.y + ENTITY_PRESENTATION_Y_OFFSET - ((sin((GetTime() + (x + y)) * 12) + 1)/2) * 1,
                                GRID_CELL_SIZE,
                                GRID_CELL_SIZE
                            },
                            origin, // Origin.
                            0, // Rotation.
                            tint // Tint.
                        );
                        break;
                    }
                }
            }
        }

        // TODO: SS - Let the main-camera follow the entity you're controlling?


        // TODO: SS - Switch on 'sim_world->match_state'.

        if(ctx->debug_draw_session_details) {
            char buf[2048];

            // Add generic info about the game to the buffer.
            uint32_t i = snprintf(&buf[0], sizeof(buf),
                "Match-state: %i\n"
                "Tick: %lu\n"
                "\n"
                "Seed: %u\n"
                "Level: %ux%u\n"
                "Tickrate: %.2f\n"
                ,
                sim_world->match_state, sim_world->tick,
                sim_world->game_world->seed,
                sim_world->game_world->grid.width, sim_world->game_world->grid.height,
                session->settings.tickrate
            );
            
            // Now, write info specific to whether you're online or offline.
            if(session->settings.online) {
                Game_Networking *networking = &instance->game_networking;

                snprintf(&buf[i], sizeof(buf),
                    "\n"
                    "[Online]\n"
                    "Host: %s\n"
                    "Session-id: %s\n"
                    "Client-id: %s\n"
                    "\n"
                    "Local player: %u\n"
                    "Player count: %u / %u\n"
                    ,
                    networking->is_host ? "true" : "false",
                    networking->session_id,
                    networking->local_client_id,
                    instance->locally_controlled_players[0].player_id,
                    game_session_get_amount_of_active_players(session), sim_world->max_players
                );
            }

            // Finally, draw the buffer to the screen (with nice shadowing!).
            DrawText(buf, 17, 17, 8, BLACK);
            DrawText(buf, 16, 16, 8, WHITE);
        }
    }
    EndMode2D();
}

static void state_exit(Presentation_State *state) {
    Presentation_State_Ingame_Context *ctx = (Presentation_State_Ingame_Context *)state->context;
    (void)ctx;
    printf("Exiting ingame\n");

    UnloadTexture(ctx->texture_shadow_basic);
    UnloadTexture(ctx->texture_grass);
    UnloadTexture(ctx->texture_apple);
    UnloadTexture(ctx->texture_snake_head);
    UnloadTexture(ctx->texture_snake_body);

    game_instance_stop_session(ctx->game_instance);
    ctx->game_instance = NULL;
}

Presentation_State presentation_state_ingame;

void presentation_state_ingame_init(Presentation_State_Ingame_Context *ctx) {
    presentation_state_ingame = (Presentation_State) {
        .name = "Ingame",
        .context = (void *)ctx,
        .enter = state_enter,
        .tick = state_tick,
        .render = state_render,
        .exit = state_exit
    };
}