#include "engine/physics.h"
#include "engine/tilemap.h"
#include "config.h"
#include <math.h>

static float s_gravity = DEFAULT_GRAVITY;
static float s_wind    = 0.0f;

void physics_init(void) {
    s_gravity = DEFAULT_GRAVITY;
    s_wind    = 0.0f;
}

void physics_set_gravity(float gravity) {
    s_gravity = gravity;
}

float physics_get_gravity(void) {
    return s_gravity;
}

void physics_set_wind(float wind) {
    s_wind = wind;
}

float physics_get_wind(void) {
    return s_wind;
}

static void resolve_tilemap_x(Body *body, const Tilemap *map) {
    if (!map) return;

    /* Check tiles the body overlaps after X movement */
    int left   = world_to_tile(body->pos.x);
    int right  = world_to_tile(body->pos.x + body->size.x - 1);
    int top    = world_to_tile(body->pos.y);
    int bottom = world_to_tile(body->pos.y + body->size.y - 1);

    body->on_wall_left = false;
    body->on_wall_right = false;

    for (int ty = top; ty <= bottom; ty++) {
        for (int tx = left; tx <= right; tx++) {
            if (!tilemap_is_solid(map, tx, ty)) continue;

            float tile_left   = tile_to_world(tx);
            float tile_right  = tile_to_world(tx + 1);

            if (body->vel.x > 0) {
                /* Moving right -> push left */
                body->pos.x = tile_left - body->size.x;
                body->vel.x = 0;
                body->on_wall_right = true;
            } else if (body->vel.x < 0) {
                /* Moving left -> push right */
                body->pos.x = tile_right;
                body->vel.x = 0;
                body->on_wall_left = true;
            }
        }
    }
}

static void resolve_tilemap_y(Body *body, const Tilemap *map) {
    if (!map) return;

    int left   = world_to_tile(body->pos.x);
    int right  = world_to_tile(body->pos.x + body->size.x - 1);
    int top    = world_to_tile(body->pos.y);
    int bottom = world_to_tile(body->pos.y + body->size.y - 1);

    body->on_ground = false;
    body->on_ceiling = false;

    for (int ty = top; ty <= bottom; ty++) {
        for (int tx = left; tx <= right; tx++) {
            uint32_t flags = tilemap_flags_at(map, tx, ty);
            if (!(flags & TILE_SOLID)) {
                /* Check one-way platforms: block when falling or resting on top.
                 * vel.y >= 0 prevents on_ground flickering when standing still —
                 * gravity adds a tiny downward vel each frame, the platform
                 * resolves it back to zero, and the cycle would repeat. */
                if ((flags & TILE_PLATFORM) && body->vel.y >= 0) {
                    float tile_top = tile_to_world(ty);
                    float body_bottom = body->pos.y + body->size.y;
                    /* Only resolve if we just crossed into the tile or are resting on it */
                    if (body_bottom - body->vel.y * DT <= tile_top + 2.0f) {
                        body->pos.y = tile_top - body->size.y;
                        body->vel.y = 0;
                        body->on_ground = true;
                    }
                }
                continue;
            }

            float tile_top    = tile_to_world(ty);
            float tile_bottom = tile_to_world(ty + 1);

            if (body->vel.y > 0) {
                /* Falling -> land on top of tile */
                body->pos.y = tile_top - body->size.y;
                body->vel.y = 0;
                body->on_ground = true;
            } else if (body->vel.y < 0) {
                /* Rising -> bonk on ceiling */
                body->pos.y = tile_bottom;
                body->vel.y = 0;
                body->on_ceiling = true;
            }
        }
    }
}

void physics_update(Body *body, float dt, const Tilemap *map) {
    /* Apply gravity */
    body->vel.y += s_gravity * body->gravity_scale * dt;

    /* Apply wind — halved on ground (friction counteracts) */
    if (s_wind != 0.0f && body->gravity_scale > 0.0f) {
        float wind_factor = body->on_ground ? 0.5f : 1.0f;
        body->vel.x += s_wind * body->gravity_scale * wind_factor * dt;
    }

    /* Clamp fall speed */
    if (body->vel.y > MAX_FALL_SPEED) {
        body->vel.y = MAX_FALL_SPEED;
    }

    /* Move X, resolve X */
    body->pos.x += body->vel.x * dt;
    resolve_tilemap_x(body, map);

    /* Move Y, resolve Y */
    body->pos.y += body->vel.y * dt;
    resolve_tilemap_y(body, map);

    /* Ground-stick probe: if not detected as on_ground but vel.y is
     * near zero (just landed or standing), check one pixel below.
     * Prevents flickering between grounded/airborne each frame due to
     * sub-pixel gravity accumulation. Checks both solid tiles and
     * one-way platforms. */
    if (!body->on_ground && body->vel.y >= 0 && body->vel.y < s_gravity * dt * 2.0f && map) {
        int left  = world_to_tile(body->pos.x);
        int right = world_to_tile(body->pos.x + body->size.x - 1);
        int probe = world_to_tile(body->pos.y + body->size.y + 1.0f);
        float body_bottom = body->pos.y + body->size.y;
        for (int tx = left; tx <= right; tx++) {
            uint32_t pf = tilemap_flags_at(map, tx, probe);
            if (pf & TILE_SOLID) {
                body->on_ground = true;
                body->vel.y = 0;
                break;
            }
            /* One-way platforms: only stick if feet are at or above the
             * platform top. Prevents snapping to a platform the player
             * is below (e.g. jumping up through it). */
            if (pf & TILE_PLATFORM) {
                float tile_top = tile_to_world(probe);
                if (body_bottom <= tile_top + 2.0f) {
                    body->on_ground = true;
                    body->vel.y = 0;
                    break;
                }
            }
        }
    }
}

bool physics_overlap(const Body *a, const Body *b) {
    return physics_aabb_overlap(a->pos, a->size, b->pos, b->size);
}

bool physics_aabb_overlap(Vec2 pos_a, Vec2 size_a, Vec2 pos_b, Vec2 size_b) {
    return pos_a.x < pos_b.x + size_b.x &&
           pos_a.x + size_a.x > pos_b.x &&
           pos_a.y < pos_b.y + size_b.y &&
           pos_a.y + size_a.y > pos_b.y;
}
