//! The [`ScriptEngine`] — a thin wrapper over a `rhai` interpreter — plus the
//! [`CompiledScript`] handle and the [`ScriptError`] type.
//!
//! `rhai` is sandboxed by default (no filesystem or OS access), which is exactly
//! what hot-reloaded game logic wants. This wrapper owns one configured engine
//! that the host reuses to compile every script, keeping engine setup (limits,
//! `print`/`debug` routing, later the engine API bindings) in one place. A
//! script is compiled once into a [`CompiledScript`] (a reusable AST) and then
//! evaluated cheaply each frame.

use oxide_engine::math::Transform;
use oxide_engine::scene::Entity;
use rhai::{Engine, Scope, AST};

use crate::bridge::{register_api, ScriptContext, SharedContext};
use crate::ScriptAsset;

/// Errors raised while compiling or running a script.
///
/// Both variants name the offending script so the editor console can attribute
/// the failure; the host uses a runtime error to **pause** that one script
/// rather than crash the editor (Stage 10 error-isolation goal).
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ScriptError {
    /// The source failed to parse / compile.
    #[error("script '{name}' failed to compile: {message}")]
    Compile {
        /// The script's diagnostic name.
        name: String,
        /// The compiler's message.
        message: String,
    },
    /// The script compiled but raised an error while running.
    #[error("script '{name}' raised a runtime error: {message}")]
    Runtime {
        /// The script's diagnostic name.
        name: String,
        /// The runtime error message.
        message: String,
    },
}

/// A compiled, ready-to-run script: its `rhai` AST and the diagnostic name it
/// was compiled from.
///
/// Cheap to keep around and re-evaluate; the host stores one per live script and
/// replaces it wholesale on live reload.
#[derive(Clone, Debug)]
pub struct CompiledScript {
    /// The compiled abstract syntax tree.
    pub ast: AST,
    /// The source script's diagnostic name (for error messages).
    pub name: String,
}

/// A configured `rhai` engine the host reuses to compile and run scripts.
///
/// One engine compiles many scripts; the configuration (resource limits,
/// `print`/`debug` routing, and — in later pieces — the engine type API) lives
/// here so every script sees the same sandbox.
pub struct ScriptEngine {
    engine: Engine,
    /// Shared scratch the engine API reads/writes: the host stages the active
    /// entity's transform here, the script mutates it, the host reads it back.
    ctx: SharedContext,
}

impl ScriptEngine {
    /// Builds an engine with Oxide's default sandbox configuration and the
    /// engine API (the [`Vec3`](oxide_engine::math::Vec3) type + ambient
    /// transform functions) registered.
    ///
    /// `print` and `debug` output is routed to the `log` crate (so the editor
    /// console can surface it); operation limits guard against an accidental
    /// infinite loop wedging the editor.
    pub fn new() -> Self {
        let mut engine = Engine::new();
        // Route script `print`/`debug` to the log so the console can capture it
        // instead of leaking to stdout.
        engine.on_print(|text| log::info!(target: "oxide_script", "{text}"));
        engine.on_debug(|text, source, pos| {
            log::debug!(target: "oxide_script", "{}:{pos:?}: {text}", source.unwrap_or("script"));
        });
        // A generous cap: enough for real per-frame logic, low enough that a
        // runaway loop surfaces as a runtime error rather than a hang.
        engine.set_max_operations(2_000_000);

        let ctx: SharedContext =
            std::sync::Arc::new(std::sync::Mutex::new(ScriptContext::default()));
        register_api(&mut engine, &ctx);

        Self { engine, ctx }
    }

    // --- Context staging (host ↔ script transform hand-off) ----------------

    /// Stages the active `entity`, its `transform`, and frame delta `dt` into the
    /// shared context before a call, so the script's `position`/`translate`/`dt`/
    /// `entity()`/… see the entity's current pose and identity. Clears any
    /// command buffer left over from a previous call (the host always
    /// [`take_commands`](Self::take_commands) after each call, so this is just
    /// belt-and-suspenders); the provisional-id counter is deliberately *not*
    /// reset, keeping spawn ids unique across a frame's `init` + `update`.
    pub fn set_context(&self, entity: Entity, transform: Transform, dt: f32) {
        let mut g = self.ctx.lock().unwrap();
        g.current = Some(entity);
        g.transform = transform;
        g.dt = dt;
        g.commands.clear();
    }

    /// Reads the (possibly script-mutated) transform back out of the context
    /// after a call, so the host can write it to the ECS.
    pub fn take_transform(&self) -> Transform {
        self.ctx.lock().unwrap().transform
    }

    /// Drains the scene-mutation commands the script issued this call, so the
    /// host can apply them against the scene + reflection registry.
    pub(crate) fn take_commands(&self) -> Vec<crate::bridge::ScriptCommand> {
        std::mem::take(&mut self.ctx.lock().unwrap().commands)
    }

    // --- Lifecycle ---------------------------------------------------------

