#include "game/player.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/input.h"
#include "engine/physics.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include "engine/audio.h"
#include "engine/assets.h"
#include <stdlib.h>
#include <string.h>
#include <math.h>

static EntityManager *s_em = NULL;

/* ── Sound effects ───────────────────────────────── */
static Sound s_sfx_jump;
static Sound s_sfx_shoot;
static Sound s_sfx_dash;
static bool  s_sfx_loaded = false;

/* ── Weapon sprite ───────────────────────────────── */
static SDL_Texture *s_weapon_tex = NULL;
static SDL_Rect     s_weapon_src = {0};    /* cropped source rect        */
static float        s_weapon_render_w = 0; /* display width in game px   */
static float        s_weapon_render_h = 0; /* display height in game px  */

/* Max camera offset when looking up (pixels) */
#define LOOK_UP_OFFSET   80.0f
#define LOOK_UP_DELAY    0.3f  /* seconds of holding up before camera pans */
#define LOOK_UP_SPEED    3.0f  /* camera pan speed multiplier              */

/* Respawn timing */
#define RESPAWN_DELAY    1.0f  /* seconds after death anim before respawn  */

/* ── Astronaut sprite animations ─────────────────── */
/* Built from the PNG spritesheets at load time        */

static bool s_anims_loaded = false;

/* Idle: 144x24, 6 frames of 24x24 */
static AnimFrame s_idle_frames[6];
static AnimDef   s_anim_idle;

/* Run: 144x24, 6 frames of 24x24 */
static AnimFrame s_run_frames[6];
static AnimDef   s_anim_run;

/* Jump: 120x24, 5 frames of 24x24 */
/* We split this: first 3 = ascending, last 2 = falling */
static AnimFrame s_jump_frames[3];
static AnimDef   s_anim_jump;

static AnimFrame s_fall_frames[2];
static AnimDef   s_anim_fall;

/* Death: 128x32, 4 frames of 32x32 */
static AnimFrame s_death_frames[4];
static AnimDef   s_anim_death;

static void build_strip_frames(AnimFrame *out, int count, int frame_w, int frame_h,
                               float duration) {
    for (int i = 0; i < count; i++) {
        out[i].src = (SDL_Rect){ i * frame_w, 0, frame_w, frame_h };
        out[i].duration = duration;
    }
}

