//! LuaHost: the first real ScriptHost binding (v0.2 step 13).
//!
//! Cashes in the step 9 seam: a real Lua 5.4 interpreter (mlua, vendored)
//! behind the same `ScriptHost` trait, behind the `lua` feature flag, with
//! the core still never naming a language. Scripts READ the world through a
//! snapshot table, record wishes through a tiny `mawida.*` API that pushes
//! [`Command`]s, and the engine applies them after the borrow ends. A
//! script error logs a warning and drops that frame's commands; the engine
//! keeps running. The script file hot-reloads on save.
//!
//! Sandbox: the interpreter gets math/table/string only. No io, no os, no
//! load-from-string of arbitrary system access: a game script can compute
//! and wish, not touch the machine.

use std::cell::RefCell;
use std::rc::Rc;

use mlua::{Lua, LuaOptions, StdLib};

use crate::color::Color;
use crate::content::ContentManager;
use crate::error::{MawidaError, Result};
use crate::script::{Bundle, Command, Component, ScriptHost, WorldView};
use crate::time::GameTime;
use crate::transform::Transform;
use crate::world::Entity;

/// The built-in vocabulary of the built-in script API: what `mawida.spawn`
/// creates. Game code may use its own components everywhere else; these two
/// exist so the engine-level API has concrete types to file into columns.
#[derive(Clone, Copy, Debug)]
pub struct Quad {
    pub w: f32,
    pub h: f32,
    pub color: Color,
}

/// Velocity in pixels/second, the scripting API's flavor (x/y fields match
/// what a Lua script naturally writes).
#[derive(Clone, Copy, Debug, Default)]
pub struct Velocity {
    pub x: f32,
    pub y: f32,
}

pub struct LuaHost {
    lua: Lua,                          // the interpreter; drop closes it
    out: Rc<RefCell<Vec<Command>>>,    // shared with the API closures
    path: std::path::PathBuf,          // resolved script path, for reload
    mtime: Option<std::time::SystemTime>, // last seen modification time
}

fn mtime_of(p: &std::path::Path) -> Option<std::time::SystemTime> {
    std::fs::metadata(p).and_then(|m| m.modified()).ok()
}

impl LuaHost {
    /// Load `script` (a path under the content root) and run its top level,
    /// which defines the global `on_update(view, dt)` the engine will call.
    pub fn new(content: &ContentManager, script: &str) -> Result<Self> {
        let bytes = content.load_bytes(script)?;
        let mut host = Self::from_source(&bytes)?;
        host.path = content.full_path(script);
        host.mtime = mtime_of(&host.path);
        Ok(host)
    }

    /// Build a host straight from script bytes: the test path (no files),
    /// and the shared core of `new`.
    pub fn from_source(bytes: &[u8]) -> Result<Self> {
        // A restricted stdlib: math/table/string yes, io/os NO.
        let lua = Lua::new_with(
            StdLib::MATH | StdLib::TABLE | StdLib::STRING,
            LuaOptions::default(),
        )
        .map_err(|e| MawidaError::Script(e.to_string()))?;

        let host = Self {
            lua,
            out: Rc::new(RefCell::new(Vec::new())),
            path: std::path::PathBuf::new(),
            mtime: None,
        };
        host.register_api()
            .map_err(|e| MawidaError::Script(e.to_string()))?;

        // Scripts are content, like textures: bytes in, then exec so the
        // script's top level runs and `on_update` becomes a Lua global.
        host.lua
            .load(bytes)
            .exec()
            .map_err(|e| MawidaError::Script(e.to_string()))?;
        Ok(host)
    }

    /// The `mawida.*` table: the entire API a script sees. Each function is
    /// a closure OWNING a clone of the `Rc` handle (a refcount bump, not a
    /// copy of the Vec); `borrow_mut()` opens the cell just long enough to
    /// push one Command. Rc + RefCell is single-threaded shared mutation:
    /// exactly the shape of "several closures feed one buffer".
    fn register_api(&self) -> mlua::Result<()> {
        let mawida = self.lua.create_table()?;

        // spawn(x, y, w, h): one white quad with a zero velocity.
        let out = Rc::clone(&self.out);
        mawida.set(
            "spawn",
            self.lua
                .create_function(move |_, (x, y, w, h): (f32, f32, f32, f32)| {
                    out.borrow_mut().push(Command::Spawn(
                        Bundle::new()
                            .with(Transform::from_xy(x, y))
                            .with(Quad { w, h, color: Color::WHITE })
                            .with(Velocity::default()),
                    ));
                    Ok(())
                })?,
        )?;

        // despawn(id): the id is the packed u64 (index + generation), so a
        // stale id a script held too long no-ops instead of aliasing.
        let out = Rc::clone(&self.out);
        mawida.set(
            "despawn",
            self.lua.create_function(move |_, id: u64| {
                out.borrow_mut().push(Command::Despawn(Entity::from_bits(id)));
                Ok(())
            })?,
        )?;

        // set_velocity(id, vx, vy): one Set, as data, like everything else.
        let out = Rc::clone(&self.out);
        mawida.set(
            "set_velocity",
            self.lua
                .create_function(move |_, (id, vx, vy): (u64, f32, f32)| {
                    out.borrow_mut().push(Command::Set(
                        Entity::from_bits(id),
                        Component::new(Velocity { x: vx, y: vy }),
                    ));
                    Ok(())
                })?,
        )?;

        self.lua.globals().set("mawida", mawida)
    }

