#include "player.h"
#include "world.h"

#include "twn_game_api.h"

#include <SDL2/SDL.h>

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


static void update_timers(Player *player) {
    player->jump_air_timer = player->jump_air_timer - 1 <= 0 ? 0 : player->jump_air_timer - 1;
    player->jump_coyote_timer = player->jump_coyote_timer - 1 <= 0 ? 0 : player->jump_coyote_timer - 1;
    player->jump_buffer_timer = player->jump_buffer_timer - 1 <= 0 ? 0 : player->jump_buffer_timer - 1;
}


static void input_move(Player *player) {
    /* apply horizontal damping when the player stops moving */
    /* in other words, make it decelerate to a standstill */
    if (!input_action_pressed("player_left") &&
        !input_action_pressed("player_right"))
    {
        player->dx *= player->horizontal_damping;
    }

    int input_dir = 0;
    if (input_action_pressed("player_left"))
        input_dir = -1;
    if (input_action_pressed("player_right"))
        input_dir = 1;
    if (input_action_pressed("player_left") &&
        input_action_pressed("player_right"))
        input_dir = 0;

    player->dx += (float)input_dir * player->run_horizontal_speed;
    player->dx = SDL_clamp(player->dx, -player->max_dx, player->max_dx);

    if (fabs(player->dx) < player->run_horizontal_speed) {
        player->dx = 0;
    }
}


static void jump(Player *player) {
    player->jump_coyote_timer = 0;
    player->jump_buffer_timer = 0;
    player->dy = player->jump_force_initial;
    player->action = PLAYER_ACTION_JUMP;
    player->jump_air_timer = player->jump_air_ticks;
}


static void input_jump(Player *player) {
    player->current_gravity_multiplier = player->jump_default_multiplier;

    if (input_action_just_pressed("player_jump")) {
        player->jump_air_timer = 0;
        player->jump_buffer_timer = player->jump_buffer_ticks;

        if (player->action == PLAYER_ACTION_GROUND || player->jump_coyote_timer > 0) {
            jump(player);
        }
    }

    if (input_action_pressed("player_jump")) {
        if (player->action != PLAYER_ACTION_GROUND && player->jump_air_timer > 0) {
            player->current_gravity_multiplier = player->jump_boosted_multiplier;
            player->dy += player->jump_force_increase;
        }
    }
}


static void update_collider_x(Player *player) {
    player->collider_x.w = player->rect.w;
    player->collider_x.h = player->rect.h - 8;
    player->collider_x.x = player->rect.x;
    player->collider_x.y = player->rect.y + ((player->rect.h - player->collider_x.h) / 2);
}


static void update_collider_y(Player *player) {
    player->collider_y.w = player->rect.w;
    player->collider_y.h = player->rect.h;
    player->collider_y.x = player->rect.x + ((player->rect.w - player->collider_y.w) / 2);
    player->collider_y.y = player->rect.y;
}


static void apply_gravity(Player *player, float gravity) {
    player->dy -= gravity * player->current_gravity_multiplier;
    player->dy = fmax(player->dy, -player->terminal_velocity);

    if (player->dy < 0) {
        /* just started falling */
        if (player->action == PLAYER_ACTION_GROUND) {
            player->jump_coyote_timer = player->jump_coyote_ticks;
        }

        player->action = PLAYER_ACTION_FALL;
    }
}


/* returns whether or not a correction was applied */
static bool corner_correct(Player *player, Rect collision) {
    /*
     * somewhat of a hack here. we only want to do corner correction
     * if the corner in question really is the corner of a "platform,"
     * and not simply the edge of a single tile in a row of many
     * tiles, as that would briefly clip the player into the ceiling,
     * halting movement.  the dumbest way i could think of to fix this
     * is to simply ensure that there's no tile to possibly clip into
     * before correcting, by checking if there's a tile right above
     * the center of the player
     */
    if (world_is_tile_at(player->world,
                         player->rect.x + (player->rect.w / 2),
                         player->rect.y - 1))
    {
        return false;
    }

    float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
    float collision_center_x = collision.x + (collision.w / 2); 
    float abs_difference = fabs(player_center_x - collision_center_x);

    /* we're good, no correction needed */
    if (abs_difference < player->jump_corner_correction_offset)
        return false;

    /* collision was on player's right side */
    if (player_center_x < collision_center_x) {
        player->rect.x -= collision.w / 2;
    }
    /* collision was on player's left side */
    else if (player_center_x > collision_center_x) {
        player->rect.x += collision.w / 2;
    }
    
    return true;
}