static void load_astronaut_anims(void) {
    if (s_anims_loaded) return;

    SDL_Texture *tex_idle  = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Idle.png");
    SDL_Texture *tex_run   = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Run.png");
    SDL_Texture *tex_jump  = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Jump.png");
    SDL_Texture *tex_death = assets_get_texture("assets/sprites/player/Astronaut/Astronaut_Death.png");

    /* Idle: 6 frames, 24x24 */
    build_strip_frames(s_idle_frames, 6, PLAYER_SPRITE_W, PLAYER_SPRITE_H, 0.15f);
    s_anim_idle = (AnimDef){
        .frames      = s_idle_frames,
        .frame_count = 6,
        .looping     = true,
        .texture     = tex_idle,
    };

    /* Run: 6 frames, 24x24 */
    build_strip_frames(s_run_frames, 6, PLAYER_SPRITE_W, PLAYER_SPRITE_H, 0.1f);
    s_anim_run = (AnimDef){
        .frames      = s_run_frames,
        .frame_count = 6,
        .looping     = true,
        .texture     = tex_run,
    };

    /* Jump (ascending): first 3 frames from jump sheet */
    for (int i = 0; i < 3; i++) {
        s_jump_frames[i].src = (SDL_Rect){ i * PLAYER_SPRITE_W, 0,
                                           PLAYER_SPRITE_W, PLAYER_SPRITE_H };
        s_jump_frames[i].duration = 0.12f;
    }
    s_anim_jump = (AnimDef){
        .frames      = s_jump_frames,
        .frame_count = 3,
        .looping     = false,
        .texture     = tex_jump,
    };

    /* Fall (descending): last 2 frames from jump sheet */
    for (int i = 0; i < 2; i++) {
        s_fall_frames[i].src = (SDL_Rect){ (3 + i) * PLAYER_SPRITE_W, 0,
                                           PLAYER_SPRITE_W, PLAYER_SPRITE_H };
        s_fall_frames[i].duration = 0.15f;
    }
    s_anim_fall = (AnimDef){
        .frames      = s_fall_frames,
        .frame_count = 2,
        .looping     = false,
        .texture     = tex_jump,  /* same texture, different frames */
    };

    /* Death: 4 frames, 32x32 */
    build_strip_frames(s_death_frames, 4, PLAYER_DEATH_SPRITE_W, PLAYER_DEATH_SPRITE_H, 0.2f);
    s_anim_death = (AnimDef){
        .frames      = s_death_frames,
        .frame_count = 4,
        .looping     = false,
        .texture     = tex_death,
    };

    s_anims_loaded = true;

    /* Load weapon sprite (mac) */
    if (!s_weapon_tex) {
        s_weapon_tex = assets_get_texture("assets/sprites/weapons/mac.png");
        if (s_weapon_tex) {
            /* Crop to actual gun pixels within the 128x128 canvas */
            s_weapon_src = (SDL_Rect){ 52, 51, 30, 17 };
            /* Scale to fit the 24x24 character — gun ~12x7 game pixels */
            s_weapon_render_w = 12.0f;
            s_weapon_render_h = 7.0f;
        }
    }

    /* Load player sound effects */
    if (!s_sfx_loaded) {
        s_sfx_jump  = audio_load_sound("assets/sounds/jump.wav");
        s_sfx_shoot = audio_load_sound("assets/sounds/laserShoot.wav");
        s_sfx_dash  = audio_load_sound("assets/sounds/dash.wav");
        s_sfx_loaded = true;
    }
}

/* ── Entity manager ref ──────────────────────────── */

void player_set_entity_manager(EntityManager *em) {
    s_em = em;
}

void player_register(EntityManager *em) {
    entity_register(em, ENT_PLAYER, player_update, player_render, player_destroy);
}

/* ── Update ──────────────────────────────────────── */

