//! The ScriptHost seam (learning path step 9c/9d).
//!
//! v0.1 ships NO scripting runtime. What it ships is the boundary: the core
//! speaks data + commands + queries and never names a language. A script
//! READS the world through a shared [`WorldView`], records its wishes as
//! [`Command`] values, and the engine applies them AFTER the read borrow
//! ends. That deferral is the whole trick: "mutate the scene from inside a
//! callback that is iterating it" is a compile error here, not a latent bug.
//!
//! v0.2+: a LuaHost implements [`ScriptHost`] via mlua behind a `lua`
//! feature flag; later a WasmHost implements the SAME trait, sandboxed.
//! Neither dependency exists in v0.1; this file is std + our World only.

use crate::world::{Entity, World};

// The first real binding lives behind the feature flag; this module (the
// seam) stays ungated and never names a language.
#[cfg(feature = "lua")]
pub mod lua_host;

// ---------------------------------------------------------------------------
// Components and bundles as type-erased, insert-once values
// ---------------------------------------------------------------------------

/// The internal trick that lets a Command carry "some component, type
/// forgotten" and still insert it later: a small custom trait object, the
/// same tool `Any` itself is built from. `self: Box<Self>` means calling
/// this consumes the box and moves the value out: an FnOnce-shaped method.
trait InsertComponent {
    fn insert_into(self: Box<Self>, world: &mut World, entity: Entity);
}

/// Every 'static type gets the impl for free: unbox and file into T's column.
impl<T: 'static> InsertComponent for T {
    fn insert_into(self: Box<Self>, world: &mut World, entity: Entity) {
        world.insert(entity, *self);
    }
}

/// One component value with its type erased, ready to be set on an entity.
/// The typed world API (`world.insert(e, value)`) is still the normal path;
/// this exists so a *command* can carry any component as plain data.
pub struct Component(Box<dyn InsertComponent>);

impl Component {
    pub fn new<T: 'static>(value: T) -> Self {
        Self(Box::new(value))
    }

    /// Consumes the component: it can be applied exactly once.
    fn insert(self, world: &mut World, entity: Entity) {
        self.0.insert_into(world, entity);
    }
}

/// A set of components to spawn one entity with: what a script means by
/// "make me one of these". Built fluently: `Bundle::new().with(Transform::default())`.
#[derive(Default)]
pub struct Bundle(Vec<Component>);

impl Bundle {
    pub fn new() -> Self {
        Self::default()
    }

    /// Consuming builder, like App's: each call moves the Bundle through.
    pub fn with<T: 'static>(mut self, value: T) -> Self {
        self.0.push(Component::new(value));
        self
    }

    /// Insert every carried component on `entity`.
    fn insert(self, world: &mut World, entity: Entity) {
        for component in self.0 {
            component.insert(world, entity);
        }
    }
}

// ---------------------------------------------------------------------------
// The command language and the read-only view
// ---------------------------------------------------------------------------

/// One deferred mutation as data: the closed set of things a script may ask
/// for. The core emits/accepts these; a language binds to them; the core
/// stays neutral. Adding a variant forces every `match` on Command in the
/// codebase to handle it before the crate compiles again: exhaustiveness
/// as an API-evolution tool.
pub enum Command {
    /// Spawn a new entity carrying the bundle's components.
    Spawn(Bundle),
    /// Despawn by handle. The generational id means a stale script handle
    /// is caught (no-op), never aliased onto the slot's next tenant.
    Despawn(Entity),
    /// Set one component value on an existing entity.
    Set(Entity, Component),
}

/// The read-only query surface a script sees: lookups yes, mutation no.
/// A shared borrow, typed: while a `WorldView` is alive, no `&mut World`
/// can exist, so "mutate during the callback" cannot compile. There is no
/// const_cast to reach for.
pub struct WorldView<'w> {
    world: &'w World,
}

impl<'w> WorldView<'w> {
    pub fn new(world: &'w World) -> Self {
        Self { world }
    }

    pub fn get<T: 'static>(&self, entity: Entity) -> Option<&T> {
        self.world.get(entity)
    }

    pub fn is_alive(&self, entity: Entity) -> bool {
        self.world.is_alive(entity)
    }

    pub fn len(&self) -> usize {
        self.world.len()
    }

    pub fn is_empty(&self) -> bool {
        self.world.is_empty()
    }

    /// Every live entity that has a `T`, read-only: the walk a script's
    /// world snapshot is built from.
    pub fn iter<T: 'static>(&self) -> impl Iterator<Item = (Entity, &T)> {
        self.world.iter::<T>()
    }

    /// Read a resource, or None: scripts may run in worlds that have not
    /// inserted one (headless tests), so the probing form is the API.
    pub fn try_resource<T: 'static>(&self) -> Option<&T> {
        self.world.try_resource::<T>()
    }
}

/// The language-neutral trait a scripting runtime implements: the single
/// entry point of the entire scripting surface. The engine hands the host a
/// view and an empty buffer; the host reads, thinks in whatever language it
/// hosts, and appends wishes. Read access is total; write access is
/// mediated through [`Command`]s the engine can validate, log, or reject.
pub trait ScriptHost {
    fn on_event(&mut self, world: &WorldView, out: &mut Vec<Command>);
}

