//! `script_spin` — a runnable tour of the Stage 10 scripting + live-reload layer.
//!
//! Run with:
//! ```sh
//! cargo run -p oxide-examples --bin script_spin
//! ```
//!
//! This example has no window or GPU dependency. It writes a tiny `.rhai` script
//! that rotates an entity around Y every frame, attaches it via a [`Script`]
//! component, and steps the app — printing the entity's yaw so you can watch it
//! spin. Then, **without restarting**, it rewrites the script on disk to spin
//! three times faster and reloads it the way the editor's file watcher does; the
//! spin rate visibly jumps while the entity keeps its current orientation. That
//! is the Stage 10 live-reload promise in a headless harness.

use std::path::PathBuf;

use oxide_engine::prelude::*;
use oxide_engine::watch::{reload_changed_assets, ChangeEvent, ChangeKind};
use oxide_script::{Script, ScriptModule};

/// The initial script: a slow spin around Y, proportional to the frame delta.
const SLOW_SPIN: &str = r#"
// Rotate this entity around Y. `dt` is the frame time in seconds.
let speed = 1.0; // radians/second

fn init() {
    print("spin script started");
}

fn update(dt) {
    rotate_y(dt * 1.0);
}
"#;

/// The live edit: the same script, spinning three times faster.
const FAST_SPIN: &str = r#"
let speed = 3.0; // radians/second

fn update(dt) {
    rotate_y(dt * 3.0);
}
"#;

fn main() {
    env_logger::init();
    println!("Oxide scripting demo — Stage 10 live-reloaded spin\n");

    // A throwaway project directory holding `assets/scripts/spin.rhai`.
    let project = TempProject::new();
    let rel = "scripts/spin.rhai";
    project.write(rel, SLOW_SPIN);
    println!("wrote {rel}:\n{}", indent(SLOW_SPIN));

    // The asset database maps the file to a stable uid the Script references.
    let mut db = AssetDatabase::new(project.root());
    let uid = db.register(rel);

    let mut app = App::new();
    app.add_modules(DefaultModules);
    app.add_module(ScriptModule);
    app.insert_resource(db);

    // One entity that runs the script.
    let spinner = app.scene.spawn("spinner", Transform::IDENTITY);
    app.scene
        .world_mut()
        .insert_one(spinner, Script::new(AssetRef::new(uid)))
        .unwrap();

    let dt = 1.0 / 60.0;
    println!("\n-- spinning (script: 1.0 rad/s) --");
    run_seconds(&mut app, spinner, dt, 1.0);

    // Edit the script live: faster spin, no restart. This is exactly what the
    // editor does when the watcher sees the file change.
    project.write(rel, FAST_SPIN);
    let abs = project.abs(rel);
    let reloaded = reload_changed_assets(
        &app.assets,
        [ChangeEvent {
            path: abs,
            kind: ChangeKind::Modified,
        }],
    );
    println!("\n>> edited spin.rhai live (reloaded {reloaded} asset) -> 3.0 rad/s\n");

    println!("-- spinning (script: 3.0 rad/s, orientation preserved) --");
    run_seconds(&mut app, spinner, dt, 1.0);

    let final_yaw = yaw_of(&app, spinner);
    println!("\nfinal yaw = {final_yaw:.2} rad — the rate jumped without a restart.");
}

/// Steps the app for `seconds` of fixed `dt` frames, printing the spinner's yaw
/// roughly ten times so the rotation is visible.
fn run_seconds(app: &mut App, entity: Entity, dt: f32, seconds: f32) {
    let frames = (seconds / dt).round() as u32;
    let every = (frames / 10).max(1);
    for frame in 1..=frames {
        app.update(dt);
        if frame % every == 0 {
            println!(
                "  t = {:.2}s   yaw = {:.3} rad",
                frame as f32 * dt,
                yaw_of(app, entity)
            );
        }
    }
}

/// The entity's yaw (rotation about Y) in radians.
fn yaw_of(app: &App, entity: Entity) -> f32 {
    app.scene
        .local_transform(entity)
        .map(|t| t.rotation.to_euler(EulerRot::YXZ).0)
        .unwrap_or(0.0)
}

/// Indents a block of text two spaces for tidy console output.
fn indent(text: &str) -> String {
    text.trim_matches('\n')
        .lines()
        .map(|l| format!("    {l}"))
        .collect::<Vec<_>>()
        .join("\n")
}

/// A throwaway project directory, removed on drop.
struct TempProject {
    root: PathBuf,
}

impl TempProject {
    fn new() -> Self {
        let mut root = std::env::temp_dir();
        root.push(format!("oxide-script-spin-{}", std::process::id()));
        std::fs::create_dir_all(root.join("assets")).expect("create temp project");
        Self { root }
    }

    fn root(&self) -> &std::path::Path {
        &self.root
    }

    fn abs(&self, relative: &str) -> PathBuf {
        self.root.join("assets").join(relative)
    }

    fn write(&self, relative: &str, contents: &str) {
        let path = self.abs(relative);
        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
        std::fs::write(path, contents).unwrap();
    }
}

impl Drop for TempProject {
    fn drop(&mut self) {
        let _ = std::fs::remove_dir_all(&self.root);
    }
}
