//! The **engine API** exposed to scripts, and the shared [`ScriptContext`] that
//! backs it.
//!
//! A script does not get a raw pointer into the ECS; instead the host stages the
//! current entity's [`Transform`] into a shared [`ScriptContext`] before each
//! call, the script reads and mutates it through ambient functions (`position`,
//! `translate`, `rotate_y`, …), and the host writes the result back to the ECS
//! afterwards. This keeps the bridge tiny and single-threaded-safe while giving
//! scripts a Unity-like `transform`-style API.
//!
//! The context is shared as an `Arc<Mutex<…>>` because the `rhai` `sync` feature
//! requires every registered function to be `Send + Sync`; the editor runs
//! scripts one at a time, so the mutex is never actually contended.

use std::sync::{Arc, Mutex};

use oxide_engine::math::{Quat, Transform, Vec3};
use oxide_engine::scene::Entity;
use rhai::Engine;

/// A script-facing reference to an entity.
///
/// A script can name either a **real** entity (e.g. its own, via `entity()`) or
/// one it `spawn`ed earlier this same frame, which does not exist in the ECS yet
/// — the latter is a **provisional** id the host resolves to a real [`Entity`]
/// when it drains and applies the [`ScriptCommand`] buffer. This lets a script
/// spawn an entity and configure it in one go without the host round-tripping
/// the new id back into the running script.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EntityHandle {
    /// An entity that already exists in the scene.
    Real(Entity),
    /// An entity `spawn`ed this frame, identified by a monotonic id until the
    /// host creates it and maps the id to a real [`Entity`].
    Provisional(u64),
}

/// A scene mutation a script requested, buffered for the host to apply after the
/// script returns.
///
/// Scripts cannot touch the ECS directly (the `rhai` engine's registered
/// functions must be `Send + Sync` and hold no borrow of the world), so every
/// structural change a script makes is recorded here and replayed by the host
/// against the scene + reflection registry — the same "stage in, read back out"
/// discipline the transform API uses, extended to the whole entity/component
/// graph. Component edits go through RON so they round-trip the reflection
/// registry exactly like the inspector and AI agents do.
#[derive(Debug, Clone)]
pub(crate) enum ScriptCommand {
    /// Create a new root entity with the given node name; its provisional id is
    /// the one handed back to the script by `spawn`.
    Spawn {
        /// The provisional id the script holds for the new entity.
        provisional: u64,
        /// The new entity's node name.
        name: String,
    },
    /// Despawn an entity (recursively, taking its subtree).
    Despawn {
        /// The entity to remove.
        target: EntityHandle,
    },
    /// Add a default-constructed component of the named type, if absent.
    AddComponent {
        /// The entity to add to.
        target: EntityHandle,
        /// The registered component type name.
        type_name: String,
    },
    /// Insert or replace a component from its RON serialization.
    SetComponent {
        /// The entity to write to.
        target: EntityHandle,
        /// The registered component type name.
        type_name: String,
        /// The component value as RON.
        ron: String,
    },
    /// Remove the named component if present.
    RemoveComponent {
        /// The entity to remove from.
        target: EntityHandle,
        /// The registered component type name.
        type_name: String,
    },
}

/// The per-call scratch a running script reads from and writes to.
///
/// The host sets [`transform`](Self::transform) to the current entity's pose and
/// [`dt`](Self::dt) to the frame delta before invoking the script, then reads
/// `transform` back out to apply the script's changes.
#[derive(Debug, Clone, Default)]
pub(crate) struct ScriptContext {
    /// The active entity's transform — staged in by the host, mutated by the
    /// script, read back out by the host.
    pub transform: Transform,
    /// The current frame delta (seconds), also passed to `update(dt)`.
    pub dt: f32,
    /// The entity the script currently runs on, so `entity()` can name it.
    pub current: Option<Entity>,
    /// Scene mutations the script requested this call; drained by the host.
    pub commands: Vec<ScriptCommand>,
    /// Monotonic source of provisional ids for `spawn`. Never reset, so ids stay
    /// unique across a frame's `init` + `update` calls (the host maps each to a
    /// real entity), avoiding collisions in the apply step.
    pub next_provisional: u64,
}

/// The shared handle the engine's registered functions close over.
pub(crate) type SharedContext = Arc<Mutex<ScriptContext>>;

/// Registers Oxide's engine API on `engine`, backed by the shared `ctx`.
///
/// This is the surface a script sees: the [`Vec3`] type plus the ambient
/// transform functions. It is intentionally small for this first piece — more
/// engine types and component accessors layer on here in later pieces.
pub(crate) fn register_api(engine: &mut Engine, ctx: &SharedContext) {
    register_vec3(engine);
    register_transform_api(engine, ctx);
    register_world_api(engine, ctx);
}