void player_update(Entity *self, float dt, const Tilemap *map) {
    PlayerData *pd = (PlayerData *)self->data;
    if (!pd) return;

    /* Handle death */
    if (self->flags & ENTITY_DEAD) {
        animation_set(&self->anim, &s_anim_death);
        animation_update(&self->anim, dt);

        /* After death animation finishes, count down to respawn */
        if (self->anim.finished) {
            pd->respawn_timer -= dt;
        }
        return;
    }

    /* Fall off bottom of level — lose 1 HP and auto-dash upward */
    float level_bottom = (float)(map->height * TILE_SIZE);
    if (self->body.pos.y + self->body.size.y > level_bottom &&
        !(self->flags & ENTITY_INVINCIBLE)) {
        self->health--;
        if (self->health <= 0) {
            /* Out of HP — die as before */
            self->health = 0;
            self->flags |= ENTITY_DEAD;
            pd->respawn_timer = 0.3f;
            return;
        }

        /* Drain all jetpack charges */
        pd->dash_charges = 0;
        pd->dash_recharge_timer = PLAYER_DASH_RECHARGE;

        /* Auto-trigger upward dash (ignores charge requirement) */
        pd->dash_timer = PLAYER_DASH_DURATION;
        pd->dash_dir   = vec2(0.0f, -1.0f);
        self->body.vel.y = 0;

        /* Grant invincibility so this doesn't re-trigger immediately */
        pd->inv_timer = PLAYER_INV_TIME;
        self->flags |= ENTITY_INVINCIBLE;

        /* Effects */
        Vec2 burst_pos = vec2(
            self->body.pos.x + self->body.size.x * 0.5f,
            self->body.pos.y + self->body.size.y
        );
        particle_emit_jetpack_burst(burst_pos, vec2(0.0f, -1.0f));
        audio_play_sound(s_sfx_dash, 96);
    }

    /* Update invincibility */
    if (pd->inv_timer > 0) {
        pd->inv_timer -= dt;
        if (pd->inv_timer <= 0) {
            pd->inv_timer = 0;
            self->flags &= ~ENTITY_INVINCIBLE;
        }
    }

    Body *body = &self->body;

    /* ── Read directional input ──────────────── */
    bool hold_left  = input_held(ACTION_LEFT);
    bool hold_right = input_held(ACTION_RIGHT);
    bool hold_up    = input_held(ACTION_UP);
    bool hold_down  = input_held(ACTION_DOWN);

    /* ── Determine aim direction ─────────────── */
    if (hold_up && (hold_left || hold_right)) {
        pd->aim_dir = AIM_DIAG_UP;
    } else if (hold_up) {
        pd->aim_dir = AIM_UP;
    } else {
        pd->aim_dir = AIM_FORWARD;
    }

    /* ── Look up tracking ────────────────────── */
    bool standing_still = body->on_ground && fabsf(body->vel.x) < 10.0f;
    if (hold_up && standing_still && !hold_left && !hold_right) {
        pd->look_up_timer += dt;
        pd->looking_up = (pd->look_up_timer > LOOK_UP_DELAY);
    } else {
        pd->look_up_timer = 0;
        pd->looking_up = false;
    }

    /* ── Dash / Jetpack ─────────────────────── */

    /* Count down jetpack boost timer */
    if (pd->jetpack_boost_timer > 0) {
        pd->jetpack_boost_timer -= dt;
        if (pd->jetpack_boost_timer < 0)
            pd->jetpack_boost_timer = 0;
    }

    /* Recharge jetpack charges over time */
    float recharge_rate = (pd->jetpack_boost_timer > 0)
        ? PLAYER_JETPACK_BOOST_RECHARGE
        : PLAYER_DASH_RECHARGE;
    if (pd->dash_charges < pd->dash_max_charges) {
        pd->dash_recharge_timer -= dt;
        if (pd->dash_recharge_timer <= 0) {
            pd->dash_charges++;
            /* Reset timer for next charge (if still not full) */
            if (pd->dash_charges < pd->dash_max_charges) {
                pd->dash_recharge_timer = recharge_rate;
            } else {
                pd->dash_recharge_timer = 0;
            }
        }
    }

    if (pd->dash_timer > 0) {
        /* Currently dashing */
        pd->dash_timer -= dt;
        body->vel.x = pd->dash_dir.x * PLAYER_DASH_SPEED;
        body->vel.y = pd->dash_dir.y * PLAYER_DASH_SPEED;

        /* Jetpack trail particles every frame */
        Vec2 exhaust_pos = vec2(
            body->pos.x + body->size.x * 0.5f,
            body->pos.y + body->size.y * 0.5f
        );
        particle_emit_jetpack_trail(exhaust_pos, pd->dash_dir);

        /* Blue flame trail when boost is active */
        if (pd->jetpack_boost_timer > 0) {
            particle_emit_jetpack_boost_trail(exhaust_pos, pd->dash_dir);
        }

        /* Skip normal movement during dash */
        physics_update(body, dt, map);
        animation_update(&self->anim, dt);
        return;
    }

    if (input_pressed(ACTION_DASH) && pd->dash_charges > 0) {
        pd->dash_charges--;
        pd->dash_recharge_timer = (pd->jetpack_boost_timer > 0)
            ? PLAYER_JETPACK_BOOST_RECHARGE : PLAYER_DASH_RECHARGE;
        pd->dash_timer = PLAYER_DASH_DURATION;

        /* Determine dash direction from input */
        Vec2 dash = vec2_zero();
        if (hold_left)  dash.x = -1.0f;
        if (hold_right) dash.x =  1.0f;
        if (hold_up)    dash.y = -1.0f;
        if (hold_down && !body->on_ground) dash.y = 1.0f;

        /* Default: dash in facing direction */
        if (dash.x == 0.0f && dash.y == 0.0f) {
            dash.x = (self->flags & ENTITY_FACING_LEFT) ? -1.0f : 1.0f;
        }

        pd->dash_dir = vec2_norm(dash);

        /* Grant brief invincibility during dash */
        pd->inv_timer = PLAYER_DASH_DURATION;
        self->flags |= ENTITY_INVINCIBLE;

        /* Cancel vertical velocity for upward/horizontal dashes */
        if (pd->dash_dir.y <= 0) {
            body->vel.y = 0;
        }

        /* Jetpack burst at dash start */
        Vec2 exhaust_pos = vec2(
            body->pos.x + body->size.x * 0.5f,
            body->pos.y + body->size.y * 0.5f
        );
        particle_emit_jetpack_burst(exhaust_pos, pd->dash_dir);

        /* Blue flame accents when boost powerup is active */
        if (pd->jetpack_boost_timer > 0) {
            particle_emit_jetpack_boost_burst(exhaust_pos, pd->dash_dir);
        }

        audio_play_sound(s_sfx_dash, 96);
        return;
    }

    /* ── Jetpack boost idle glow ─────────────── */
    /* Ambient blue flame from the player's back while boost is active
     * and not dashing. Emits from the rear center of the sprite. */
    if (pd->jetpack_boost_timer > 0) {
        bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
        Vec2 back_pos = vec2(
            facing_left ? body->pos.x + body->size.x - 1.0f
                        : body->pos.x + 1.0f,
            body->pos.y + body->size.y * 0.45f
        );
        particle_emit_jetpack_boost_idle(back_pos, facing_left);
    }

    /* ── Horizontal movement ─────────────────── */
    float target_vx = 0.0f;
    if (hold_left)  target_vx -= PLAYER_SPEED;
    if (hold_right) target_vx += PLAYER_SPEED;

    /* Set facing direction */
    if (target_vx < 0) self->flags |= ENTITY_FACING_LEFT;
    if (target_vx > 0) self->flags &= ~ENTITY_FACING_LEFT;

    /* Acceleration / deceleration */
    float accel = body->on_ground ? PLAYER_ACCEL : PLAYER_AIR_ACCEL;
    if (target_vx != 0.0f) {
        if (body->vel.x < target_vx) {
            body->vel.x += accel * dt;
            if (body->vel.x > target_vx) body->vel.x = target_vx;
        } else if (body->vel.x > target_vx) {
            body->vel.x -= accel * dt;
            if (body->vel.x < target_vx) body->vel.x = target_vx;
        }
    } else {
        float decel = body->on_ground ? PLAYER_DECEL : PLAYER_AIR_ACCEL;
        if (body->vel.x > 0) {
            body->vel.x -= decel * dt;
            if (body->vel.x < 0) body->vel.x = 0;
        } else if (body->vel.x < 0) {
            body->vel.x += decel * dt;
            if (body->vel.x > 0) body->vel.x = 0;
        }
    }

    /* ── Coyote time ─────────────────────────── */
    if (body->on_ground) {
        pd->coyote_timer = PLAYER_COYOTE_TIME;
    } else {
        pd->coyote_timer -= dt;
    }

    /* ── Jump buffer ─────────────────────────── */
    if (input_pressed(ACTION_JUMP)) {
        pd->jump_buffer_timer = PLAYER_JUMP_BUFFER;
    } else {
        pd->jump_buffer_timer -= dt;
    }

    /* ── Jump execution ──────────────────────── */
    if (pd->jump_buffer_timer > 0 && pd->coyote_timer > 0) {
        body->vel.y = -PLAYER_JUMP_FORCE;
        pd->jumping = true;
        pd->jump_buffer_timer = 0;
        pd->coyote_timer = 0;
        audio_play_sound(s_sfx_jump, 96);
    }

    /* Variable jump height: cut velocity on release */
    if (pd->jumping && input_released(ACTION_JUMP) && body->vel.y < 0) {
        body->vel.y *= PLAYER_JUMP_CUT;
        pd->jumping = false;
    }

    if (body->on_ground && body->vel.y >= 0) {
        pd->jumping = false;
    }

    /* ── Shooting ────────────────────────────── */
    pd->shoot_cooldown -= dt;
    if (pd->has_gun && input_pressed(ACTION_SHOOT) && pd->shoot_cooldown <= 0 && s_em) {
        pd->shoot_cooldown = PLAYER_SHOOT_COOLDOWN;

        bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
        float forward = facing_left ? -1.0f : 1.0f;

        Vec2 shoot_dir;
        Vec2 bullet_pos;

        switch (pd->aim_dir) {
        case AIM_UP:
            shoot_dir = vec2(0, -1.0f);
            bullet_pos = vec2(
                body->pos.x + body->size.x * 0.5f - 4.0f,
                body->pos.y - 8.0f
            );
            break;
        case AIM_DIAG_UP:
            shoot_dir = vec2(forward, -1.0f);
            bullet_pos = vec2(
                facing_left ? body->pos.x - 4.0f : body->pos.x + body->size.x - 4.0f,
                body->pos.y - 4.0f
            );
            break;
        case AIM_FORWARD:
        default:
            shoot_dir = vec2(forward, 0);
            bullet_pos = vec2(
                facing_left ? body->pos.x - 8.0f : body->pos.x + body->size.x,
                body->pos.y + body->size.y * 0.15f
            );
            break;
        }

        projectile_spawn_dir(s_em, bullet_pos, shoot_dir, true);
        /* Muzzle flash slightly ahead of bullet origin (at barrel tip) */
        Vec2 flash_pos = vec2(
            bullet_pos.x + shoot_dir.x * 4.0f,
            bullet_pos.y + shoot_dir.y * 4.0f + 3.0f
        );
        particle_emit_muzzle_flash(flash_pos, shoot_dir);
        audio_play_sound(s_sfx_shoot, 80);
    }

    /* ── Physics ─────────────────────────────── */
    physics_update(body, dt, map);

    /* ── Landing detection ───────────────────── */
    if (body->on_ground && !pd->was_on_ground) {
        /* Just landed — emit dust at feet */
        Vec2 feet = vec2(
            body->pos.x + body->size.x * 0.5f,
            body->pos.y + body->size.y
        );
        particle_emit_landing_dust(feet);
    }
    pd->was_on_ground = body->on_ground;

    /* ── Animation ───────────────────────────── */
    if (!body->on_ground) {
        if (body->vel.y < 0) {
            animation_set(&self->anim, &s_anim_jump);
        } else {
            animation_set(&self->anim, &s_anim_fall);
        }
    } else if (fabsf(body->vel.x) > 10.0f) {
        animation_set(&self->anim, &s_anim_run);
    } else {
        animation_set(&self->anim, &s_anim_idle);
    }

    animation_update(&self->anim, dt);
}

