use crate::project::Project;

/// Enum used for the `demangling` option in `Config`.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Demangling {
    /// Don't try to demangle
    NoDemangling,
    /// Try to demangle using the C++ demangler (suitable for `Project`s containing C++ code).
    /// Names that fail to demangle will simply be printed as-is.
    CPP,
    /// Try to demangle using the Rust demangler (suitable for `Project`s containing Rust code).
    /// Names that fail to demangle will simply be printed as-is.
    Rust,
}

impl Demangling {
    /// Attempts to demangle the given function name, as appropriate based on the
    /// `Demangling` setting.
    //
    // (takes `self` by value because `self` is `Copy`)
    pub fn maybe_demangle(self, funcname: &str) -> String {
        match self {
            Demangling::NoDemangling => funcname.to_owned(),
            Demangling::CPP => cpp_demangle_or_id(funcname),
            Demangling::Rust => rust_demangle_or_id(funcname),
        }
    }

    /// Guesses an appropriate `Demangling` for the given `Project`.
    pub fn autodetect(proj: &Project) -> Self {
        // our autodetection is pretty unsophisticated right now,
        // but something is better than nothing

        // if any file in the `Project` comes from a source file
        // ending in `.rs`, then use Rust demangling.
        // Empirically, bitcode generated by `rustc` may have a "source
        // filename" ending in `cgu.0` instead (for example, our test file
        // `panic.rs` is compiled to a bitcode file with a "source filename"
        // of `panic.3a1fbbbh-cgu.0`), so also check for filenames ending in
        // `u.0`. (False positives aren't the end of the world, because any
        // symbols that aren't actually valid Rust symbols will be passed
        // through the demangler unchanged.)
        // TODO figure out a tighter test here, hopefully we can avoid both
        // false positives and false negatives.
        if proj
            .module_source_file_names()
            .any(|name| name.ends_with(".rs") || name.ends_with("u.0"))
        {
            return Demangling::Rust;
        }

        // otherwise, if any file in the `Project` comes from a source
        // file ending in `.cpp`, then use C++ demangling
        if proj
            .module_source_file_names()
            .any(|name| name.ends_with(".cpp"))
        {
            return Demangling::CPP;
        }

        // otherwise give up and don't try to demangle
        Demangling::NoDemangling
    }
}

/// Helper function to demangle function names with the C++ demangler.
///
/// Returns `Some` if successfully demangled, or `None` if any error occurs
/// (for instance, if `funcname` isn't a valid C++ mangled name)
pub(crate) fn try_cpp_demangle(funcname: &str) -> Option<String> {
    let opts = cpp_demangle::DemangleOptions { no_params: true };
    cpp_demangle::Symbol::new(funcname)
        .ok()
        .and_then(|sym| sym.demangle(&opts).ok())
}

/// Like `try_cpp_demangle()`, but just returns the input string unmodified in
/// the case of any error, rather than returning `None`.
pub(crate) fn cpp_demangle_or_id(funcname: &str) -> String {
    try_cpp_demangle(funcname).unwrap_or_else(|| funcname.to_owned())
}

/// Helper function to demangle function names with the Rust demangler.
///
/// Returns `Some` if successfully demangled, or `None` if any error occurs
/// (for instance, if `funcname` isn't a valid Rust mangled name)
pub(crate) fn try_rust_demangle(funcname: &str) -> Option<String> {
    rustc_demangle::try_demangle(funcname)
        .ok()
        .map(|demangled| format!("{:#}", demangled))
}

/// Like `try_rust_demangle()`, but just returns the input string unmodified in
/// the case of any error, rather than returning `None`.
pub(crate) fn rust_demangle_or_id(funcname: &str) -> String {
    format!("{:#}", rustc_demangle::demangle(funcname))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn autodetect() -> Result<(), String> {
        // A `Project` from a single C file
        let c_proj = Project::from_bc_path("tests/bcfiles/basic.bc")?;
        assert_eq!(Demangling::autodetect(&c_proj), Demangling::NoDemangling);

        // A `Project` from a single C++ file
        let cpp_proj = Project::from_bc_path("tests/bcfiles/throwcatch.bc")?;
        assert_eq!(Demangling::autodetect(&cpp_proj), Demangling::CPP);

        // A `Project` from a single Rust file
        let rust_proj = Project::from_bc_path("tests/bcfiles/panic.bc")?;
        assert_eq!(Demangling::autodetect(&rust_proj), Demangling::Rust);

        // A `Project` containing multiple C files
        let c_proj = Project::from_bc_paths(&[
            "tests/bcfiles/basic.bc",
            "tests/bcfiles/call.bc",
            "tests/bcfiles/globals.bc",
            "tests/bcfiles/simd.bc",
        ])?;
        assert_eq!(Demangling::autodetect(&c_proj), Demangling::NoDemangling);

        // A `Project` containing both C and Rust files
        let c_rust_proj = Project::from_bc_paths(&[
            "tests/bcfiles/basic.bc",
            "tests/bcfiles/call.bc",
            "tests/bcfiles/panic.bc",
        ])?;
        assert_eq!(Demangling::autodetect(&c_rust_proj), Demangling::Rust);

        // A `Project` containing both C and C++ files
        let c_cpp_proj = Project::from_bc_paths(&[
            "tests/bcfiles/basic.bc",
            "tests/bcfiles/call.bc",
            "tests/bcfiles/throwcatch.bc",
        ])?;
        assert_eq!(Demangling::autodetect(&c_cpp_proj), Demangling::CPP);

        Ok(())
    }
}
