#include "game_world.h"

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

#define MIN_LEVEL_WIDTH 8 // TODO: SS - Move these out of here.
#define MIN_LEVEL_HEIGHT 8

Game_World *game_world_create(uint32_t seed, uint16_t max_players, uint16_t level_width, uint16_t level_height) {
    assert(level_width >= MIN_LEVEL_WIDTH); // We should probably have failed earlier.
    assert(level_height >= MIN_LEVEL_HEIGHT);

    Game_World *w = (Game_World *)calloc(1, sizeof(Game_World));
    assert(w != NULL);

    w->seed = seed;
    random_init(&w->random_generator, w->seed);

    w->max_players = max_players;
    w->players = (Game_World_Player *)calloc(w->max_players, sizeof(Game_World_Player));
    assert(w->players != NULL);

    w->max_entities = level_width * level_height;
    w->entities = (Entity *)calloc(w->max_entities, sizeof(Entity));
    assert(w->entities != NULL);

    // Set up entity-id queue.
    assert(squeue_init(&w->entity_id_queue, w->max_entities, sizeof(Entity_ID)));
    for(uint16_t i = 0; i < w->entity_id_queue.capacity; i++) {
        Entity_ID id = (Entity_ID)i;
        assert(squeue_push(&w->entity_id_queue, &id));
    }

    grid_initialize(&w->grid, level_width, level_height);

    // Spawn initial food(s). One per player.
    for(uint16_t i = 0; i < w->max_players; i++) {
        Entity_ID entity_food;

        uint16_t x = 0;
        uint16_t y = 0;
        assert(game_world_find_position_to_spawn(w, &x, &y));
        
        assert(game_world_create_entity(
            w,
            Entity_Type_Food,
            x, y,
            &entity_food
        ));
    }
    
    return w;
}

void game_world_destroy(Game_World *world) {
    assert(world != NULL);
    
    world->seed = 0;

    world->max_players = 0;
    free(world->players);
    world->players = NULL;
    
    world->max_entities = 0;
    free(world->entities);
    world->entities = NULL;
    
    grid_dispose(&world->grid);
    squeue_free(&world->entity_id_queue);
    
    free(world);
}

bool game_world_create_entity(Game_World *world, Entity_Type type, uint16_t x, uint16_t y, Entity_ID *out_entity_id) {
    assert(world != NULL);

    *out_entity_id = INVALID_ENTITY_ID;

    if(x >= world->grid.width) {
        printf("Failed to create entity; Invalid x-coordinate.\n");
        return false;
    }
    if(y >= world->grid.height) {
        printf("Failed to create entity; Invalid y-coordinate.\n");
        return false;
    }
    
    Entity_ID id = 0;
    if(!squeue_pop(&world->entity_id_queue, (void *)&id)) {
        printf("No free entity ids.\n");
        return false;
    }
    
    world->entities[id] = (Entity) {
        .active = true,
        .id = id,
        .type = type,
        .child = INVALID_ENTITY_ID
    };

    Grid_Cell *cell = grid_get_cell(&world->grid, x, y); // TEMP: SS - Hardcoded coordinates. // TODO: SS - Find good coordinate.
    assert(cell != NULL);
    assert(grid_try_add_entity_to_cell(cell, &world->entities[id]));

    *out_entity_id = id;
    return true;
}

void game_world_destroy_entity(Game_World *world, Entity_ID entity_id, bool destroy_children) {
    assert(world != NULL);
    assert(entity_id != INVALID_ENTITY_ID);

    Entity_ID id_to_remove = entity_id;
    
    while(id_to_remove != INVALID_ENTITY_ID) {
        Entity *e = game_world_try_get_entity_by_id(
            world,
            id_to_remove
        );
        assert(e != NULL);

        Grid_Cell *cell = grid_get_cell(&world->grid, e->x, e->y);
        assert(cell != NULL);
        assert(grid_try_remove_entity_from_cell(cell));

        squeue_push(&world->entity_id_queue, (void *)&e->id);
        
        id_to_remove = e->child;
        memset(e, 0, sizeof(Entity));

        if(!destroy_children) {
            break;
        }
    }
}