/* ── Render ──────────────────────────────────────── */

void player_render(Entity *self, const Camera *cam) {
    if (self->flags & ENTITY_DEAD) {
        /* Render death animation if available */
        if (!self->anim.def) return;
        if (self->anim.finished) return;
    }

    PlayerData *pd = (PlayerData *)self->data;
    Body *body = &self->body;

    /* Flash during invincibility (but not during dash) */
    if (pd && pd->inv_timer > 0 && pd->dash_timer <= 0) {
        int blink = (int)(pd->inv_timer / 0.1f);
        if (blink % 2) return;
    }

    /* Get texture from animation (per-anim texture) or fall back to global */
    SDL_Texture *tex = animation_texture(&self->anim);
    if (!tex) tex = g_spritesheet;

    if (tex && self->anim.def) {
        SDL_Rect src = animation_current_rect(&self->anim);

        /* Determine sprite size for this frame */
        float spr_w = (float)src.w;
        float spr_h = (float)src.h;

        /* Center the sprite on the hitbox, compensating for bottom padding
         * in the astronaut spritesheets (4px transparent below feet) */
        Vec2 render_pos = vec2(
            body->pos.x + body->size.x * 0.5f - spr_w * 0.5f,
            body->pos.y + body->size.y - spr_h + 4.0f
        );

        Sprite spr = {
            .texture = tex,
            .src     = src,
            .pos     = render_pos,
            .size    = vec2(spr_w, spr_h),
            .flip_x  = (self->flags & ENTITY_FACING_LEFT) != 0,
            .flip_y  = false,
            .layer   = LAYER_ENTITIES,
            .alpha   = 255,
            .rotation = 0.0,
        };
        renderer_submit(&spr);

        /* ── Weapon overlay ─────────────────── */
        if (s_weapon_tex && !(self->flags & ENTITY_DEAD) && pd && pd->has_gun) {
            bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;

            /* Anchor gun to the player sprite position (not body pos)
             * so it moves in exact lockstep — no sub-pixel jitter. */
            float anchor_x = (float)(int)(render_pos.x + spr_w * 0.5f);
            float anchor_y = (float)(int)(render_pos.y + spr_h * 0.5f);

            /* Offset from sprite center to grip point */
            float gun_offset_x = 6.0f;  /* pixels in front of center */
            float gun_offset_y = 1.0f;   /* slightly below center    */

            /* Rotation angle based on aim direction */
            double gun_rotation = 0.0;
            switch (pd->aim_dir) {
            case AIM_UP:
                gun_rotation = -90.0;
                gun_offset_x = 0.0f;
                gun_offset_y = -4.0f;
                break;
            case AIM_DIAG_UP:
                gun_rotation = -45.0;
                gun_offset_x = 1.0f;
                gun_offset_y = -3.0f;
                break;
            case AIM_FORWARD:
            default:
                gun_rotation = 0.0;
                break;
            }

            /* Flip adjustments for facing left */
            if (facing_left) {
                gun_offset_x = -gun_offset_x;
                gun_rotation = -gun_rotation;
            }

            /* Position gun relative to anchor, snapped to integer pixels */
            Vec2 gun_pos = vec2(
                (float)(int)(anchor_x + gun_offset_x - s_weapon_render_w * 0.5f),
                (float)(int)(anchor_y + gun_offset_y - s_weapon_render_h * 0.5f)
            );

            Sprite gun = {
                .texture  = s_weapon_tex,
                .src      = s_weapon_src,
                .pos      = gun_pos,
                .size     = vec2(s_weapon_render_w, s_weapon_render_h),
                .flip_x   = facing_left,
                .flip_y   = false,
                .layer    = LAYER_ENTITIES,
                .alpha    = 255,
                .rotation = gun_rotation,
            };
            renderer_submit(&gun);
        }
    } else {
        /* Fallback: colored rectangle */
        SDL_Color color;
        if (body->on_ground) {
            color = (SDL_Color){100, 200, 100, 255};
        } else {
            color = (SDL_Color){100, 150, 255, 255};
        }
        renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
    }
}

