//! Owner des dispatch-relevanten Spielzustands und der Borrow-View, durch
//! die Signal-Actions und Ink-Story-Steps darauf zugreifen.
//!
//! `Game` hält die Slots, die Actions lesen oder mutieren dürfen.
//! `ActionCtx` ist der schmale `&mut`-View; er wird über `Game::action_ctx()`
//! konstruiert und durch die Dispatch-Pipeline gereicht.
//!
//! Verben, deren Ziel-Subsystem (Szene, Audio, Renderer) noch nicht
//! existiert oder den Kern nichts angeht, produzieren [`Action`]-Werte
//! statt direkt zu wirken. Frontends konsumieren die Queue: die CLI druckt
//! sie, die Engine führt sie später aus. So bleibt der Kern headless
//! testbar und kennt keines der Subsysteme.

use std::collections::HashMap;

use crate::engine::ink;
use crate::engine::kv::Store;

/// Signal-Table aus `assets/signals.toml`: Signal-Name → Action-Strings.
/// Geladen/geparst von [`crate::engine::signals`]; definiert ist der Typ
/// hier beim Zustand, damit `game` ← `signals` eine Einbahnstraße bleibt.
pub type Signals = HashMap<String, Vec<String>>;

/// Action, die der Kern nicht selbst ausführt, sondern ans Frontend
/// weiterreicht. Die Queue in `Game::actions` gehört nach jedem Dispatch
/// geleert (konsumiert).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
    /// `assets/sounds/{name}.wav` als Einmal-SFX abspielen.
    PlaySound(String),
    /// Anzeige-/Eingabemodus wechseln (Spiel, Flycam, Menü). Vom Frontend
    /// auf seinen `Mode` gemappt; der Dialog-Modus ist hier bewusst nicht
    /// wählbar — der entsteht nur aus dem Story-Ablauf.
    SetMode(ModeTarget),
}

/// Frontend-neutrales Ziel eines Moduswechsels. Spiegelt die nicht-Dialog-
/// Varianten von `session::Mode`, ohne dass der Kern die rich `Mode`-Daten
/// (Dialog-State) kennen muss. So bleibt `game` ← `session` eine Einbahnstraße.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeTarget {
    /// First-Person-Spiel.
    Play,
    /// Noclip-Debug-Flycam.
    Free,
    /// Menü (pausiert die Welt, gibt die Maus frei).
    Menu,
}

impl ModeTarget {
    /// `play`/`free`/`menu` (case-insensitiv) → Ziel; sonst `None`.
    pub fn parse(s: &str) -> Option<Self> {
        match s.trim().to_ascii_lowercase().as_str() {
            "play" => Some(Self::Play),
            "free" => Some(Self::Free),
            "menu" => Some(Self::Menu),
            _      => None,
        }
    }

    /// Kanonischer Name fürs Echo/Logging.
    pub fn label(self) -> &'static str {
        match self {
            Self::Play => "play",
            Self::Free => "free",
            Self::Menu => "menu",
        }
    }
}

pub struct Game {
    pub kv:      Store,
    pub signals: Signals,
    pub story:   Option<ink::Story>,
    pub actions: Vec<Action>,
}

pub struct ActionCtx<'a> {
    pub kv:      &'a mut Store,
    pub signals: &'a Signals,
    pub story:   &'a mut Option<ink::Story>,
    pub actions: &'a mut Vec<Action>,
    /// Auslöser-Identifier: bei Objekt-Interaktion der volle Instance-Name
    /// (mit Blender-Suffix, z.B. `Mushroom.005`). `$self` in Action-Args
    /// wird damit substituiert. `None` bei Signalen ohne Quell-Instance
    /// (z.B. `init`, Story-Tags).
    pub instance_name: Option<String>,
}

impl Game {
    pub fn new(signals: Signals) -> Self {
        Self { kv: Store::new(), signals, story: None, actions: Vec::new() }
    }

    pub fn action_ctx(&mut self, instance_name: Option<String>) -> ActionCtx<'_> {
        ActionCtx {
            kv:      &mut self.kv,
            signals: &self.signals,
            story:   &mut self.story,
            actions: &mut self.actions,
            instance_name,
        }
    }
}
