from __future__ import annotations

import random
from dataclasses import dataclass, field
from typing import cast

import pyray as rl

from grim.assets import PaqTextureCache
from grim.audio import AudioState
from grim.config import CrimsonConfig
from grim.console import ConsoleState
from grim.geom import Vec2
from grim.math import clamp
from grim.view import ViewContext

from ..creatures.runtime import CreatureFlags
from ..game_modes import GameMode
from ..gameplay import survival_check_level_up
from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
from ..perks.state import CreatureForPerks
from ..sim.input import PlayerInput
from ..tutorial.timeline import TutorialFrameActions, TutorialState, tick_tutorial_timeline
from ..ui.cursor import draw_menu_cursor
from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
from ..ui.perk_menu import (
    PerkMenuAssets,
    UiButtonState,
    button_draw,
    button_update,
    button_width,
    load_perk_menu_assets,
)
from ..weapon_runtime import weapon_assign_player
from .base_gameplay_mode import BaseGameplayMode
from .components.perk_menu_controller import PerkMenuContext, PerkMenuController

UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
UI_SPONSOR_COLOR = rl.Color(255, 255, 255, int(255 * 0.5))


@dataclass(slots=True)
class _TutorialUiLayout:
    panel_pos: Vec2 = field(default_factory=lambda: Vec2(0.0, 64.0))
    panel_padding: Vec2 = field(default_factory=lambda: Vec2(20.0, 8.0))