    /// The snapshot: World data -> one Lua table. Only data crosses the
    /// border. Entities arrive as a 1-based array of {id, x, y} rows.
    fn view_to_table(&self, world: &WorldView) -> mlua::Result<mlua::Table> {
        let view = self.lua.create_table()?;
        let entities = self.lua.create_table()?;
        for (i, (e, t)) in world.iter::<Transform>().enumerate() {
            let row = self.lua.create_table()?;
            row.set("id", e.to_bits())?;
            row.set("x", t.translation.x)?;
            row.set("y", t.translation.y)?;
            entities.set(i + 1, row)?; // Lua arrays are 1-based
        }
        view.set("entities", entities)?;
        Ok(view)
    }

    /// Hot reload: if the file's mtime changed, re-exec it. A failed reload
    /// keeps the last good script; a missing file keeps the loaded one.
    /// Edit fountain.lua while the engine runs, save, watch it change.
    fn maybe_reload(&mut self) {
        if self.path.as_os_str().is_empty() {
            return; // from_source host: nothing to watch
        }
        let now = mtime_of(&self.path);
        if now == self.mtime {
            return; // unchanged (or still missing): done
        }
        self.mtime = now;
        match std::fs::read(&self.path) {
            Err(_) => log::warn!("script missing; keeping the last loaded version"),
            Ok(bytes) => {
                if let Err(e) = self.lua.load(&bytes).exec() {
                    log::warn!("reload failed; keeping the last good script: {e}");
                }
            }
        }
    }
}

impl ScriptHost for LuaHost {
    fn on_event(&mut self, world: &WorldView, out: &mut Vec<Command>) {
        self.maybe_reload();

        // 1. snapshot: World data -> one Lua table. Only data crosses.
        let view = match self.view_to_table(world) {
            Ok(t) => t,
            Err(e) => {
                log::warn!("view snapshot failed: {e}");
                return;
            }
        };
        let dt = world
            .try_resource::<GameTime>()
            .map(|t| t.delta_seconds())
            .unwrap_or(0.0);

        // 2. call the script's on_update(view, dt), if it defines one.
        //    A Lua error surfaces as Err here, never as a crash.
        if let Ok(f) = self.lua.globals().get::<mlua::Function>("on_update") {
            if let Err(e) = f.call::<()>((view, dt)) {
                log::warn!("script error, frame's commands dropped: {e}");
                self.out.borrow_mut().clear(); // half a frame is worse than none
            }
        }

        // 3. drain: whatever the API closures pushed becomes this frame's
        //    output; the engine applies it after this borrow ends.
        out.append(&mut self.out.borrow_mut());
    }
}

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

    #[test]
    fn a_script_spawns_through_the_seam() {
        let mut host = LuaHost::from_source(
            b"function on_update(view, dt) mawida.spawn(10, 20, 16, 16) end",
        )
        .expect("host");
        let mut world = World::new();
        let mut out = Vec::new();

        host.on_event(&WorldView::new(&world), &mut out);
        assert_eq!(out.len(), 1);
        for cmd in out.drain(..) {
            world.apply(cmd);
        }
        assert_eq!(world.len(), 1); // a Lua script just made an entity

        // and the entity is visible to the NEXT frame's snapshot
        host.on_event(&WorldView::new(&world), &mut out);
        assert_eq!(out.len(), 1);
    }

    #[test]
    fn a_broken_script_never_crashes_the_engine() {
        let mut host =
            LuaHost::from_source(b"function on_update() error('boom') end").expect("host");
        let world = World::new();
        let mut out = Vec::new();

        host.on_event(&WorldView::new(&world), &mut out); // logs a warning...
        assert!(out.is_empty()); // ...and yields nothing
    }

    #[test]
    fn a_stale_script_id_cannot_alias() {
        let mut host = LuaHost::from_source(
            // the script remembers an id across frames via a Lua upvalue
            b"local saved
              function on_update(view, dt)
                if view.entities[1] then saved = view.entities[1].id end
                if saved then mawida.despawn(saved) end
              end",
        )
        .expect("host");
        let mut world = World::new();
        let e = world.spawn();
        world.insert(e, Transform::from_xy(1.0, 1.0));

        // frame 1: script sees e, despawns it
        let mut out = Vec::new();
        host.on_event(&WorldView::new(&world), &mut out);
        for cmd in out.drain(..) {
            world.apply(cmd);
        }
        assert!(!world.is_alive(e));

        // slot recycles: a NEW entity in the old slot
        let e2 = world.spawn();
        world.insert(e2, Transform::from_xy(2.0, 2.0));

        // frame 2: the script's saved id is stale; despawn must NO-OP
        // against e2 (the generation check crossing the border and back).
        host.on_event(&WorldView::new(&world), &mut out);
        // note: view.entities[1] is now e2, so the script re-saves e2's id
        // and despawns it legitimately. To pin the STALE case, apply the
        // saved-id despawn against a world where the slot was recycled:
        let stale = Command::Despawn(e);
        world.apply(stale);
        assert!(world.is_alive(e2)); // e2 untouched by e's stale handle
    }
}
