use rhai::{Array, Engine, EvalAltResult, FLOAT, INT};

pub mod empty_module {
    use rhai::plugin::*;

    #[allow(non_snake_case)]
    #[export_module]
    pub mod EmptyModule {}
}

#[test]
fn empty_module_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::empty_module::EmptyModule);
    engine.register_static_module("Module::Empty", m.into());

    Ok(())
}

pub mod one_fn_module {
    use rhai::plugin::*;

    #[export_module]
    pub mod advanced_math {
        use rhai::FLOAT;
        pub fn get_mystic_number() -> FLOAT {
            42.0 as FLOAT
        }
    }
}

#[test]
fn one_fn_module_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::one_fn_module::advanced_math);
    engine.register_static_module("Math::Advanced", m.into());

    assert_eq!(
        engine.eval::<FLOAT>(r#"let m = Math::Advanced::get_mystic_number();m"#)?,
        42.0
    );
    Ok(())
}

pub mod one_fn_and_const_module {
    use rhai::plugin::*;

    #[export_module]
    pub mod advanced_math {
        use rhai::FLOAT;

        pub const MYSTIC_NUMBER: FLOAT = 42.0 as FLOAT;

        pub fn euclidean_distance(x1: FLOAT, y1: FLOAT, x2: FLOAT, y2: FLOAT) -> FLOAT {
            ((y2 - y1).abs().powf(2.0) + (x2 - x1).abs().powf(2.0)).sqrt()
        }
    }
}

#[test]
fn one_fn_and_const_module_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::one_fn_and_const_module::advanced_math);
    engine.register_static_module("Math::Advanced", m.into());

    assert_eq!(
        engine.eval::<FLOAT>(
            "
                let m = Math::Advanced::MYSTIC_NUMBER;
                let x = Math::Advanced::euclidean_distance(0.0, 1.0, 0.0, m);
                x
            "
        )?,
        41.0
    );
    Ok(())
}

pub mod raw_fn_str_module {
    use rhai::plugin::*;

    #[export_module]
    pub mod host_io {
        pub fn write_out_str(message: &str) -> bool {
            eprintln!("{message}");
            true
        }
    }
}

#[test]
fn raw_fn_str_module_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::raw_fn_str_module::host_io);
    engine.register_static_module("Host::IO", m.into());

    assert!(engine
        .eval::<bool>(r#"let x = Host::IO::write_out_str("hello world!"); x"#)
        .unwrap());
    Ok(())
}

pub mod mut_opaque_ref_module {
    use rhai::plugin::*;
    use rhai::INT;

    #[allow(dead_code)] // used inside `exported_module!`
    #[derive(Clone)]
    pub struct StatusMessage {
        os_code: Option<INT>,
        message: String,
        is_ok: bool,
    }

    #[export_module]
    pub mod host_msg {
        use super::{StatusMessage, INT};

        pub fn new_message(is_ok: bool, message: &str) -> StatusMessage {
            StatusMessage {
                is_ok,
                os_code: None,
                message: message.to_string(),
            }
        }

        pub fn new_os_message(is_ok: bool, os_code: INT) -> StatusMessage {
            StatusMessage {
                is_ok,
                os_code: Some(os_code),
                message: format!("OS Code {os_code}"),
            }
        }

        pub fn write_out_message(message: &mut StatusMessage) -> bool {
            eprintln!("{}", message.message);
            true
        }
    }
}

#[test]
fn mut_opaque_ref_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::mut_opaque_ref_module::host_msg);
    engine.register_static_module("Host::Msg", m.into());

    assert!(engine
        .eval::<bool>(
            r#"
                let success = "it worked";
                let message1 = Host::Msg::new_message(true, success);
                let ok1 = Host::Msg::write_out_message(message1);
                let message2 = Host::Msg::new_os_message(true, 0);
                let ok2 = Host::Msg::write_out_message(message2);
                ok1 && ok2
            "#
        )
        .unwrap());
    Ok(())
}