    /// Whether the compiled script defines a function named `name` taking
    /// `arity` parameters (used to skip absent lifecycle hooks rather than treat
    /// "no such function" as an error).
    pub fn has_function(&self, script: &CompiledScript, name: &str, arity: usize) -> bool {
        script
            .ast
            .iter_functions()
            .any(|f| f.name == name && f.params.len() == arity)
    }

    /// Starts a script in `scope`: runs its top level once (defining functions
    /// and one-shot setup), then calls `init()` if it defines one.
    ///
    /// `scope` persists across frames, so top-level `let` bindings live on as the
    /// script's state. A [`ScriptError::Runtime`] is returned (never panicked) so
    /// the host can pause just this script.
    pub fn start(&self, scope: &mut Scope, script: &CompiledScript) -> Result<(), ScriptError> {
        self.engine
            .run_ast_with_scope(scope, &script.ast)
            .map_err(|err| self.runtime_error(script, err))?;
        if self.has_function(script, "init", 0) {
            self.engine
                .call_fn::<()>(scope, &script.ast, "init", ())
                .map_err(|err| self.runtime_error(script, err))?;
        }
        Ok(())
    }

    /// Runs one frame of a started script: calls `update(dt)` if it defines one.
    /// The host must [`set_context`](Self::set_context) first.
    pub fn run_update(
        &self,
        scope: &mut Scope,
        script: &CompiledScript,
        dt: f32,
    ) -> Result<(), ScriptError> {
        if self.has_function(script, "update", 1) {
            self.engine
                .call_fn::<()>(scope, &script.ast, "update", (dt,))
                .map_err(|err| self.runtime_error(script, err))?;
        }
        Ok(())
    }

    /// Builds a [`ScriptError::Runtime`] from a `rhai` evaluation error.
    fn runtime_error(&self, script: &CompiledScript, err: Box<rhai::EvalAltResult>) -> ScriptError {
        ScriptError::Runtime {
            name: script.name.clone(),
            message: err.to_string(),
        }
    }

    /// Borrows the underlying `rhai` engine (for host wiring that registers
    /// types/functions on it).
    pub fn raw(&self) -> &Engine {
        &self.engine
    }

    /// Mutably borrows the underlying engine, e.g. to register engine API
    /// functions a script can call.
    pub fn raw_mut(&mut self) -> &mut Engine {
        &mut self.engine
    }

    /// Compiles `asset`'s source into a reusable [`CompiledScript`], or reports a
    /// [`ScriptError::Compile`] naming the script.
    pub fn compile(&self, asset: &ScriptAsset) -> Result<CompiledScript, ScriptError> {
        match self.engine.compile(&asset.source) {
            Ok(ast) => Ok(CompiledScript {
                ast,
                name: asset.name.clone(),
            }),
            Err(err) => Err(ScriptError::Compile {
                name: asset.name.clone(),
                message: err.to_string(),
            }),
        }
    }

    /// Runs a compiled script's top level in a fresh scope, discarding its value.
    ///
    /// This executes statements at file scope (where a script defines its
    /// functions and any one-shot setup). A [`ScriptError::Runtime`] is returned
    /// — never panicked — so the host can pause just this script.
    pub fn run(&self, script: &CompiledScript) -> Result<(), ScriptError> {
        self.engine
            .run_ast(&script.ast)
            .map_err(|err| ScriptError::Runtime {
                name: script.name.clone(),
                message: err.to_string(),
            })
    }
}

impl Default for ScriptEngine {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compiles_and_runs_valid_source() {
        let engine = ScriptEngine::new();
        let asset = ScriptAsset::from_source("ok", "let x = 1 + 2; print(x);");
        let compiled = engine.compile(&asset).expect("compiles");
        assert_eq!(compiled.name, "ok");
        engine.run(&compiled).expect("runs");
    }

    #[test]
    fn a_syntax_error_is_a_compile_error_naming_the_script() {
        let engine = ScriptEngine::new();
        let asset = ScriptAsset::from_source("broken", "let x = ;");
        let err = engine.compile(&asset).unwrap_err();
        match err {
            ScriptError::Compile { name, .. } => assert_eq!(name, "broken"),
            other => panic!("expected a compile error, got {other:?}"),
        }
    }

    #[test]
    fn a_runtime_failure_is_isolated_as_a_runtime_error() {
        let engine = ScriptEngine::new();
        // Throws at run time, not compile time.
        let asset = ScriptAsset::from_source("throws", "throw \"boom\";");
        let compiled = engine.compile(&asset).expect("compiles");
        let err = engine.run(&compiled).unwrap_err();
        match err {
            ScriptError::Runtime { name, .. } => assert_eq!(name, "throws"),
            other => panic!("expected a runtime error, got {other:?}"),
        }
    }

    #[test]
    fn an_infinite_loop_is_capped_rather_than_hanging() {
        let engine = ScriptEngine::new();
        let asset = ScriptAsset::from_source("spin", "let i = 0; loop { i += 1; }");
        let compiled = engine.compile(&asset).expect("compiles");
        // The operation cap turns the runaway loop into a runtime error.
        assert!(engine.run(&compiled).is_err());
    }
}
