use crate::{
    bindings::{
        metacall_clear, metacall_execution_path, metacall_load_from_file, metacall_load_from_memory,
    },
    cstring_enum,
    types::MetaCallLoaderError,
};
use std::{
    ffi::CString,
    fmt,
    os::raw::c_void,
    path::{Path, PathBuf},
    ptr::null_mut,
};

#[derive(Debug, Clone, Copy)]
pub enum Tag {
    C,
    Cobol,
    Crystal,
    CSharp,
    Dart,
    Deno,
    Extension,
    File,
    Java,
    Julia,
    JavaScript,
    JSM,
    Kind,
    LLVM,
    Lua,
    Mock,
    NodeJS,
    Python,
    Ruby,
    RPC,
    Rust,
    TypeScript,
    Wasm,
}

impl Tag {
    /// Returns the matching tag for a given file extension, or `None` if unsupported.
    pub fn from_extension(ext: &str) -> Option<Self> {
        match ext {
            "c" => Some(Tag::C),
            "cob" | "cbl" | "cpy" => Some(Tag::Cobol),
            "cr" => Some(Tag::Crystal),
            "cs" | "vb" => Some(Tag::CSharp),
            "dart" => Some(Tag::Dart),
            "java" => Some(Tag::Java),
            "jl" => Some(Tag::Julia),
            "js" | "mjs" | "cjs" | "node" => Some(Tag::NodeJS),
            "jsm" => Some(Tag::JSM),
            "lua" => Some(Tag::Lua),
            "mock" => Some(Tag::Mock),
            "py" => Some(Tag::Python),
            "rb" => Some(Tag::Ruby),
            "rs" => Some(Tag::Rust),
            "ts" | "jsx" | "tsx" => Some(Tag::TypeScript),
            "wat" | "wasm" => Some(Tag::Wasm),
            _ => None,
        }
    }

    /// Returns the matching tag for a package extension (compiled artifacts like .dll, .rlib).
    pub fn from_package_extension(ext: &str) -> Option<Self> {
        match ext {
            "dll" => Some(Tag::CSharp),
            "wasm" => Some(Tag::Wasm),
            "rlib" => Some(Tag::Rust),
            _ => None,
        }
    }
}

impl fmt::Display for Tag {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Tag::C => write!(f, "c"),
            Tag::Cobol => write!(f, "cob"),
            Tag::Crystal => write!(f, "cr"),
            Tag::CSharp => write!(f, "cs"),
            Tag::Dart => write!(f, "dart"),
            Tag::Deno => write!(f, "deno"),
            Tag::Extension => write!(f, "ext"),
            Tag::File => write!(f, "file"),
            Tag::Java => write!(f, "java"),
            Tag::Julia => write!(f, "jl"),
            Tag::JavaScript => write!(f, "js"),
            Tag::JSM => write!(f, "jsm"),
            Tag::Kind => write!(f, "kind"),
            Tag::LLVM => write!(f, "llvm"),
            Tag::Lua => write!(f, "lua"),
            Tag::Mock => write!(f, "mock"),
            Tag::NodeJS => write!(f, "node"),
            Tag::Python => write!(f, "py"),
            Tag::Ruby => write!(f, "rb"),
            Tag::RPC => write!(f, "rpc"),
            Tag::Rust => write!(f, "rs"),
            Tag::TypeScript => write!(f, "ts"),
            Tag::Wasm => write!(f, "wasm"),
        }
    }
}

/// A MetaCall Handle is basically a C Pointer, One can pass it to when loading a script
/// (pass it to [[from_single_file]] for example) to keep the script in a local scope related to
/// this specific [[Handle]]
/// [[Handle]] implements Send + Sync traits because we are sure 100% and guarantee developers that
/// it is thread-safe. The Handle is a read-only pointer so there is no problem here.
pub struct Handle(*mut c_void);

unsafe impl Send for Handle {}
unsafe impl Sync for Handle {}

impl Handle {
    pub fn new() -> Self {
        Self(null_mut())
    }