/// Registers the [`Vec3`] type with constructors, component access, and the
/// arithmetic a script needs to do vector math.
fn register_vec3(engine: &mut Engine) {
    engine
        .register_type_with_name::<Vec3>("Vec3")
        .register_fn("vec3", Vec3::new)
        .register_fn("vec3", || Vec3::ZERO)
        .register_get_set("x", |v: &mut Vec3| v.x, |v: &mut Vec3, x: f32| v.x = x)
        .register_get_set("y", |v: &mut Vec3| v.y, |v: &mut Vec3, y: f32| v.y = y)
        .register_get_set("z", |v: &mut Vec3| v.z, |v: &mut Vec3, z: f32| v.z = z)
        .register_fn("+", |a: Vec3, b: Vec3| a + b)
        .register_fn("-", |a: Vec3, b: Vec3| a - b)
        .register_fn("*", |a: Vec3, s: f32| a * s)
        .register_fn("*", |s: f32, a: Vec3| a * s)
        .register_fn("length", |v: &mut Vec3| v.length())
        .register_fn("normalize", |v: &mut Vec3| v.normalize_or_zero())
        .register_fn("to_string", |v: &mut Vec3| {
            format!("({}, {}, {})", v.x, v.y, v.z)
        });
}

/// Registers the ambient transform API: functions that read and mutate the
/// active entity's pose via the shared context.
fn register_transform_api(engine: &mut Engine, ctx: &SharedContext) {
    let c = ctx.clone();
    engine.register_fn("position", move || c.lock().unwrap().transform.translation);

    let c = ctx.clone();
    engine.register_fn("set_position", move |p: Vec3| {
        c.lock().unwrap().transform.translation = p;
    });

    let c = ctx.clone();
    engine.register_fn("translate", move |v: Vec3| {
        c.lock().unwrap().transform.translation += v;
    });
    let c = ctx.clone();
    engine.register_fn("translate", move |x: f32, y: f32, z: f32| {
        c.lock().unwrap().transform.translation += Vec3::new(x, y, z);
    });

    let c = ctx.clone();
    engine.register_fn("scale", move || c.lock().unwrap().transform.scale);
    let c = ctx.clone();
    engine.register_fn("set_scale", move |s: Vec3| {
        c.lock().unwrap().transform.scale = s;
    });

    // Rotations are right-handed, in radians, pre-multiplied onto the current
    // orientation (so repeated calls accumulate spin).
    let c = ctx.clone();
    engine.register_fn("rotate_x", move |angle: f32| {
        let mut g = c.lock().unwrap();
        g.transform.rotation = Quat::from_rotation_x(angle) * g.transform.rotation;
    });
    let c = ctx.clone();
    engine.register_fn("rotate_y", move |angle: f32| {
        let mut g = c.lock().unwrap();
        g.transform.rotation = Quat::from_rotation_y(angle) * g.transform.rotation;
    });
    let c = ctx.clone();
    engine.register_fn("rotate_z", move |angle: f32| {
        let mut g = c.lock().unwrap();
        g.transform.rotation = Quat::from_rotation_z(angle) * g.transform.rotation;
    });

    let c = ctx.clone();
    engine.register_fn("dt", move || c.lock().unwrap().dt);
}

