from __future__ import annotations

"""Player damage intake helpers.

This is a minimal, rewrite-focused port of `player_take_damage` (0x00425e50).
See: `docs/crimsonland-exe/player-damage.md`.
"""

from collections.abc import Callable, Sequence

from grim.sfx_map import SfxId

from .math_parity import f32
from .perks import PerkId
from .perks.helpers import perk_active
from .rng_caller_static import RngCallerStatic
from .sim.state_types import GameplayState, PlayerState

__all__ = ["player_take_damage", "player_take_projectile_damage"]
_PLAYER_PAIN_SFX: tuple[SfxId, ...] = (
    SfxId.TROOPER_INPAIN_01,
    SfxId.TROOPER_INPAIN_02,
    SfxId.TROOPER_INPAIN_03,
)
_PLAYER_DEATH_SFX: tuple[SfxId, ...] = (SfxId.TROOPER_DIE_01, SfxId.TROOPER_DIE_02)
_THICK_SKINNED_DAMAGE_SCALE_F32 = 0.6660000085830688


def player_take_damage(
    state: GameplayState,
    player: PlayerState,
    damage: float,
    *,
    dt: float | None = None,
    players: Sequence[PlayerState] | None = None,
    on_lethal: Callable[[], None] | None = None,
) -> float:
    """Apply damage to a player, returning the actual damage applied."""

    raw_damage = float(damage)
    if raw_damage <= 0.0:
        return 0.0
    if state.debug_god_mode:
        return 0.0

    if perk_active(player, PerkId.DEATH_CLOCK):
        return 0.0

    damage_scaled = float(raw_damage)
    if perk_active(player, PerkId.TOUGH_RELOADER) and player.weapon.reload_active:
        damage_scaled *= 0.5
    spread_heat_damage = float(damage_scaled)

    state.survival_reward_damage_seen = True

    if float(player.shield_timer) > 0.0:
        return 0.0

    was_alive_source = player
    # Native bug: the pre-hit alive flag is read from player 1 health even when
    # damaging player 2; preserve this under `--preserve-bugs`.
    if state.preserve_bugs and players:
        was_alive_source = players[0]
    was_alive = float(was_alive_source.health) > 0.0

    if perk_active(player, PerkId.THICK_SKINNED):
        # Native uses an f32 constant (`~0.666`) here, not exact 2/3.
        damage_scaled = float(f32(float(damage_scaled) * float(_THICK_SKINNED_DAMAGE_SCALE_F32)))

    dodged = False
    if perk_active(player, PerkId.NINJA):
        dodged = (state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_NINJA) % 3) == 0
    elif perk_active(player, PerkId.DODGER):
        dodged = (state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_DODGER) % 5) == 0

    health_before = float(player.health)
    if not dodged:
        if perk_active(player, PerkId.HIGHLANDER):
            if (state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_HIGHLANDER) % 10) == 0:
                player.health = 0.0
        else:
            player.health = float(f32(float(player.health) - float(damage_scaled)))

    # Native routes exact-zero Highlander kills through the pain branch; default
    # rewrite mode treats `health == 0` as lethal here.
    lethal_hit = float(player.health) < 0.0
    if not state.preserve_bugs and float(player.health) == 0.0:
        lethal_hit = True
    if not dodged and lethal_hit and dt is not None and float(dt) > 0.0:
        player.death_timer -= float(dt) * 28.0

    # Native emits pain/death VO before heading jitter + low-health timer RNG work.
    if not lethal_hit:
        state.sfx_queue.append(
            _PLAYER_PAIN_SFX[state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_PAIN_SFX) % len(_PLAYER_PAIN_SFX)],
        )
        if not was_alive:
            return max(0.0, health_before - float(player.health))
    else:
        if not was_alive:
            return max(0.0, health_before - float(player.health))
        if not perk_active(player, PerkId.FINAL_REVENGE):
            state.sfx_queue.append(_PLAYER_DEATH_SFX[state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_DEATH_SFX) & 1])
        elif on_lethal is not None:
            on_lethal()
            state.player_death_hook_skip_indices.add(int(player.index))

    if not dodged:
        if not perk_active(player, PerkId.UNSTOPPABLE):
            player.heading += float((state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_HEADING) % 100) - 50) * 0.04
            # Native uses post-Tough-Reloader damage (before Thick Skinned) for spread heat growth.
            player.spread_heat = min(0.48, float(player.spread_heat) + spread_heat_damage * 0.01)

        if player.health <= 20.0 and (state.rng.rand(caller=RngCallerStatic.PLAYER_TAKE_DAMAGE_LOW_HEALTH) & 7) == 3:
            player.low_health_timer = 0.0

    return max(0.0, health_before - float(player.health))


def player_take_projectile_damage(state: GameplayState, player: PlayerState, damage: float) -> float:
    """Apply projectile damage to a player (modeled after `projectile_update` player-hit logic).

    Native `projectile_update` does not call `player_take_damage` for projectile hits: it sets
    `projectile.life_timer = 0.25` and subtracts a fixed amount (usually 10.0) if shield is down.
    """

    dmg = float(damage)
    if dmg <= 0.0:
        return 0.0
    if state.debug_god_mode:
        return 0.0
    if float(player.shield_timer) > 0.0:
        return 0.0

    player.health -= dmg
    return dmg