class TutorialMode(BaseGameplayMode):
    def __init__(
        self,
        ctx: ViewContext,
        *,
        demo_mode_active: bool = False,
        texture_cache: PaqTextureCache | None = None,
        config: CrimsonConfig | None = None,
        console: ConsoleState | None = None,
        audio: AudioState | None = None,
        audio_rng: random.Random | None = None,
    ) -> None:
        super().__init__(
            ctx,
            world_size=1024.0,
            default_game_mode_id=int(GameMode.TUTORIAL),
            demo_mode_active=bool(demo_mode_active),
            difficulty_level=0,
            hardcore=False,
            texture_cache=texture_cache,
            config=config,
            console=console,
            audio=audio,
            audio_rng=audio_rng,
        )
        self._tutorial = TutorialState()
        self._tutorial_actions = TutorialFrameActions()

        self._ui_assets: PerkMenuAssets | None = None
        self._ui_layout = _TutorialUiLayout()

        self._perk_menu = PerkMenuController()

        self._skip_button = UiButtonState("Skip tutorial", force_wide=True)
        self._play_button = UiButtonState("Play a game", force_wide=True)
        self._repeat_button = UiButtonState("Repeat tutorial", force_wide=True)

    def open(self) -> None:
        super().open()
        self._ui_assets = load_perk_menu_assets(self._assets_root)

        self._perk_menu.reset()

        self._skip_button = UiButtonState("Skip tutorial", force_wide=True)
        self._play_button = UiButtonState("Play a game", force_wide=True)
        self._repeat_button = UiButtonState("Repeat tutorial", force_wide=True)

        self._tutorial = TutorialState()
        self._tutorial_actions = TutorialFrameActions()

        self.state.perk_selection.pending_count = 0
        self.state.perk_selection.choices.clear()
        self.state.perk_selection.choices_dirty = True

        self.player.pos = Vec2(float(self.world.world_size) * 0.5, float(self.world.world_size) * 0.5)
        weapon_assign_player(self.player, 1)

    def close(self) -> None:
        self._ui_assets = None
        super().close()

    def _perk_menu_context(self) -> PerkMenuContext:
        fx_toggle = self.config.fx_toggle
        fx_detail = self.config.fx_detail(level=0, default=False)
        return PerkMenuContext(
            state=self.state,
            perk_state=self.state.perk_selection,
            players=[self.player],
            creatures=cast("list[CreatureForPerks]", self.creatures.entries),
            player=self.player,
            game_mode=int(GameMode.TUTORIAL),
            player_count=1,
            fx_toggle=fx_toggle,
            fx_detail=fx_detail,
            font=self._small,
            assets=self._ui_assets,
            mouse=self._ui_mouse_pos(),
            play_sfx=None,
        )

    def _handle_input(self) -> None:
        if self._perk_menu.open and rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
            self._perk_menu.close()
            return

        if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
            self._paused = not self._paused

        if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
            self._action = "open_pause_menu"
            return

    def _build_input(self) -> PlayerInput:
        keybinds = config_keybinds(self.config)
        if not keybinds:
            keybinds = (0x11, 0x1F, 0x1E, 0x20, 0x100)
        up_key, down_key, left_key, right_key, fire_key = player_move_fire_binds(keybinds, 0)

        move = Vec2(
            float(input_code_is_down(right_key)) - float(input_code_is_down(left_key)),
            float(input_code_is_down(down_key)) - float(input_code_is_down(up_key)),
        )

        mouse = self._ui_mouse_pos()
        aim = self.world.screen_to_world(Vec2.from_xy(mouse))

        fire_down = input_code_is_down(fire_key)
        fire_pressed = input_code_is_pressed(fire_key)
        reload_key = self.config.keybind_reload
        reload_pressed = input_code_is_pressed(reload_key)

        return PlayerInput(
            move=move,
            aim=aim,
            fire_down=bool(fire_down),
            fire_pressed=bool(fire_pressed),
            reload_pressed=bool(reload_pressed),
        )

    def _prompt_panel_rect(self, text: str, *, pos: Vec2, scale: float) -> tuple[rl.Rectangle, list[str], float]:
        lines = text.splitlines() if text else [""]
        line_h = float(self._ui_line_height(scale))
        max_w = 0.0
        for line in lines:
            max_w = max(max_w, float(self._ui_text_width(line, scale)))

        pad_x = self._ui_layout.panel_padding.x * scale
        pad_y = self._ui_layout.panel_padding.y * scale
        w = max_w + pad_x * 2.0
        h = float(len(lines)) * line_h + pad_y * 2.0

        screen_w = float(rl.get_screen_width())
        x = (screen_w - w) * 0.5
        rect = rl.Rectangle(float(x), pos.y, float(w), float(h))
        return rect, lines, line_h

    def _update_prompt_buttons(self, *, dt_ms: float, mouse: rl.Vector2, click: bool) -> None:
        if self._ui_assets is None:
            return

        stage = int(self._tutorial.stage_index)
        prompt_alpha = float(self._tutorial_actions.prompt_alpha)
        if stage == 8:
            self._play_button.alpha = prompt_alpha
            self._repeat_button.alpha = prompt_alpha
            self._play_button.enabled = prompt_alpha > 1e-3
            self._repeat_button.enabled = prompt_alpha > 1e-3
        else:
            skip_alpha = clamp(float(self._tutorial.stage_timer_ms - 1000) * 0.001, 0.0, 1.0)
            self._skip_button.alpha = skip_alpha
            self._skip_button.enabled = skip_alpha > 1e-3

        if stage == 8:
            rect, _lines, _line_h = self._prompt_panel_rect(
                self._tutorial_actions.prompt_text,
                pos=self._ui_layout.panel_pos,
                scale=1.0,
            )
            gap = 18.0
            button_base_pos = Vec2(rect.x + 10.0, rect.y + rect.height + 10.0)
            play_w = button_width(self._small, self._play_button.label, scale=1.0, force_wide=True)
            repeat_w = button_width(self._small, self._repeat_button.label, scale=1.0, force_wide=True)
            if button_update(
                self._play_button,
                pos=button_base_pos,
                width=play_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                self.close_requested = True
                return
            if button_update(
                self._repeat_button,
                pos=button_base_pos.offset(dx=play_w + gap),
                width=repeat_w,
                dt_ms=dt_ms,
                mouse=mouse,
                click=click,
            ):
                self.open()
                return
            return

        if self._skip_button.enabled:
            y = float(rl.get_screen_height()) - 50.0
            w = button_width(self._small, self._skip_button.label, scale=1.0, force_wide=True)
            if button_update(self._skip_button, pos=Vec2(10.0, y), width=w, dt_ms=dt_ms, mouse=mouse, click=click):
                self.close_requested = True

    def update(self, dt: float) -> None:
        self._update_audio(dt)
        dt_frame, dt_ui_ms = self._tick_frame(dt, clamp_cursor_pulse=True)

        self._handle_input()
        if self._action == "open_pause_menu":
            return
        if self.close_requested:
            return

        perk_ctx = self._perk_menu_context()
        perk_pending = int(self.state.perk_selection.pending_count) > 0 and self.player.health > 0.0
        if int(self._tutorial.stage_index) == 6 and perk_pending and not self._perk_menu.open:
            self._perk_menu.open_if_available(perk_ctx)

        perk_menu_active = self._perk_menu.active
        if self._perk_menu.open:
            self._perk_menu.handle_input(perk_ctx, dt_frame=dt_frame, dt_ui_ms=dt_ui_ms)
        self._perk_menu.tick_timeline(dt_ui_ms)

        dt_world = 0.0 if self._paused or perk_menu_active else dt_frame

        input_state = self._build_input()
        any_move_active = input_state.move.length_sq() > 0.0
        any_fire_active = bool(input_state.fire_pressed or input_state.fire_down)

        hint_alive_before = False
        hint_ref = self._tutorial.hint_bonus_creature_ref
        if hint_ref is not None and 0 <= int(hint_ref) < len(self.creatures.entries):
            entry = self.creatures.entries[int(hint_ref)]
            hint_alive_before = bool(entry.active and entry.hp > 0.0)

        if dt_world > 0.0:
            self.world.update(
                dt_world,
                inputs=[input_state],
                auto_pick_perks=False,
                game_mode=int(GameMode.TUTORIAL),
                perk_progression_enabled=True,
            )

        hint_alive_after = hint_alive_before
        if hint_ref is not None and 0 <= int(hint_ref) < len(self.creatures.entries):
            entry = self.creatures.entries[int(hint_ref)]
            hint_alive_after = bool(entry.active and entry.hp > 0.0)
        hint_bonus_died = hint_alive_before and (not hint_alive_after)

        creatures_none_active = not bool(self.creatures.iter_active())
        bonus_pool_empty = not bool(self.state.bonus_pool.iter_active())
        perk_pending_count = int(self.state.perk_selection.pending_count)

        self._tutorial, actions = tick_tutorial_timeline(
            self._tutorial,
            frame_dt_ms=dt_world * 1000.0,
            any_move_active=any_move_active,
            any_fire_active=any_fire_active,
            creatures_none_active=creatures_none_active,
            bonus_pool_empty=bonus_pool_empty,
            perk_pending_count=perk_pending_count,
            hint_bonus_died=hint_bonus_died,
            preserve_bugs=bool(self.world.preserve_bugs),
        )
        self._tutorial_actions = actions

        self.player.health = float(actions.force_player_health)
        if actions.force_player_experience is not None:
            self.player.experience = int(actions.force_player_experience)
            survival_check_level_up(self.player, self.state.perk_selection)

        detail_preset = 5
        if self.world.config is not None:
            detail_preset = self.world.config.detail_preset

        for call in actions.spawn_bonuses:
            spawned = self.state.bonus_pool.spawn_at(
                pos=call.pos,
                bonus_id=int(call.bonus_id),
                duration_override=int(call.amount),
                state=self.state,
                world_width=float(self.world.world_size),
                world_height=float(self.world.world_size),
            )
            if spawned is not None:
                self.state.effects.spawn_burst(
                    pos=spawned.pos,
                    count=12,
                    rand=self.state.rng.rand,
                    detail_preset=detail_preset,
                )

        for call in actions.spawn_templates:
            mapping, primary = self.creatures.spawn_template(
                int(call.template_id),
                call.pos,
                float(call.heading),
                self.state.rng,
                rand=self.state.rng.rand,
            )
            if int(call.template_id) == 0x27 and primary is not None and actions.stage5_bonus_carrier_drop is not None:
                drop_id, drop_amount = actions.stage5_bonus_carrier_drop
                self._tutorial.hint_bonus_creature_ref = int(primary)
                if 0 <= int(primary) < len(self.creatures.entries):
                    creature = self.creatures.entries[int(primary)]
                    creature.flags |= CreatureFlags.BONUS_ON_DEATH
                    creature.bonus_id = int(drop_id)
                    creature.bonus_duration_override = int(drop_amount)

        mouse = self._ui_mouse_pos()
        click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
        self._update_prompt_buttons(dt_ms=dt_ui_ms, mouse=mouse, click=click)

    def draw(self) -> None:
        perk_menu_active = self._perk_menu.active
        self.world.draw(
            draw_aim_indicators=not perk_menu_active,
            entity_alpha=self._world_entity_alpha(),
        )
        self._draw_screen_fade()

        hud_bottom = 0.0
        if (not perk_menu_active) and self._hud_assets is not None:
            hud_flags = hud_flags_for_game_mode(self._config_game_mode_id())
            self._draw_target_health_bar()
            hud_bottom = draw_hud_overlay(
                self._hud_assets,
                state=self._hud_state,
                player=self.player,
                players=self.world.players,
                bonus_hud=self.state.bonus_hud,
                elapsed_ms=float(self._tutorial.stage_timer_ms),
                score=int(self.player.experience),
                font=self._small,
                alpha=1.0,
                frame_dt_ms=self._last_dt_ms,
                show_health=hud_flags.show_health,
                show_weapon=hud_flags.show_weapon,
                show_xp=hud_flags.show_xp,
                show_time=hud_flags.show_time,
                show_quest_hud=hud_flags.show_quest_hud,
                small_indicators=self._hud_small_indicators(),
                preserve_bugs=bool(self.world.preserve_bugs),
            )

        self._draw_tutorial_prompts(hud_bottom=hud_bottom)

        if perk_menu_active:
            self._perk_menu.draw(self._perk_menu_context())
            self._draw_menu_cursor()

    def _draw_tutorial_prompts(self, *, hud_bottom: float) -> None:
        actions = self._tutorial_actions
        if actions.prompt_text and actions.prompt_alpha > 1e-3:
            self._draw_prompt_panel(
                actions.prompt_text,
                alpha=float(actions.prompt_alpha),
                pos=self._ui_layout.panel_pos,
            )
        if actions.hint_text and actions.hint_alpha > 1e-3:
            self._draw_prompt_panel(
                actions.hint_text,
                alpha=float(actions.hint_alpha),
                pos=self._ui_layout.panel_pos.offset(dy=84.0),
            )

        if self._ui_assets is None:
            return

        stage = int(self._tutorial.stage_index)
        if stage == 8:
            rect, _lines, _line_h = self._prompt_panel_rect(
                actions.prompt_text,
                pos=self._ui_layout.panel_pos,
                scale=1.0,
            )
            gap = 18.0
            button_base_pos = Vec2(rect.x + 10.0, rect.y + rect.height + 10.0)
            play_w = button_width(self._small, self._play_button.label, scale=1.0, force_wide=True)
            repeat_w = button_width(self._small, self._repeat_button.label, scale=1.0, force_wide=True)
            button_draw(
                self._ui_assets,
                self._small,
                self._play_button,
                pos=button_base_pos,
                width=play_w,
                scale=1.0,
            )
            button_draw(
                self._ui_assets,
                self._small,
                self._repeat_button,
                pos=button_base_pos.offset(dx=play_w + gap),
                width=repeat_w,
                scale=1.0,
            )
            return

        if self._skip_button.alpha > 1e-3:
            y = float(rl.get_screen_height()) - 50.0
            w = button_width(self._small, self._skip_button.label, scale=1.0, force_wide=True)
            button_draw(self._ui_assets, self._small, self._skip_button, pos=Vec2(10.0, y), width=w, scale=1.0)

        if self._paused:
            x = 18.0
            y = max(18.0, hud_bottom + 10.0)
            self._draw_ui_text("paused (TAB)", Vec2(x, y), UI_HINT_COLOR)

    def _draw_prompt_panel(self, text: str, *, alpha: float, pos: Vec2) -> None:
        alpha = clamp(float(alpha), 0.0, 1.0)
        rect, lines, line_h = self._prompt_panel_rect(text, pos=pos, scale=1.0)
        fill = rl.Color(0, 0, 0, int(255 * alpha * 0.8))
        border = rl.Color(255, 255, 255, int(255 * alpha))
        rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), fill)
        rl.draw_rectangle_lines(int(rect.x), int(rect.y), int(rect.width), int(rect.height), border)

        text_alpha = int(255 * clamp(alpha * 0.9, 0.0, 1.0))
        color = rl.Color(255, 255, 255, text_alpha)
        x = rect.x + self._ui_layout.panel_padding.x
        line_y = rect.y + self._ui_layout.panel_padding.y
        for line in lines:
            self._draw_ui_text(line, Vec2(x, line_y), color, scale=1.0)
            line_y += line_h

    def _draw_menu_cursor(self) -> None:
        assets = self._ui_assets
        if assets is None:
            return
        cursor_tex = assets.cursor
        mouse_pos = self._ui_mouse
        draw_menu_cursor(
            self.world.particles_texture,
            cursor_tex,
            pos=mouse_pos,
            pulse_time=float(self._cursor_pulse_time),
        )