    pub fn as_mut_raw_ptr(&self) -> *mut c_void {
        self.0
    }
}

impl Default for Handle {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for Handle {
    fn drop(&mut self) {
        let result = unsafe { metacall_clear(self.0) };

        if result != 0 {
            // Log or handle the error as needed
            eprintln!("Error during cleanup, metacall_clear returned: {}", result);
        }
    }
}

/// Loads a file from a single file. Usage example: ...
/// ```
/// // A Nodejs path
/// metacall::load::from_single_file(Tag::NodeJS, "index.js", None).unwrap();
/// ```
pub fn from_single_file(
    tag: Tag,
    path: impl AsRef<Path>,
    handle: Option<&mut Handle>,
) -> Result<(), MetaCallLoaderError> {
    from_file(tag, [path], handle)
}

/// Loads a path from file. Usage example: ...
/// ```
/// // A Nodejs script
/// metacall::load::from_file(Tag::NodeJS, ["index.js", "main.js"], None).unwrap();
/// ```
pub fn from_file(
    tag: Tag,
    paths: impl IntoIterator<Item = impl AsRef<Path>>,
    handle: Option<&mut Handle>,
) -> Result<(), MetaCallLoaderError> {
    let c_tag = cstring_enum!(tag, MetaCallLoaderError)?;
    let mut c_path: CString;

    let mut new_paths: Vec<*const i8> = Vec::new();
    for path in paths.into_iter() {
        let path_as_pathbuf = PathBuf::from(path.as_ref());
        let path_as_str = path_as_pathbuf.to_str().unwrap();

        if !path_as_pathbuf.exists() {
            return Err(MetaCallLoaderError::FileNotFound(path_as_pathbuf));
        }
        if !path_as_pathbuf.is_file() {
            return Err(MetaCallLoaderError::NotAFileOrPermissionDenied(
                path_as_pathbuf,
            ));
        }

        c_path = cstring_enum!(path_as_str, MetaCallLoaderError)?;

        new_paths.push(c_path.as_ptr());
    }

    let handle_ref = match handle {
        Some(handle_ptr) => &mut handle_ptr.0,
        None => null_mut(),
    };

    if unsafe {
        metacall_load_from_file(
            c_tag.as_ptr(),
            new_paths.as_mut_ptr(),
            new_paths.len(),
            handle_ref,
        )
    } != 0
    {
        return Err(MetaCallLoaderError::FromFileFailure);
    }

    Ok(())
}

/// Loads a script from memory. Usage example: ...
/// ```
/// let script = "function greet() { return 'hi there!' }; module.exports = { greet };";
///
/// // A Nodejs script
/// metacall::load::from_memory(Tag::NodeJS, script, None).unwrap();
/// ```
pub fn from_memory(
    tag: Tag,
    script: impl ToString,
    handle: Option<&mut Handle>,
) -> Result<(), MetaCallLoaderError> {
    let script = script.to_string();
    let c_tag = cstring_enum!(tag, MetaCallLoaderError)?;
    let c_script = cstring_enum!(script, MetaCallLoaderError)?;

    let handle_ref = match handle {
        Some(handle_ptr) => &mut handle_ptr.0,
        None => null_mut(),
    };

    if unsafe {
        metacall_load_from_memory(
            c_tag.as_ptr(),
            c_script.as_ptr(),
            script.len() + 1,
            handle_ref,
        )
    } != 0
    {
        return Err(MetaCallLoaderError::FromMemoryFailure);
    }

    Ok(())
}

/// Sets the execution path for the given loader tag.
pub fn execution_path(tag: Tag, path: impl AsRef<Path>) -> Result<(), MetaCallLoaderError> {
    let c_tag = cstring_enum!(tag, MetaCallLoaderError)?;
    let c_path = cstring_enum!(path.as_ref().to_str().unwrap(), MetaCallLoaderError)?;

    if unsafe { metacall_execution_path(c_tag.as_ptr(), c_path.as_ptr()) } != 0 {
        return Err(MetaCallLoaderError::ExecutionPathFailure);
    }

    Ok(())
}