/* ── Lifecycle ───────────────────────────────────── */

void player_destroy(Entity *self) {
    free(self->data);
    self->data = NULL;
}

Entity *player_spawn(EntityManager *em, Vec2 pos) {
    /* Ensure astronaut animations are loaded */
    load_astronaut_anims();

    Entity *e = entity_spawn(em, ENT_PLAYER, pos);
    if (!e) return NULL;

    e->body.size = vec2(PLAYER_WIDTH, PLAYER_HEIGHT);
    e->body.gravity_scale = 1.0f;
    e->health = 3;
    e->max_health = 3;
    e->flags |= ENTITY_ALWAYS_UPDATE;

    PlayerData *pd = calloc(1, sizeof(PlayerData));
    pd->has_gun          = true;  /* armed by default; moon level overrides */
    pd->dash_charges     = PLAYER_DASH_MAX_CHARGES;
    pd->dash_max_charges = PLAYER_DASH_MAX_CHARGES;
    pd->respawn_timer    = RESPAWN_DELAY;
    pd->spawn_point      = pos;
    e->data = pd;

    return e;
}

float player_get_look_up_offset(const Entity *self) {
    if (!self || !self->data) return 0.0f;
    const PlayerData *pd = (const PlayerData *)self->data;
    if (!pd->looking_up) return 0.0f;

    /* Smoothly ramp up the offset */
    float t = (pd->look_up_timer - LOOK_UP_DELAY) * LOOK_UP_SPEED;
    if (t > 1.0f) t = 1.0f;
    return -LOOK_UP_OFFSET * t;
}

