use super::Module;
use crate::{
    throw_string, Class, ClassID, ContextRef, Error, Result, TryIntoValue, Value, ValueRef,
};
use oden_js_sys as sys;
use std::collections::HashSet;
use std::ffi::CString;

/// A helper structure for declaring values. Use this in the implementation
/// of a NativeModule. (See the documentation for `NativeModule` for more
/// information.)
pub struct Declarations {
    declarations: HashSet<CString>,
}

impl Declarations {
    pub(crate) fn new() -> Self {
        Declarations {
            declarations: HashSet::new(),
        }
    }

    /// Declare that a native module will eventually export a value named
    /// `name`.
    pub fn declare<S>(&mut self, name: S) -> Result<&mut Self>
    where
        S: Into<Vec<u8>>,
    {
        self.declarations.insert(CString::new(name)?);
        Ok(self)
    }

    /// Apply all the declarations to the module. (Don't let user code see
    /// the JSModuleDef, right?)
    pub(crate) unsafe fn apply(self, ctx: &ContextRef, m: *mut sys::JSModuleDef) -> Result<()> {
        for k in self.declarations {
            let res = unsafe { sys::JS_AddModuleExport(ctx.ctx, m, k.into_raw()) };
            if res < 0 {
                return Err(Error::OutOfMemory);
            }
        }
        Ok(())
    }
}

struct Export {
    name: CString,
    value: Value,
}

/// A helper structure for exporting values. Use this in the implementation
/// of a NativeModule. (See the documentation for `NativeModule` for more
/// information.)
pub struct Exports<'ctx> {
    context: &'ctx ContextRef,
    exports: Vec<Export>,
}

impl<'ctx> Exports<'ctx> {
    pub(crate) fn new(context: &'ctx ContextRef) -> Self {
        Exports {
            context,
            exports: Vec::new(),
        }
    }

    /// Export a value named `name` from the current module. You *must* have
    /// provided the same name in a previous call to `Declarations::declare`.
    pub fn export<N: Into<Vec<u8>>, T: TryIntoValue>(
        &mut self,
        name: N,
        value: T,
    ) -> Result<&mut Self> {
        let name = CString::new(name.into())?;
        let value = value.try_into_value(&self.context)?;
        self.export_value(name, &value)
    }

    /// Export a value named `name` from the current module. You *must* have
    /// provided the same name in a previous call to `Declarations::declare`.
    pub fn export_value(&mut self, name: CString, value: &ValueRef) -> Result<&mut Self> {
        self.exports.push(Export {
            name,
            value: value.dup(self.context),
        });
        Ok(self)
    }

    /// Actually export the values in the module. (Don't let user code see
    /// the JSModuleDef!)
    pub(crate) fn apply(self, module: *mut sys::JSModuleDef) -> Result<()> {
        for export in self.exports {
            let name = export.name;
            let value = export.value;

            let res = unsafe {
                // Ownership of name is retained
                // Ownership of value is transfered.
                sys::JS_DupValue(self.context.ctx, value.val);
                sys::JS_SetModuleExport(self.context.ctx, module, name.as_ref().as_ptr(), value.val)
            };

            if res < 0 {
                return Err(Error::OutOfMemory);
            }
        }
        Ok(())
    }
}

/// Implement this trait to implement a JavaScript module in Rust.
///
/// JavaScript modules proceed in two phases.  First, we resolve all the
/// imports, and once the import graph has been determined, then "execute"
/// the body of the modules to actually generate values. We reflect these
/// two phases here, in the `declare` and `define` methods.
///
/// (You might find the `NativeModuleBuilder` structure easier to use.)
pub trait NativeModule {
    /// Phase 1: Declare all the names you're going to export. Call
    /// `declarations.declare` once for each value you will eventually export
    /// in phase 2.
    fn declare(&self, declarations: &mut Declarations) -> Result<()>;

    /// Phase 2: Define all the values you're going to define. Call
    /// `exports.export` once for each value you declared in phase 1.
    fn define<'ctx>(&self, context: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()>;
}

struct NativeModuleState<T: NativeModule> {
    module: T,
}

impl<T: NativeModule> NativeModuleState<T> {
    fn new(module: T) -> Self {
        NativeModuleState { module }
    }

    fn define(context: &ContextRef, m: *mut sys::JSModuleDef) -> Result<()> {
        let import_meta =
            context.check_exception(unsafe { sys::JS_GetImportMeta(context.ctx, m) })?;
        let native_value = import_meta.get_property(context, "native_module")?;
        let native = Self::try_from_value(&native_value)?;

        let mut exports = Exports::new(context);
        native
            .module
            .define(context, &mut exports)
            .and_then(|_| exports.apply(m))
    }
}

impl<T: NativeModule> Class for NativeModuleState<T> {
    fn class_id() -> &'static ClassID {
        static ID: ClassID = ClassID::new("NativeModuleState");
        &ID
    }
}