/// The v0.1 host: does nothing, costs nothing. The seam exists; no runtime
/// does. An engine with NullHost registered does exactly what an engine
/// with no scripting does, so nobody pays for the future before it arrives.
pub struct NullHost;

impl ScriptHost for NullHost {
    fn on_event(&mut self, _world: &WorldView, _out: &mut Vec<Command>) {}
}

// ---------------------------------------------------------------------------
// Applying commands: phase 2 of the collect-then-apply dance
// ---------------------------------------------------------------------------

impl World {
    /// Apply one command with exclusive access, after the read borrow ended.
    /// A match over the closed command language; deferred application gives
    /// the engine a checkpoint where commands could be validated, logged for
    /// replay, counted for budgets, or dropped for a misbehaving mod.
    pub fn apply(&mut self, cmd: Command) {
        match cmd {
            Command::Spawn(bundle) => {
                let e = self.spawn();
                bundle.insert(self, e);
            }
            Command::Despawn(e) => self.despawn(e),
            Command::Set(e, c) => {
                // is_alive is checked inside insert; a stale handle no-ops.
                c.insert(self, e);
            }
        }
    }
}

// ---------------------------------------------------------------------------
// ScriptModule: the carrier that plugs ANY host into the frame loop
// ---------------------------------------------------------------------------

/// Wraps any [`ScriptHost`] as a [`Module`](crate::module::Module): each
/// frame it runs the step 9c dance verbatim - collect commands through a
/// read-only view, then apply them after the borrow ends. It holds a
/// `Box<dyn ScriptHost>` and never names a language; only concrete hosts
/// (LuaHost) live behind feature flags.
#[cfg(feature = "runtime")]
pub struct ScriptModule {
    host: Box<dyn ScriptHost>,
    commands: Vec<Command>, // reused buffer: no per-frame allocation
}

#[cfg(feature = "runtime")]
impl ScriptModule {
    pub fn new(host: impl ScriptHost + 'static) -> Self {
        Self {
            host: Box::new(host),
            commands: Vec::new(),
        }
    }
}

#[cfg(feature = "runtime")]
impl crate::module::Module for ScriptModule {
    fn update(&mut self, world: &mut World, _dt: f32) {
        self.commands.clear();
        {
            let view = WorldView::new(&*world); // shared borrow begins
            self.host.on_event(&view, &mut self.commands); // host READS only
        } // shared borrow ENDS here
        for cmd in self.commands.drain(..) {
            world.apply(cmd); // engine mutates, after the fact
        }
    }
}

// ---------------------------------------------------------------------------
// Tests: the seam proven headless. No window, no GPU, no Lua, no features.
// ---------------------------------------------------------------------------

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

    /// Stands in for Lua until v0.2: a "script" that spawns two entities.
    /// A unit struct implementing a trait is a five-line test double; no
    /// mocking framework, and #[cfg(test)] keeps it out of release builds.
    struct TestHost;

    impl ScriptHost for TestHost {
        fn on_event(&mut self, _world: &WorldView, out: &mut Vec<Command>) {
            out.push(Command::Spawn(Bundle::new().with(Transform::default())));
            out.push(Command::Spawn(Bundle::new().with(Transform::default())));
        }
    }

    #[test]
    fn host_commands_apply_headlessly() {
        let mut world = World::new();
        let mut host = TestHost;
        let mut commands = Vec::new();

        // phase 1: the host READS through a shared view and records wishes
        host.on_event(&WorldView::new(&world), &mut commands);
        // (the view's borrow has ended) phase 2: the engine applies them
        for cmd in commands.drain(..) {
            world.apply(cmd);
        }

        assert_eq!(world.len(), 2); // two entities, spawned by a "script"
    }

    #[test]
    fn null_host_costs_nothing() {
        let world = World::new();
        let mut commands = Vec::new();
        NullHost.on_event(&WorldView::new(&world), &mut commands);
        assert!(commands.is_empty());
    }

    #[test]
    fn set_and_despawn_commands() {
        let mut world = World::new();
        let e = world.spawn();

        world.apply(Command::Set(e, Component::new(Transform::from_xy(3.0, 4.0))));
        assert_eq!(world.get::<Transform>(e).unwrap().translation.x, 3.0);

        world.apply(Command::Despawn(e));
        assert!(!world.is_alive(e));

        // a stale handle in a command is caught, not aliased: both no-op
        world.apply(Command::Despawn(e));
        world.apply(Command::Set(e, Component::new(Transform::default())));
        assert_eq!(world.len(), 0);
    }

    #[test]
    fn worldview_is_read_only_by_type() {
        let mut world = World::new();
        let e = world.spawn();
        world.insert(e, Transform::from_xy(1.0, 2.0));

        let view = WorldView::new(&world);
        assert!(view.is_alive(e));
        assert_eq!(view.len(), 1);
        assert_eq!(view.get::<Transform>(e).unwrap().translation.y, 2.0);
        // There is no method on WorldView that mutates; and while `view`
        // lives, `world.insert(..)` would not compile. The signature is
        // the security policy.
    }
}