/// Registers the **world API**: the [`Entity`] handle type plus the ambient
/// functions a script uses to spawn/despawn entities and add, edit, or remove
/// their components. Every mutation is recorded as a [`ScriptCommand`] in the
/// shared context for the host to apply after the script returns.
fn register_world_api(engine: &mut Engine, ctx: &SharedContext) {
    engine
        .register_type_with_name::<EntityHandle>("Entity")
        .register_fn("to_string", |e: &mut EntityHandle| match e {
            EntityHandle::Real(ent) => format!("Entity({})", ent.to_bits()),
            EntityHandle::Provisional(id) => format!("Entity(new#{id})"),
        });

    // The entity this script runs on. Provisional(0) is never produced by
    // `spawn` (its counter starts at 1), so it reads as an unset placeholder if
    // the host forgot to stage a current entity — which never happens in normal
    // operation.
    let c = ctx.clone();
    engine.register_fn("entity", move || {
        c.lock()
            .unwrap()
            .current
            .map(EntityHandle::Real)
            .unwrap_or(EntityHandle::Provisional(0))
    });

    // spawn_entity() / spawn_entity(name) -> a provisional Entity, created on
    // apply. (`spawn` is a reserved word in `rhai`, hence the longer name.)
    let c = ctx.clone();
    engine.register_fn("spawn_entity", move || spawn(&c, "Entity"));
    let c = ctx.clone();
    engine.register_fn("spawn_entity", move |name: &str| spawn(&c, name));

    let c = ctx.clone();
    engine.register_fn("despawn", move |target: EntityHandle| {
        c.lock()
            .unwrap()
            .commands
            .push(ScriptCommand::Despawn { target });
    });

    let c = ctx.clone();
    engine.register_fn(
        "add_component",
        move |target: EntityHandle, type_name: &str| {
            c.lock()
                .unwrap()
                .commands
                .push(ScriptCommand::AddComponent {
                    target,
                    type_name: type_name.to_string(),
                });
        },
    );

    let c = ctx.clone();
    engine.register_fn(
        "set_component",
        move |target: EntityHandle, type_name: &str, ron: &str| {
            c.lock()
                .unwrap()
                .commands
                .push(ScriptCommand::SetComponent {
                    target,
                    type_name: type_name.to_string(),
                    ron: ron.to_string(),
                });
        },
    );

    let c = ctx.clone();
    engine.register_fn(
        "remove_component",
        move |target: EntityHandle, type_name: &str| {
            c.lock()
                .unwrap()
                .commands
                .push(ScriptCommand::RemoveComponent {
                    target,
                    type_name: type_name.to_string(),
                });
        },
    );
}

/// Allocates a fresh provisional id, records a [`ScriptCommand::Spawn`], and
/// hands the provisional handle back to the script.
fn spawn(ctx: &SharedContext, name: &str) -> EntityHandle {
    let mut g = ctx.lock().unwrap();
    g.next_provisional += 1;
    let provisional = g.next_provisional;
    g.commands.push(ScriptCommand::Spawn {
        provisional,
        name: name.to_string(),
    });
    EntityHandle::Provisional(provisional)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{ScriptAsset, ScriptEngine};

    /// A throwaway entity id for unit tests that exercise the context directly
    /// without a real scene.
    fn dummy_entity() -> Entity {
        // hecs packs a nonzero generation in the high 32 bits; id 0, generation 1.
        Entity::from_bits(1 << 32).expect("valid entity bits")
    }

    #[test]
    fn a_script_can_read_and_translate_the_transform() {
        let engine = ScriptEngine::new();
        engine.set_context(
            dummy_entity(),
            Transform::from_translation(Vec3::new(1.0, 0.0, 0.0)),
            0.5,
        );

        let compiled = engine
            .compile(&ScriptAsset::from_source(
                "move",
                "translate(vec3(2.0, 0.0, 0.0)); set_position(position() + vec3(0.0, dt(), 0.0));",
            ))
            .unwrap();
        engine.run(&compiled).unwrap();

        let t = engine.take_transform();
        assert!((t.translation.x - 3.0).abs() < 1e-6);
        assert!((t.translation.y - 0.5).abs() < 1e-6); // dt was 0.5
    }

    #[test]
    fn world_calls_buffer_commands_for_the_host() {
        let engine = ScriptEngine::new();
        engine.set_context(dummy_entity(), Transform::IDENTITY, 0.016);

        // spawn returns a provisional handle the script can configure at once.
        let compiled = engine
            .compile(&ScriptAsset::from_source(
                "spawner",
                r#"
                let e = spawn_entity("Bullet");
                set_component(e, "MeshRenderer", "()");
                add_component(entity(), "Marker");
                despawn(e);
                "#,
            ))
            .unwrap();
        engine.run(&compiled).unwrap();

        let cmds = engine.take_commands();
        assert_eq!(cmds.len(), 4, "four world calls were buffered");
        match &cmds[0] {
            ScriptCommand::Spawn { provisional, name } => {
                assert_eq!(provisional, &1);
                assert_eq!(name, "Bullet");
            }
            other => panic!("expected Spawn, got {other:?}"),
        }
        // The provisional id from spawn flows into the later set_component/despawn.
        assert!(matches!(
            &cmds[1],
            ScriptCommand::SetComponent { target: EntityHandle::Provisional(1), type_name, .. }
                if type_name == "MeshRenderer"
        ));
        assert!(matches!(
            &cmds[2],
            ScriptCommand::AddComponent { target: EntityHandle::Real(_), type_name }
                if type_name == "Marker"
        ));
        assert!(matches!(
            &cmds[3],
            ScriptCommand::Despawn {
                target: EntityHandle::Provisional(1)
            }
        ));

        // Draining cleared the buffer.
        assert!(engine.take_commands().is_empty());
    }
}