bool player_get_dash_charges(const Entity *self, int *charges, int *max_charges,
                             float *recharge_pct, bool *boosted) {
    if (!self || !self->data || self->type != ENT_PLAYER) return false;
    const PlayerData *pd = (const PlayerData *)self->data;
    if (charges)      *charges      = pd->dash_charges;
    if (max_charges)  *max_charges  = pd->dash_max_charges;
    if (boosted)      *boosted      = pd->jetpack_boost_timer > 0;
    if (recharge_pct) {
        float rate = (pd->jetpack_boost_timer > 0)
            ? PLAYER_JETPACK_BOOST_RECHARGE : PLAYER_DASH_RECHARGE;
        if (pd->dash_charges >= pd->dash_max_charges) {
            *recharge_pct = 1.0f;
        } else {
            *recharge_pct = 1.0f - (pd->dash_recharge_timer / rate);
        }
    }
    return true;
}

void player_give_gun(Entity *self) {
    if (!self || !self->data || self->type != ENT_PLAYER) return;
    PlayerData *pd = (PlayerData *)self->data;
    pd->has_gun = true;
}

bool player_has_gun(const Entity *self) {
    if (!self || !self->data || self->type != ENT_PLAYER) return false;
    const PlayerData *pd = (const PlayerData *)self->data;
    return pd->has_gun;
}

