from __future__ import annotations

import math
from collections.abc import Callable
from pathlib import Path

import msgspec

from grim.assets import TextureLoader
from grim.config import CrimsonConfig
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
from grim.geom import Rect, Vec2
from grim.raylib_api import rl

from ..game_modes import GameMode
from ..persistence.highscores import (
    NAME_MAX_EDIT,
    TABLE_MAX,
    HighScoreRecord,
    rank_index,
    read_highscore_table,
    scores_path_for_config,
    upsert_highscore_record,
)
from ..weapons import WEAPON_BY_ID, WeaponId, weapon_display_name
from .cursor import draw_menu_cursor
from .formatting import format_ordinal, format_time_mm_ss
from .hud import HudAssets
from .layout import menu_widescreen_y_shift, ui_scale
from .menu_panel import draw_classic_menu_panel
from .perk_menu import (
    PerkMenuAssets,
    UiButtonState,
    button_draw,
    button_update,
    button_width,
    draw_ui_text,
    load_perk_menu_assets,
)
from .text_input import flush_text_input_events, gameplay_controls_held, poll_text_input

GAME_OVER_PANEL_X = -45.0
# `ui_menu_layout_init` sets game-over panel pos to (-45, 110):
#   _DAT_0048cc60 = 0xc2340000 (-45.0)
#   _DAT_0048cc64 = 0x42dc0000 (110.0)
GAME_OVER_PANEL_Y = 110.0
# `DAT_0048cc48` is cloned from the 3-slice menu panel layout (`ui_menu_item_element._pad4+0xac`)
# in `ui_menu_layout_init`; trace confirms a 510x378 bbox for both phase 0 and phase 1.
GAME_OVER_PANEL_W = 510.0
GAME_OVER_PANEL_H = 378.0

# Measured from ui_render_trace at 1024x768 (stable timeline):
# panel top-left is (pos_x + 21, pos_y - 81) and size is 510x254, plus a shadow pass at +7,+7.
GAME_OVER_PANEL_OFFSET_X = 21.0
GAME_OVER_PANEL_OFFSET_Y = -81.0

TEXTURE_TOP_BANNER_W = 256.0
TEXTURE_TOP_BANNER_H = 64.0

# `game_over_screen_update` (0x0040ffc0) computes banner/content X from:
#   local_10 = quad0_x0 + pos_x + 180.0
#   local_18 = offset_x + local_10 + 44.0 - 10.0
# so banner/content anchor is +214 from the panel-left edge in steady state.
GAME_OVER_BANNER_X_OFFSET = 214.0

INPUT_BOX_W = 166.0  # `_DAT_0048259c = 0xa6` before `ui_text_input_update`
INPUT_BOX_H = 18.0

PANEL_SLIDE_DURATION_MS = 250.0

COLOR_TEXT = rl.Color(255, 255, 255, 255)
COLOR_TEXT_MUTED = rl.Color(255, 255, 255, int(255 * 0.8))
COLOR_SCORE_LABEL = rl.Color(230, 230, 230, 255)
COLOR_SCORE_VALUE = rl.Color(230, 230, 255, 255)


def _weapon_icon_src(texture: rl.Texture, weapon_id_native: int) -> rl.Rectangle | None:
    weapon_id = int(weapon_id_native)
    icon_index = WEAPON_BY_ID[WeaponId(weapon_id)].icon_index
    if icon_index < 0 or icon_index > 31:
        return None
    grid = 8
    cell_w = float(texture.width) / grid
    cell_h = float(texture.height) / grid
    frame = int(icon_index) * 2
    col = frame % grid
    row = frame // grid
    return rl.Rectangle(float(col * cell_w), float(row * cell_h), float(cell_w * 2), float(cell_h))


class GameOverAssets(msgspec.Struct):
    menu_panel: rl.Texture | None
    text_reaper: rl.Texture | None
    text_well_done: rl.Texture | None
    particles: rl.Texture | None
    perk_menu_assets: PerkMenuAssets


class _GameOverPanelLayout(msgspec.Struct, frozen=True):
    panel: Rect
    top_left: Vec2