static void calc_collisions_x(Player *player) {
    Rect collision;
    bool is_colliding = world_find_rect_intersects(player->world, player->collider_x, &collision);
    if (!is_colliding) return;

    float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
    float collision_center_x = collision.x + (collision.w / 2); 

    typedef enum CollisionDirection { COLLISION_LEFT = -1, COLLISION_RIGHT = 1 } CollisionDirection;
    CollisionDirection dir_x =
        player_center_x > collision_center_x ? COLLISION_LEFT : COLLISION_RIGHT;

    player->rect.x -= collision.w * (float)dir_x;
    player->dx = 0;
}


static void calc_collisions_y(Player *player) {
    Rect collision;
    bool is_colliding = world_find_rect_intersects(player->world, player->collider_y, &collision);
    if (!is_colliding) return;

    float player_center_y = player->collider_y.y + (player->collider_y.h / 2);
    float collision_center_y = collision.y + (collision.h / 2); 

    typedef enum CollisionDirection { COLLISION_ABOVE = -1, COLLISION_BELOW = 1 } CollisionDirection;
    CollisionDirection dir_y =
        player_center_y > collision_center_y ? COLLISION_ABOVE : COLLISION_BELOW;

    /* before the resolution */
    if (dir_y == COLLISION_ABOVE) {
        if (corner_correct(player, collision)) {
            return;
        }
    }

    /* resolution */
    player->rect.y -= collision.h * (float)dir_y;
    player->dy = 0;

    /* after the resolution */
    if (dir_y == COLLISION_BELOW) {
        /* bandaid fix for float precision-related jittering */
        player->rect.y = ceilf(player->rect.y);

        player->action = PLAYER_ACTION_GROUND;

        if (player->jump_buffer_timer > 0) {
            jump(player);
            apply_gravity(player, player->world->gravity);
        }
    } else if (dir_y == COLLISION_ABOVE) {
        player->jump_air_timer = 0;
    }
}


Player *player_create(World *world) {
    Player *player = malloc(sizeof *player);

    *player = (Player) {
        .world = world,

        .sprite_w = 48,
        .sprite_h = 48,

        .rect = (Rect) {
            .x = 92,
            .y = 200,
            .w = 16,
            .h = 32,
        },

        .action = PLAYER_ACTION_GROUND,

        .collider_thickness = 42,
        .max_dx = 8,
        .run_horizontal_speed = 0.5f,
        .horizontal_damping = 0.9f,
        .current_gravity_multiplier = 1,
        .terminal_velocity = 30,

        .jump_force_initial = 10,
        .jump_force_increase = 1,
        .jump_air_ticks = 18,
        .jump_coyote_ticks = 8,
        .jump_buffer_ticks = 8,

        .jump_default_multiplier = 1.4f,
        .jump_boosted_multiplier = 1,

        .jump_corner_correction_offset = 16.0f,
    };

    return player;
}


static void drawdef(Player *player) {
    m_sprite("/assets/player/baron-walk.png",
            (Rect) {
                .x = player->rect.x + ((player->rect.w - player->sprite_w) / 2),
                .y = player->rect.y - 8,
                .w = player->sprite_w,
                .h = player->sprite_h,
            });

    draw_circle((Vec2) { 256, 128 },
                24,
                (Color) { 255, 0, 0, 255 });

    draw_circle((Vec2) { 304, 128 },
                24,
                (Color) { 255, 0, 0, 255 });
}


static void drawdef_debug(Player *player) {
    if (!ctx.debug)
        return;

    /* const int info_separation = 24; */
    /* const struct RectPrimitive info_theme = { */
    /*  .x = 8, */
    /*  .r = 0, .g = 0, .b = 0, .a = 255, */
    /* }; */

    draw_rectangle(player->collider_x,
                   (Color){ 0, 0, 255, 128 });

    draw_rectangle(player->collider_y,
                   (Color){ 0, 0, 255, 128 });
}


void player_destroy(Player *player) {
    free(player);
}


void player_calc(Player *player) {
    update_timers(player);

    input_move(player);
    input_jump(player);

    player->rect.x += player->dx;
    update_collider_x(player);
    calc_collisions_x(player);

    apply_gravity(player, player->world->gravity);
    player->rect.y -= player->dy;
    update_collider_y(player);
    calc_collisions_y(player);

    update_collider_x(player);
    update_collider_y(player);

    drawdef(player);
    drawdef_debug(player);
}