static void kill_and_respawn_player(Game_World *world, Entity *entity) {
    assert(world != NULL);
    assert(entity != NULL);

    // Loop over the players to find the owner of this entity.
    Game_World_Player *owner_player = NULL;
    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) {
            owner_player = player;
            break;
        }
    }
    assert(owner_player != NULL);

    game_world_destroy_entity(world, entity->id, true);
    owner_player->entity_id = INVALID_ENTITY_ID;
    
    { // Respawn.
        Entity_ID entity_snake_id;

        uint16_t spawn_x = 0;
        uint16_t spawn_y = 0;
        assert(game_world_find_position_to_spawn(world, &spawn_x, &spawn_y));

        assert(game_world_create_entity(
            world,
            Entity_Type_Snake_Head,
            spawn_x, spawn_y,
            &entity_snake_id
        ));

        owner_player->entity_id = entity_snake_id;

        Entity *player_entity = game_world_try_get_entity_by_id(
            world,
            owner_player->entity_id
        );

        // Set initial move-direction for player-entities.
        player_entity->move_direction = game_world_choose_initial_move_direction_based_on_coords(
            world,
            spawn_x, spawn_y
        );
    }
}

void game_world_tick(Game_World *world) {
    // Game-logic! :)
    
    for(uint16_t i = 0; i < world->max_entities; i++) {
        Entity *entity = &world->entities[i];
        if(!entity->active) {
            continue;
        }

        switch(entity->type) {
            case Entity_Type_Snake_Head: {
                { // Move entity
                    int16_t dx = 0;
                    int16_t dy = 0;
        
                    switch(entity->move_direction) {
                        case Entity_Movement_Direction_None: {
                            break;
                        }
                        case Entity_Movement_Direction_Up: {
                            dy = -1;
                            break;
                        }
                        case Entity_Movement_Direction_Down: {
                            dy = 1;
                            break;
                        }
                        case Entity_Movement_Direction_Right: {
                            dx = 1;
                            break;
                        }
                        case Entity_Movement_Direction_Left: {
                            dx = -1;
                            break;
                        }
                    }
        
                    if(dx != 0 || dy != 0) {
                        // Try moving.
        
                        // Figure out what cell we're on and what cell we want to go to.
                        Grid_Cell *current_cell = grid_get_cell(&world->grid, entity->x, entity->y);
                        assert(current_cell != NULL);
                        Grid_Cell *target_cell  = grid_get_cell(&world->grid, entity->x + dx, entity->y + dy);
        
                        if(target_cell == NULL) {
                            // Target cell does not exist.

                            // TODO: SS - Check the session's settings.
                            // Maybe each session could decide whether the snake should "loop-around" or not.
                            // If the snake shouldn't loop around, die. For now, we say that it's not okay to loop around.
                            bool should_loop_around = false; 
                            if(should_loop_around) {
                                // TODO: SS - Implement looping around.
                            }
                            else {
                                kill_and_respawn_player(world, entity);
                            }
                        }
                        else {
                            bool should_move_to_target_cell = false;
                            bool snake_ate = false;
        
                            if(target_cell->entity != NULL) {
                                // Target cell is occupied.
        
                                // Check what type of entity it is and determine what should happen.
                                switch(target_cell->entity->type) {
                                    case Entity_Type_Snake_Head: {
                                        kill_and_respawn_player(world, entity);
                                        break;
                                    }
                                    case Entity_Type_Snake_Body: {
                                        kill_and_respawn_player(world, entity);
                                        break;
                                    }
                                    case Entity_Type_Food: {
                                        // Eat!
                                        assert(grid_try_remove_entity_from_cell(target_cell));
        
                                        snake_ate = true;
                                        should_move_to_target_cell = true;
        
                                        break;
                                    }
                                }
                            }
                            else {
                                // Target cell is unoccupied and free to enter.
                                should_move_to_target_cell = true;
                            }
                            
                            if(should_move_to_target_cell) {
                                { // Move snake recursively.
                                    Entity *e = entity;
                                    Grid_Cell *t = target_cell;
                                    while(e != NULL) {
                                        Grid_Cell *a = grid_get_cell(&world->grid, e->x, e->y);
                                        assert(a != NULL);
                                        
                                        grid_move_entity_from_cell_to_cell(a, t);
        
                                        e = game_world_try_get_entity_by_id(world, e->child);
                                        t = a;
                                    }
                                }
        
                                if(snake_ate) {
                                    Entity *e = entity;
                                    uint16_t child_index = 0;
                                    while(e->child != INVALID_ENTITY_ID) {
                                        e = game_world_try_get_entity_by_id(world, e->child);
                                        child_index += 1;
                                    }
        
                                    Entity_ID child_entity = INVALID_ENTITY_ID;
                                    assert(game_world_create_entity(
                                        world,
                                        Entity_Type_Snake_Body,
                                        e->prev_x,
                                        e->prev_y,
                                        &child_entity
                                    ));
        
                                    assert(e->child == INVALID_ENTITY_ID);
                                    e->child = child_entity;
        
                                    { // Spawn food.
                                        uint16_t food_spawn_x = 0;
                                        uint16_t food_spawn_y = 0;
        
                                        if(!game_world_find_position_to_spawn(world, &food_spawn_x, &food_spawn_y)) {
                                            printf("Failed to find a position to spawn food.\n");
                                        }
                                        else {
                                            Entity_ID entity_food;
                                            assert(game_world_create_entity(
                                                world,
                                                Entity_Type_Food,
                                                food_spawn_x, food_spawn_y,
                                                &entity_food
                                            ));
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                break;
            }
            case Entity_Type_Snake_Body: {
                // Skip.
                break;
            }
            case Entity_Type_Food: {
                // Skip (for now).
                break;
            }
            default: {
                printf("Unhandled entity-type %i.\n", entity->type);
                break;
            }
        }

    }
}

bool game_world_add_player(Game_World *world, Game_World_Player **out_gw_player) {
    assert(world != NULL);
    assert(out_gw_player != NULL);

    for(uint16_t i = 0; i < world->max_players; i++) {
        Game_World_Player *player = &world->players[i];
        
        if(player->active) {
            continue;
        }

        // Found free player.
        player->active = true;
        *out_gw_player = player;
        return true;
    }
    
    return false;
}

bool game_world_remove_player(Game_World *world, Game_World_Player *player) {
    assert(world != NULL);
    assert(player != NULL);

    game_world_destroy_entity(world, player->entity_id, true);

    memset(player, 0, sizeof(Game_World_Player));

    return true;
}


Entity *game_world_try_get_entity_by_id(Game_World *world, Entity_ID id) {
    assert(world != NULL);

    if(id >= world->max_entities) {
        return NULL;
    }

    return &world->entities[id];
}

bool game_world_find_position_to_spawn(Game_World *world, uint16_t *out_x, uint16_t *out_y) {
    assert(world != NULL);
    assert(out_x != NULL);
    assert(out_y != NULL);
    
    uint32_t attempts = world->grid.width * world->grid.height;
    while(attempts > 0) {
        uint16_t potential_x = random_u32_range(&world->random_generator, 0, (uint32_t) world->grid.width - 1);
        uint16_t potential_y = random_u32_range(&world->random_generator, 0, (uint32_t) world->grid.height - 1);

        Grid_Cell *cell = grid_get_cell(&world->grid, potential_x, potential_y);
        assert(cell != NULL);

        // Check if cell is free.
        if(cell->entity == NULL) {
            *out_x = potential_x;
            *out_y = potential_y;
            return true;
        }

        if(attempts > 0) {
            attempts -= 1;
        }
        else {
            break;
        }
    }
    
    *out_x = 0;
    *out_y = 0;
    return false;
}

Entity_Movement_Direction game_world_choose_initial_move_direction_based_on_coords(Game_World *world, uint16_t x, uint16_t y) {
    assert(world != NULL);

    Grid *grid = &world->grid;
    
    // TODO: SS - Consider potential obstacles in the grid (lava, walls etc) if we ever have them.

    if (x == 0) {
        return Entity_Movement_Direction_Right;
    }

    if (x == grid->width - 1) {
        return Entity_Movement_Direction_Left;
    }

    if (y == 0) {
        return Entity_Movement_Direction_Down;
    }

    if (y == grid->height - 1) {
        return Entity_Movement_Direction_Up;
    }

    return Entity_Movement_Direction_Up;
}