unsafe extern "C" fn init_func<T: NativeModule>(
    ctx: *mut sys::JSContext,
    m: *mut sys::JSModuleDef,
) -> std::os::raw::c_int {
    let context = ContextRef::from_raw(ctx);
    match NativeModuleState::<T>::define(&context, m) {
        Ok(_) => 0,
        Err(Error::Exception(e, _, _)) => unsafe {
            // If we returned `Error::Exception` then we're propagating an
            // exception through the JS stack, just flip it.
            let exc = &e.val;
            sys::JS_DupValue(ctx, *exc);
            sys::JS_Throw(ctx, *exc);
            -1
        },
        Err(err) => {
            throw_string(&context, err.to_string());
            -1
        }
    }
}

/// Define a new native module, with the provided name and
/// implementation. Once this succeeds, the specified module is ready to be
/// consumed by javascript, although the `define` method will not be called
/// until the first time it gets successfully imported somewhere.
pub fn define_native_module<T: NativeModule>(
    ctx: &ContextRef,
    name: &str,
    module: T,
) -> Result<Module> {
    let c_name = CString::new(name)?;
    let m = unsafe { sys::JS_NewCModule(ctx.ctx, c_name.as_ptr(), Some(init_func::<T>)) };
    if m.is_null() {
        return Err(ctx.exception_error());
    }

    let mut declarations = Declarations::new();
    module
        .declare(&mut declarations)
        .and_then(|_| unsafe { declarations.apply(&ctx, m) })?;

    let native_value = NativeModuleState::new(module).into_value(ctx)?;
    let mut import_meta = ctx.check_exception(unsafe { sys::JS_GetImportMeta(ctx.ctx, m) })?;
    import_meta.set_property(ctx, "native_module", &native_value)?;

    Ok(Module::from_raw(m, ctx.get_runtime()))
}

struct GenericNativeModule {
    exports: Vec<Export>,
}

impl GenericNativeModule {
    fn new(exports: Vec<Export>) -> Self {
        GenericNativeModule { exports }
    }
}

impl NativeModule for GenericNativeModule {
    fn declare(&self, declarations: &mut Declarations) -> Result<()> {
        for e in self.exports.iter() {
            declarations.declare(e.name.clone())?;
        }
        Ok(())
    }

    fn define<'ctx>(&self, _: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()> {
        for e in self.exports.iter() {
            exports.export_value(e.name.clone(), &e.value)?;
        }
        Ok(())
    }
}

/// A helper to define a native module, by defining it export-by-export.
pub struct NativeModuleBuilder<'ctx> {
    exports: Exports<'ctx>,
}

impl<'ctx> NativeModuleBuilder<'ctx> {
    /// Construct a new native module builder, which will use the given
    /// context to define members.
    pub fn new(context: &'ctx ContextRef) -> Self {
        NativeModuleBuilder {
            exports: Exports::new(context),
        }
    }

    /// Define a new export that will be in the native module.
    pub fn export<N: Into<Vec<u8>>, T: TryIntoValue>(
        &mut self,
        name: N,
        value: T,
    ) -> Result<&mut Self> {
        self.exports.export(name, value)?;
        Ok(self)
    }

    /// Finish constructing the native module.
    pub fn build(&self, name: &str) -> Result<Module> {
        let context = self.exports.context;
        let mut exports = Vec::new();
        for export in self.exports.exports.iter() {
            exports.push(Export {
                name: export.name.clone(),
                value: export.value.dup(context),
            });
        }
        let generic_module = GenericNativeModule::new(exports);
        define_native_module(context, name, generic_module)
    }
}

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

    struct TestingNativeModule {}

    impl NativeModule for TestingNativeModule {
        fn declare(&self, declarations: &mut Declarations) -> Result<()> {
            declarations.declare("foo")?;
            Ok(())
        }

        fn define<'ctx>(&self, _: &'ctx ContextRef, exports: &mut Exports<'ctx>) -> Result<()> {
            exports.export("foo", 23)?;
            Ok(())
        }
    }

    #[test]
    fn test_define_native_module() {
        let runtime = Runtime::new();
        let context = Context::new(runtime);

        define_native_module(&context, "the_test", TestingNativeModule {})
            .expect("Module load should succeed");

        let js_module = context
            .eval_module(
                r#"
import { foo } from "the_test";
export const my_foo = foo;
"#,
                "test",
            )
            .expect("Evaluation of the test script should succeed");

        let my_foo = js_module
            .get_export(&context, "my_foo")
            .expect("Retrieving JS export should succeed");

        assert_eq!(my_foo.to_string(&context).unwrap(), String::from("23"));
    }

    #[test]
    fn test_native_module_builder() {
        let runtime = Runtime::new();
        let context = Context::new(runtime);

        NativeModuleBuilder::new(&context)
            .export("foo", 123)
            .expect("define should succeed")
            .export("bar", 321)
            .expect("define should succeed")
            .build("the_test")
            .expect("Module build should succeed");

        let js_module = context
            .eval_module(
                r#"
import { foo, bar } from "the_test";
export const my_foo = foo + bar;
"#,
                "test",
            )
            .expect("Evaluation of the test script should succeed");

        let my_foo = js_module
            .get_export(&context, "my_foo")
            .expect("Retrieving JS export should succeed");

        assert_eq!(my_foo.to_string(&context).unwrap(), String::from("444"));
    }
}