mod duplicate_fn_rename {
    use rhai::plugin::*;
    #[export_module]
    pub mod my_adds {
        use rhai::{FLOAT, INT};

        #[rhai_fn(name = "add")]
        pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        #[rhai_fn(name = "add")]
        pub fn add_int(i1: INT, i2: INT) -> INT {
            i1 + i2
        }
    }
}

#[test]
fn duplicate_fn_rename_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    engine.register_fn("get_mystic_number", || 42 as FLOAT);
    let m = rhai::exported_module!(crate::duplicate_fn_rename::my_adds);
    engine.register_static_module("Math::Advanced", m.into());

    let output_array = engine.eval::<Array>(
        "
            let fx = get_mystic_number();
            let fy = Math::Advanced::add(fx, 1.0);
            let ix = 42;
            let iy = Math::Advanced::add(ix, 1);
            [fy, iy]
        ",
    )?;
    assert_eq!(&output_array[0].as_float().unwrap(), &43.0);
    assert_eq!(&output_array[1].as_int().unwrap(), &43);
    Ok(())
}

mod multiple_fn_rename {
    use rhai::plugin::*;
    #[export_module]
    pub mod my_adds {
        use rhai::{FLOAT, INT};

        pub fn get_mystic_number() -> FLOAT {
            42.0
        }
        #[rhai_fn(name = "add", name = "+", name = "add_together")]
        pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2 * 2.0
        }

        #[rhai_fn(name = "add", name = "+", name = "add_together")]
        pub fn add_int(i1: INT, i2: INT) -> INT {
            i1 + i2 * 2
        }

        #[rhai_fn(name = "prop", get = "prop")]
        pub fn get_prop(x: FLOAT) -> FLOAT {
            x * 2.0
        }

        #[rhai_fn(name = "idx", index_get)]
        pub fn index(x: FLOAT, i: INT) -> FLOAT {
            x + (i as FLOAT)
        }
    }
}

#[test]
fn multiple_fn_rename_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::multiple_fn_rename::my_adds);
    engine.register_global_module(m.into());
    engine.set_fast_operators(false);

    let output_array = engine.eval::<Array>(
        "
            let fx = get_mystic_number();
            let fy1 = add(fx, 1.0);
            let fy2 = add_together(fx, 1.0);
            let fy3 = fx + 1.0;
            let p1 = fx.prop;
            let p2 = prop(fx);
            let idx1 = fx[1];
            let idx2 = idx(fx, 1);
            let ix = 42;
            let iy1 = add(ix, 1);
            let iy2 = add_together(ix, 1);
            let iy3 = ix + 1;
            [fy1, fy2, fy3, iy1, iy2, iy3, p1, p2, idx1, idx2]
        ",
    )?;
    assert_eq!(&output_array[0].as_float().unwrap(), &44.0);
    assert_eq!(&output_array[1].as_float().unwrap(), &44.0);
    assert_eq!(&output_array[2].as_float().unwrap(), &44.0);
    assert_eq!(&output_array[3].as_int().unwrap(), &44);
    assert_eq!(&output_array[4].as_int().unwrap(), &44);
    assert_eq!(&output_array[5].as_int().unwrap(), &44);
    assert_eq!(&output_array[6].as_float().unwrap(), &84.0);
    assert_eq!(&output_array[7].as_float().unwrap(), &84.0);
    assert_eq!(&output_array[8].as_float().unwrap(), &43.0);
    assert_eq!(&output_array[9].as_float().unwrap(), &43.0);
    Ok(())
}

#[allow(dead_code)] // used inside `export_module!`
mod export_by_prefix {
    use rhai::plugin::*;

    #[export_module(export_prefix = "foo_")]
    pub mod my_adds {
        use rhai::{FLOAT, INT};