def load_game_over_assets(assets_root: Path) -> GameOverAssets:
    perk_menu_assets = load_perk_menu_assets(assets_root)
    loader = TextureLoader.from_assets_root(assets_root)
    menu_panel = loader.get(name="ui_menuPanel", paq_rel="ui/ui_menuPanel.jaz")
    text_reaper = loader.get(name="ui_textReaper", paq_rel="ui/ui_textReaper.jaz")
    text_well_done = loader.get(
        name="ui_textWellDone",
        paq_rel="ui/ui_textWellDone.jaz",
    )
    particles = loader.get(name="particles", paq_rel="game/particles.jaz")
    return GameOverAssets(
        menu_panel=menu_panel,
        text_reaper=text_reaper,
        text_well_done=text_well_done,
        particles=particles,
        perk_menu_assets=perk_menu_assets,
    )


def _draw_texture_centered(tex: rl.Texture, pos: Vec2, w: float, h: float, alpha: float) -> None:
    src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
    dst = rl.Rectangle(pos.x, pos.y, float(w), float(h))
    tint = rl.Color(255, 255, 255, int(255 * max(0.0, min(1.0, alpha))))
    rl.draw_texture_pro(tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)


def _ease_out_cubic(t: float) -> float:
    t = max(0.0, min(1.0, float(t)))
    return 1.0 - (1.0 - t) ** 3