bool player_wants_respawn(const Entity *self) {
    if (!self || !self->data || self->type != ENT_PLAYER) return false;
    if (!(self->flags & ENTITY_DEAD)) return false;
    const PlayerData *pd = (const PlayerData *)self->data;
    return pd->respawn_timer <= 0;
}

void player_respawn(Entity *self, Vec2 pos) {
    if (!self || !self->data) return;
    PlayerData *pd = (PlayerData *)self->data;

    /* Reset entity state */
    self->health     = self->max_health;
    self->flags      = 0;       /* clear DEAD, INVINCIBLE, etc. */
    self->body.pos   = pos;
    self->body.vel   = vec2_zero();

    /* Grant brief invincibility on respawn */
    pd->inv_timer    = PLAYER_INV_TIME;
    self->flags     |= ENTITY_INVINCIBLE;

    /* Reset player-specific state (preserve has_gun across respawn) */
    pd->coyote_timer       = 0;
    pd->jump_buffer_timer  = 0;
    pd->jumping            = false;
    pd->was_on_ground      = false;
    pd->shoot_cooldown     = 0;
    pd->dash_timer         = 0;
    pd->dash_charges       = pd->dash_max_charges;
    pd->dash_recharge_timer = 0;
    pd->jetpack_boost_timer = 0;
    pd->aim_dir            = AIM_FORWARD;
    pd->looking_up         = false;
    pd->look_up_timer      = 0;
    pd->respawn_timer      = RESPAWN_DELAY;

    /* Reset animation */
    animation_set(&self->anim, &s_anim_idle);
}