        #[rhai_fn(name = "foo_add_f")]
        pub fn foo_add1(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        #[rhai_fn(name = "bar_add_i")]
        fn foo_add_int(i1: INT, i2: INT) -> INT {
            i1 + i2
        }

        #[rhai_fn(name = "foo_add_float2")]
        pub fn add_float2(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        pub fn foo_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        fn foo_n(i1: INT, i2: INT) -> INT {
            i1 + i2
        }

        pub fn bar_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }
    }
}

#[test]
fn export_by_prefix_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::export_by_prefix::my_adds);
    engine.register_static_module("Math::Advanced", m.into());

    let output_array = engine.eval::<Array>(
        "
            let ex = 41.0;
            let fx = Math::Advanced::foo_add_f(ex, 1.0);
            let gx = Math::Advanced::foo_m(41.0, 1.0);
            let ei = 41;
            let fi = Math::Advanced::bar_add_i(ei, 1);
            let gi = Math::Advanced::foo_n(41, 1);
            [fx, gx, fi, gi]
        ",
    )?;
    assert_eq!(&output_array[0].as_float().unwrap(), &42.0);
    assert_eq!(&output_array[1].as_float().unwrap(), &42.0);
    assert_eq!(&output_array[2].as_int().unwrap(), &42);
    assert_eq!(&output_array[3].as_int().unwrap(), &42);

    assert!(matches!(*engine.eval::<FLOAT>(
        "
            let ex = 41.0;
            let fx = Math::Advanced::foo_add_float2(ex, 1.0);
            fx
        ").unwrap_err(),
        EvalAltResult::ErrorFunctionNotFound(s, ..)
            if s == "Math::Advanced::foo_add_float2 (f64, f64)"));

    assert!(matches!(*engine.eval::<FLOAT>(
        "
            let ex = 41.0;
            let fx = Math::Advanced::bar_m(ex, 1.0);
            fx
        ").unwrap_err(),
        EvalAltResult::ErrorFunctionNotFound(s, ..)
            if s == "Math::Advanced::bar_m (f64, f64)"));

    Ok(())
}

#[allow(dead_code)] // used inside `export_module!`
mod export_all {
    use rhai::plugin::*;

    #[export_module(export_all)]
    pub mod my_adds {
        use rhai::{FLOAT, INT};

        #[rhai_fn(name = "foo_add_f")]
        pub fn add_float(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        #[rhai_fn(name = "foo_add_i")]
        fn add_int(i1: INT, i2: INT) -> INT {
            i1 + i2
        }

        #[rhai_fn(skip)]
        pub fn add_float2(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        pub fn foo_m(f1: FLOAT, f2: FLOAT) -> FLOAT {
            f1 + f2
        }

        fn foo_n(i1: INT, i2: INT) -> INT {
            i1 + i2
        }

        #[rhai_fn(skip)]
        fn foo_p(i1: INT, i2: INT) -> INT {
            i1 * i2
        }
    }
}

#[test]
fn export_all_test() -> Result<(), Box<EvalAltResult>> {
    let mut engine = Engine::new();
    let m = rhai::exported_module!(crate::export_all::my_adds);
    engine.register_static_module("Math::Advanced", m.into());

    let output_array = engine.eval::<Array>(
        "
            let ex = 41.0;
            let fx = Math::Advanced::foo_add_f(ex, 1.0);
            let gx = Math::Advanced::foo_m(41.0, 1.0);
            let ei = 41;
            let fi = Math::Advanced::foo_add_i(ei, 1);
            let gi = Math::Advanced::foo_n(41, 1);
            [fx, gx, fi, gi]
        ",
    )?;
    assert_eq!(&output_array[0].as_float().unwrap(), &42.0);
    assert_eq!(&output_array[1].as_float().unwrap(), &42.0);
    assert_eq!(&output_array[2].as_int().unwrap(), &42);
    assert_eq!(&output_array[3].as_int().unwrap(), &42);

    assert!(matches!(*engine.eval::<INT>(
        "
            let ex = 41;
            let fx = Math::Advanced::foo_p(ex, 1);
            fx
        ").unwrap_err(),
        EvalAltResult::ErrorFunctionNotFound(s, ..)
            if s == "Math::Advanced::foo_p (i64, i64)"));

    Ok(())
}