class GameOverUi(msgspec.Struct):
    assets_root: Path
    base_dir: Path

    config: CrimsonConfig
    preserve_bugs: bool = False

    assets: GameOverAssets | None = None
    font: SmallFontData | None = None

    input_text: str = ""
    input_caret: int = 0
    phase: int = -1  # -1 init, 0 name entry (if qualifies), 1 results/buttons
    rank: int = TABLE_MAX
    _candidate_record: HighScoreRecord | None = None
    _saved: bool = False
    _dt: float = 0.0

    _hover_weapon: float = 0.0
    _hover_time: float = 0.0
    _hover_hit_ratio: float = 0.0
    _intro_ms: float = 0.0
    _cursor_pulse_time: float = 0.0
    _panel_open_sfx_played: bool = False
    _closing: bool = False
    _close_action: str | None = None

    # Buttons (rendered via existing ui_button implementation)
    _ok_button: UiButtonState = msgspec.field(default_factory=lambda: UiButtonState("OK", force_wide=False))
    _play_again_button: UiButtonState = msgspec.field(default_factory=lambda: UiButtonState("Play Again", force_wide=True))
    _high_scores_button: UiButtonState = msgspec.field(default_factory=lambda: UiButtonState("High scores", force_wide=True))
    _main_menu_button: UiButtonState = msgspec.field(default_factory=lambda: UiButtonState("Main Menu", force_wide=True))

    _consume_enter: bool = False
    _defer_name_input_until_controls_released: bool = False

    def open(self) -> None:
        self.close()
        self.font = load_small_font(self.assets_root)
        self.assets = load_game_over_assets(self.assets_root)
        self.phase = -1
        self.rank = TABLE_MAX
        self._candidate_record = None
        self._saved = False
        self._dt = 0.0
        self._hover_weapon = 0.0
        self._hover_time = 0.0
        self._hover_hit_ratio = 0.0
        self._intro_ms = 0.0
        self._cursor_pulse_time = 0.0
        self._panel_open_sfx_played = False
        self._closing = False
        self._close_action = None
        self.input_text = ""
        self.input_caret = 0
        self._consume_enter = True
        self._defer_name_input_until_controls_released = False

    def close(self) -> None:
        if self.assets is not None:
            self.assets = None
        if self.font is not None:
            rl.unload_texture(self.font.texture)
            self.font = None

    def consume_enter(self) -> bool:
        if self._consume_enter:
            self._consume_enter = False
            return True
        return False

    def world_entity_alpha(self) -> float:
        if not self._closing:
            return 1.0
        if PANEL_SLIDE_DURATION_MS <= 1e-6:
            return 0.0
        alpha = float(self._intro_ms) / float(PANEL_SLIDE_DURATION_MS)
        if alpha < 0.0:
            return 0.0
        if alpha > 1.0:
            return 1.0
        return alpha

    def _text_width(self, text: str, scale: float) -> float:
        if self.font is None:
            return float(rl.measure_text(text, int(20 * scale)))
        return float(measure_small_text_width(self.font, text, scale))

    def _draw_small(self, text: str, pos: Vec2, scale: float, color: rl.Color) -> None:
        if self.font is not None:
            draw_small_text(self.font, text, pos, scale, color)
        else:
            rl.draw_text(text, int(pos.x), int(pos.y), int(20 * scale), color)

    def _panel_layout(self, *, screen_w: float, scale: float) -> _GameOverPanelLayout:
        # Keep consistent with the main menu panel offsets.
        t = self._intro_ms / PANEL_SLIDE_DURATION_MS if PANEL_SLIDE_DURATION_MS > 1e-6 else 1.0
        eased = _ease_out_cubic(t)
        panel_slide_x = -GAME_OVER_PANEL_W * (1.0 - eased)

        panel_pos = Vec2((GAME_OVER_PANEL_X + panel_slide_x) * scale, 0.0)
        layout_w = screen_w / scale if scale else screen_w
        widescreen_shift_y = menu_widescreen_y_shift(layout_w)
        panel_pos = Vec2(panel_pos.x, (GAME_OVER_PANEL_Y + widescreen_shift_y) * scale)
        panel_origin = Vec2(-(GAME_OVER_PANEL_OFFSET_X * scale), -(GAME_OVER_PANEL_OFFSET_Y * scale))
        top_left = panel_pos - panel_origin
        panel = Rect.from_top_left(top_left, GAME_OVER_PANEL_W * scale, GAME_OVER_PANEL_H * scale)
        return _GameOverPanelLayout(panel=panel, top_left=top_left)

    def _begin_close_transition(self, action: str) -> None:
        if self._closing:
            return
        self._closing = True
        self._close_action = action

    def update(
        self,
        dt: float,
        *,
        record: HighScoreRecord,
        player_name_default: str,
        play_sfx: Callable[[str], None] | None = None,
        rand: Callable[[], int] | None = None,
        mouse: rl.Vector2 | None = None,
    ) -> str | None:
        self._dt = float(min(dt, 0.1))
        dt_ms = self._dt * 1000.0
        self._cursor_pulse_time += self._dt * 1.1
        if mouse is None:
            mouse = rl.get_mouse_position()
        if rand is None:

            def rand() -> int:
                return 0

        if self.assets is None:
            return None

        if self._closing:
            self._intro_ms = max(0.0, float(self._intro_ms) - dt_ms)
            if self._intro_ms <= 1e-3 and self._close_action is not None:
                action = self._close_action
                self._close_action = None
                self._closing = False
                return action
            return None

        self._intro_ms = min(PANEL_SLIDE_DURATION_MS, self._intro_ms + dt_ms)
        if (
            (not self._panel_open_sfx_played)
            and play_sfx is not None
            and self._intro_ms >= PANEL_SLIDE_DURATION_MS - 1e-3
        ):
            play_sfx("sfx_ui_panelclick")
            self._panel_open_sfx_played = True
        if self._consume_enter:
            self._consume_enter = False
            rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
        if self.phase == -1:
            # If in the top 100, prompt for a name. Otherwise show score-too-low message and buttons.
            try:
                game_mode_id = GameMode(self.config.game_mode)
            except ValueError:
                game_mode_id = GameMode.DEMO
            candidate = record.copy()
            candidate.game_mode_id = game_mode_id
            self._candidate_record = candidate

            path = scores_path_for_config(self.base_dir, self.config)
            records = read_highscore_table(path, game_mode_id=game_mode_id)
            idx = rank_index(records, candidate)
            self.rank = int(idx)
            flush_text_input_events()
            # Match native `grim_was_key_pressed(ENTER)` after the input flush.
            rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
            rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)
            if idx < TABLE_MAX:
                self.phase = 0
                self.input_text = player_name_default[:NAME_MAX_EDIT]
                self.input_caret = len(self.input_text)
                self._defer_name_input_until_controls_released = True
                return None
            else:
                self.phase = 1

        # Basic text input behavior for the name-entry phase.
        if self.phase == 0:
            if self._defer_name_input_until_controls_released:
                flush_text_input_events()
                rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
                rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)
                if not gameplay_controls_held(self.config):
                    self._defer_name_input_until_controls_released = False
                return None
            click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
            typed = poll_text_input(NAME_MAX_EDIT - len(self.input_text), allow_space=True)
            if typed:
                self.input_text = (self.input_text[: self.input_caret] + typed + self.input_text[self.input_caret :])[
                    :NAME_MAX_EDIT
                ]
                self.input_caret = min(len(self.input_text), self.input_caret + len(typed))
                if play_sfx is not None:
                    play_sfx("sfx_ui_typeclick_01" if (int(rand()) & 1) == 0 else "sfx_ui_typeclick_02")
            if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
                if self.input_caret > 0:
                    self.input_text = self.input_text[: self.input_caret - 1] + self.input_text[self.input_caret :]
                    self.input_caret -= 1
                    if play_sfx is not None:
                        play_sfx("sfx_ui_typeclick_01" if (int(rand()) & 1) == 0 else "sfx_ui_typeclick_02")
            if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
                self.input_caret = max(0, self.input_caret - 1)
            if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
                self.input_caret = min(len(self.input_text), self.input_caret + 1)
            if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
                self.input_caret = 0
            if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
                self.input_caret = len(self.input_text)

            screen_w = float(rl.get_screen_width())
            screen_h = float(rl.get_screen_height())
            scale = ui_scale(screen_w, screen_h)
            panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
            banner_pos = panel_layout.top_left + Vec2(GAME_OVER_BANNER_X_OFFSET * scale, 40.0 * scale)
            form_pos = banner_pos + Vec2(8.0 * scale, 84.0 * scale)
            ok_pos = form_pos + Vec2(170.0 * scale, 32.0 * scale)
            ok_w = button_width(self.font, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
            ok_clicked = button_update(self._ok_button, pos=ok_pos, width=ok_w, dt_ms=dt_ms, mouse=mouse, click=click)

            if ok_clicked or rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
                if self.input_text.strip():
                    if play_sfx is not None:
                        play_sfx("sfx_ui_typeenter")
                    candidate = (self._candidate_record or record).copy()
                    candidate.set_name(self.input_text)
                    self.config.set_player_name(self.input_text)
                    self.config.save()
                    path = scores_path_for_config(self.base_dir, self.config)
                    if not self._saved:
                        upsert_highscore_record(path, candidate)
                        self._saved = True
                    self.phase = 1
                    return None
                if play_sfx is not None:
                    play_sfx("sfx_shock_hit_01")
        else:
            # Buttons phase: let the caller handle navigation; we just report actions.
            click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
            screen_w = float(rl.get_screen_width())
            screen_h = float(rl.get_screen_height())
            scale = ui_scale(screen_w, screen_h)
            panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
            banner_pos = panel_layout.top_left + Vec2(GAME_OVER_BANNER_X_OFFSET * scale, 40.0 * scale)
            button_pos = banner_pos + Vec2(52.0 * scale, (210.0 if self.rank < TABLE_MAX else 208.0) * scale)

            play_again_w = button_width(
                self.font, self._play_again_button.label, scale=scale, force_wide=self._play_again_button.force_wide,
            )
            if button_update(
                self._play_again_button,
                pos=button_pos,
                width=play_again_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx("sfx_ui_buttonclick")
                self._begin_close_transition("play_again")
                return None
            button_pos = button_pos.offset(dy=32.0 * scale)

            high_scores_w = button_width(
                self.font, self._high_scores_button.label, scale=scale, force_wide=self._high_scores_button.force_wide,
            )
            if button_update(
                self._high_scores_button,
                pos=button_pos,
                width=high_scores_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx("sfx_ui_buttonclick")
                self._begin_close_transition("high_scores")
                return None
            button_pos = button_pos.offset(dy=32.0 * scale)

            main_menu_w = button_width(
                self.font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide,
            )
            if button_update(
                self._main_menu_button,
                pos=button_pos,
                width=main_menu_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                if play_sfx is not None:
                    play_sfx("sfx_ui_buttonclick")
                self._begin_close_transition("main_menu")
                return None
        return None

    def _draw_score_card(
        self,
        *,
        pos: Vec2,
        record: HighScoreRecord,
        hud_assets: HudAssets | None,
        alpha: float,
        show_weapon_row: bool,
        scale: float,
        mouse: rl.Vector2,
    ) -> None:
        dt_hover = float(self._dt) * 2.0
        label_color = rl.Color(COLOR_SCORE_LABEL.r, COLOR_SCORE_LABEL.g, COLOR_SCORE_LABEL.b, int(255 * alpha * 0.8))
        value_color = rl.Color(COLOR_SCORE_VALUE.r, COLOR_SCORE_VALUE.g, COLOR_SCORE_VALUE.b, int(255 * alpha))
        hint_color = rl.Color(COLOR_SCORE_LABEL.r, COLOR_SCORE_LABEL.g, COLOR_SCORE_LABEL.b, int(255 * alpha * 0.7))

        card_origin = pos.offset(dx=4.0 * scale)
        mode_raw = int(record.game_mode_id)
        try:
            mode_id = GameMode(mode_raw)
        except ValueError:
            mode_id = GameMode.DEMO

        # Left column: Score + value + Rank.
        score_label = "Score"
        score_label_w = self._text_width(score_label, 1.0 * scale)
        self._draw_small(
            score_label,
            card_origin.offset(dx=32.0 * scale - score_label_w * 0.5),
            1.0 * scale,
            label_color,
        )

        match mode_id:
            case GameMode.RUSH | GameMode.QUESTS:
                seconds = float(int(record.survival_elapsed_ms)) * 0.001
                score_value = f"{seconds:.2f} secs"
            case _:
                score_value = f"{int(record.score_xp)}"
        score_value_w = self._text_width(score_value, 1.0 * scale)
        self._draw_small(
            score_value,
            card_origin + Vec2(32.0 * scale - score_value_w * 0.5, 15.0 * scale),
            1.0 * scale,
            value_color,
        )

        rank_value = format_ordinal(int(self.rank) + 1)
        rank_text = f"Rank: {rank_value}"
        rank_w = self._text_width(rank_text, 1.0 * scale)
        self._draw_small(
            rank_text,
            card_origin + Vec2(32.0 * scale - rank_w * 0.5, 30.0 * scale),
            1.0 * scale,
            label_color,
        )

        # Separator between columns (mirrors FUN_00441220 + offset adjustments).
        separator_x = card_origin.x + 80.0 * scale
        rl.draw_line(
            int(separator_x),
            int(card_origin.y),
            int(separator_x),
            int(card_origin.y + 48.0 * scale),
            label_color,
        )

        # Right column: Game time + gauge, or Experience in quest mode.
        col2_pos = card_origin.offset(dx=96.0 * scale)
        match mode_id:
            case GameMode.QUESTS:
                self._draw_small("Experience", col2_pos, 1.0 * scale, label_color)
                xp_value = f"{int(record.score_xp)}"
                xp_w = self._text_width(xp_value, 1.0 * scale)
                self._draw_small(
                    xp_value,
                    col2_pos + Vec2(32.0 * scale - xp_w * 0.5, 15.0 * scale),
                    1.0 * scale,
                    label_color,
                )
                self._hover_time = max(0.0, float(self._hover_time) - dt_hover)
            case _:
                self._draw_small("Game time", col2_pos.offset(dx=6.0 * scale), 1.0 * scale, label_color)
                time_rect_pos = col2_pos + Vec2(8.0 * scale, 16.0 * scale)
                time_rect = Rect.from_top_left(time_rect_pos, 64.0 * scale, 29.0 * scale)
                hovering_time = time_rect.contains(mouse)
                self._hover_time = float(max(0.0, min(1.0, self._hover_time + (dt_hover if hovering_time else -dt_hover))))

                elapsed_ms = int(record.survival_elapsed_ms)
                if hud_assets is not None and hud_assets.clock_table is not None:
                    src = rl.Rectangle(0.0, 0.0, float(hud_assets.clock_table.width), float(hud_assets.clock_table.height))
                    clock_table_pos = col2_pos + Vec2(8.0 * scale, 14.0 * scale)
                    dst = rl.Rectangle(clock_table_pos.x, clock_table_pos.y, 32.0 * scale, 32.0 * scale)
                    rl.draw_texture_pro(
                        hud_assets.clock_table,
                        src,
                        dst,
                        rl.Vector2(0.0, 0.0),
                        0.0,
                        rl.Color(255, 255, 255, int(255 * alpha)),
                    )
                if hud_assets is not None and hud_assets.clock_pointer is not None:
                    src = rl.Rectangle(
                        0.0, 0.0, float(hud_assets.clock_pointer.width), float(hud_assets.clock_pointer.height),
                    )
                    # NOTE: Raylib's draw_texture_pro uses dst.x/y as the rotation origin position;
                    # offset by half-size so the 32x32 quad stays aligned with the table.
                    clock_pointer_pos = col2_pos + Vec2(24.0 * scale, 30.0 * scale)
                    dst = rl.Rectangle(clock_pointer_pos.x, clock_pointer_pos.y, 32.0 * scale, 32.0 * scale)
                    seconds = max(0, elapsed_ms // 1000)
                    rotation = float(seconds) * 6.0
                    origin = rl.Vector2(16.0 * scale, 16.0 * scale)
                    rl.draw_texture_pro(
                        hud_assets.clock_pointer, src, dst, origin, rotation, rl.Color(255, 255, 255, int(255 * alpha)),
                    )

                time_text = format_time_mm_ss(elapsed_ms)
                self._draw_small(time_text, col2_pos + Vec2(40.0 * scale, 19.0 * scale), 1.0 * scale, label_color)

        # Second row: weapon icon + frags + hit ratio (suppressed while entering the name).
        row_pos = card_origin.offset(dy=52.0 * scale)
        self._hover_weapon = float(max(0.0, min(1.0, self._hover_weapon)))
        self._hover_hit_ratio = float(max(0.0, min(1.0, self._hover_hit_ratio)))
        if show_weapon_row and hud_assets is not None and hud_assets.wicons is not None:
            weapon_pos = row_pos
            weapon_rect = Rect.from_top_left(weapon_pos, 64.0 * scale, 32.0 * scale)
            hovering_weapon = weapon_rect.contains(mouse)
            self._hover_weapon = float(
                max(0.0, min(1.0, self._hover_weapon + (dt_hover if hovering_weapon else -dt_hover))),
            )

            src = _weapon_icon_src(hud_assets.wicons, record.most_used_weapon_id)
            if src is not None:
                dst = rl.Rectangle(weapon_pos.x, weapon_pos.y, 64.0 * scale, 32.0 * scale)
                rl.draw_texture_pro(
                    hud_assets.wicons, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.Color(255, 255, 255, int(255 * alpha)),
                )

            weapon_id = record.most_used_weapon_id
            weapon_name = weapon_display_name(weapon_id, preserve_bugs=bool(self.preserve_bugs))
            name_w = self._text_width(weapon_name, 1.0 * scale)
            name_pos = Vec2(card_origin.x + max(0.0, (32.0 * scale - name_w * 0.5)), row_pos.y + 32.0 * scale)
            self._draw_small(weapon_name, name_pos, 1.0 * scale, hint_color)

            frags_text = f"Frags: {int(record.creature_kill_count)}"
            stats_pos = row_pos.offset(dx=110.0 * scale)
            self._draw_small(frags_text, stats_pos.offset(dy=1.0 * scale), 1.0 * scale, label_color)

            fired = max(0, int(record.shots_fired))
            hit = max(0, int(record.shots_hit))
            ratio = int((hit * 100) / fired) if fired > 0 else 0
            hit_text = f"Hit %: {ratio}%"
            self._draw_small(hit_text, stats_pos.offset(dy=15.0 * scale), 1.0 * scale, label_color)

            hit_rect_pos = stats_pos.offset(dy=15.0 * scale)
            hit_rect = Rect.from_top_left(hit_rect_pos, 64.0 * scale, 17.0 * scale)
            hovering_hit = hit_rect.contains(mouse)
            self._hover_hit_ratio = float(
                max(0.0, min(1.0, self._hover_hit_ratio + (dt_hover if hovering_hit else -dt_hover))),
            )
            tooltip_pos = row_pos.offset(dy=48.0 * scale)
        else:
            self._hover_weapon = max(0.0, float(self._hover_weapon) - dt_hover)
            self._hover_hit_ratio = 0.0
            tooltip_pos = row_pos

        self._hover_weapon = float(max(0.0, min(1.0, self._hover_weapon)))
        self._hover_time = float(max(0.0, min(1.0, self._hover_time)))
        self._hover_hit_ratio = float(max(0.0, min(1.0, self._hover_hit_ratio)))

        if self._hover_weapon > 0.5:
            t = (self._hover_weapon - 0.5) * 2.0
            col = rl.Color(label_color.r, label_color.g, label_color.b, int(255 * alpha * t))
            self._draw_small(
                "Most used weapon during the game",
                tooltip_pos.offset(dx=-20.0 * scale),
                1.0 * scale,
                col,
            )
        if self._hover_time > 0.5:
            t = (self._hover_time - 0.5) * 2.0
            col = rl.Color(label_color.r, label_color.g, label_color.b, int(255 * alpha * t))
            self._draw_small(
                "The time the game lasted",
                tooltip_pos.offset(dx=12.0 * scale),
                1.0 * scale,
                col,
            )
        if self._hover_hit_ratio > 0.5:
            t = (self._hover_hit_ratio - 0.5) * 2.0
            col = rl.Color(label_color.r, label_color.g, label_color.b, int(255 * alpha * t))
            hit_ratio_tooltip = (
                "The % of shot bullets hit the target"
                if bool(self.preserve_bugs)
                else "The % of bullets that hit the target"
            )
            self._draw_small(
                hit_ratio_tooltip,
                tooltip_pos.offset(dx=-22.0 * scale),
                1.0 * scale,
                col,
            )

    def draw(
        self,
        *,
        record: HighScoreRecord,
        banner_kind: str,
        hud_assets: HudAssets | None,
        mouse: rl.Vector2 | None = None,
    ) -> None:
        if self.assets is None:
            return
        if mouse is None:
            mouse = rl.get_mouse_position()

        screen_w = float(rl.get_screen_width())
        screen_h = float(rl.get_screen_height())
        scale = ui_scale(screen_w, screen_h)

        panel_layout = self._panel_layout(screen_w=screen_w, scale=scale)
        panel = panel_layout.panel
        panel_top_left = panel_layout.top_left

        # Panel background
        if self.assets.menu_panel is not None:
            fx_detail = self.config.fx_detail(level=0, default=False)
            draw_classic_menu_panel(
                self.assets.menu_panel,
                dst=panel.to_rl(),
                tint=rl.WHITE,
                shadow=fx_detail,
            )

        # Banner (Reaper / Well done)
        banner_pos = panel_top_left + Vec2(GAME_OVER_BANNER_X_OFFSET * scale, 40.0 * scale)
        banner = self.assets.text_reaper if banner_kind == "reaper" else self.assets.text_well_done
        if banner is not None:
            _draw_texture_centered(
                banner,
                banner_pos,
                TEXTURE_TOP_BANNER_W * scale,
                TEXTURE_TOP_BANNER_H * scale,
                1.0,
            )

        if self.phase == 0:
            form_pos = banner_pos + Vec2(8.0 * scale, 84.0 * scale)
            self._draw_small("State your name, trooper!", form_pos.offset(dx=42.0 * scale), 1.0 * scale, COLOR_TEXT)

            input_pos = form_pos.offset(dy=40.0 * scale)
            rl.draw_rectangle_lines(
                int(input_pos.x), int(input_pos.y), int(INPUT_BOX_W * scale), int(INPUT_BOX_H * scale), rl.WHITE,
            )
            rl.draw_rectangle(
                int(input_pos.x + 1.0 * scale),
                int(input_pos.y + 1.0 * scale),
                int((INPUT_BOX_W - 2.0) * scale),
                int((INPUT_BOX_H - 2.0) * scale),
                rl.Color(0, 0, 0, 255),
            )
            draw_ui_text(
                self.font,
                self.input_text,
                input_pos + Vec2(4.0 * scale, 2.0 * scale),
                scale=1.0 * scale,
                color=COLOR_TEXT_MUTED,
            )
            caret_alpha = 1.0
            if math.sin(float(rl.get_time()) * 4.0) > 0.0:
                caret_alpha = 0.4
            caret_color = rl.Color(255, 255, 255, int(255 * caret_alpha))
            caret_x = input_pos.x + 4.0 * scale + self._text_width(self.input_text[: self.input_caret], 1.0 * scale)
            rl.draw_rectangle(
                int(caret_x), int(input_pos.y + 2.0 * scale), int(1.0 * scale), int(14.0 * scale), caret_color,
            )

            ok_pos = form_pos + Vec2(170.0 * scale, 32.0 * scale)
            ok_w = button_width(self.font, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
            button_draw(
                self.assets.perk_menu_assets,
                self.font,
                self._ok_button,
                pos=ok_pos,
                width=ok_w,
                scale=scale,
            )

            score_pos = form_pos + Vec2(16.0 * scale, 116.0 * scale)
            self._draw_score_card(
                pos=score_pos,
                record=record,
                hud_assets=hud_assets,
                alpha=1.0,
                show_weapon_row=False,
                scale=scale,
                mouse=mouse,
            )
        else:
            score_card_pos = banner_pos + Vec2(
                30.0 * scale,
                (80.0 if self.rank < TABLE_MAX else 78.0) * scale,
            )
            if self.rank >= TABLE_MAX and banner_kind == "reaper":
                self._draw_small(
                    "Score too low for top100.",
                    banner_pos + Vec2(38.0 * scale, 62.0 * scale),
                    1.0 * scale,
                    rl.Color(200, 200, 200, 255),
                )

            self._draw_score_card(
                pos=score_card_pos,
                record=record,
                hud_assets=hud_assets,
                alpha=1.0,
                show_weapon_row=True,
                scale=scale,
                mouse=mouse,
            )

        # Buttons phase rendering.
        if self.phase == 1:
            button_pos = banner_pos + Vec2(52.0 * scale, (210.0 if self.rank < TABLE_MAX else 208.0) * scale)
            play_again_w = button_width(
                self.font, self._play_again_button.label, scale=scale, force_wide=self._play_again_button.force_wide,
            )
            button_draw(
                self.assets.perk_menu_assets,
                self.font,
                self._play_again_button,
                pos=button_pos,
                width=play_again_w,
                scale=scale,
            )
            button_pos = button_pos.offset(dy=32.0 * scale)

            high_scores_w = button_width(
                self.font, self._high_scores_button.label, scale=scale, force_wide=self._high_scores_button.force_wide,
            )
            button_draw(
                self.assets.perk_menu_assets,
                self.font,
                self._high_scores_button,
                pos=button_pos,
                width=high_scores_w,
                scale=scale,
            )
            button_pos = button_pos.offset(dy=32.0 * scale)

            main_menu_w = button_width(
                self.font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide,
            )
            button_draw(
                self.assets.perk_menu_assets,
                self.font,
                self._main_menu_button,
                pos=button_pos,
                width=main_menu_w,
                scale=scale,
            )

        draw_menu_cursor(
            self.assets.particles,
            self.assets.perk_menu_assets.cursor,
            pos=Vec2.from_xy(mouse),
            pulse_time=float(self._cursor_pulse_time),
        )
