use std::path::{Path, PathBuf};

use jni::{
    jni_sig, jni_str,
    objects::{JClass, JObject, JString, JThrowable, JValue},
};

use crate::{Error, Result, TaskMonitorOptions};

pub(crate) type JavaHandle = i64;

pub(crate) fn java_string(env: &mut jni::Env<'_>, value: JObject<'_>) -> Result<String> {
    let value = env.cast_local::<JString>(value)?;
    Ok(value.try_to_string(env)?)
}

pub(crate) fn java_optional_string(
    env: &mut jni::Env<'_>,
    value: JObject<'_>,
) -> Result<Option<String>> {
    if value.is_null() {
        return Ok(None);
    }
    Ok(Some(java_string(env, value)?))
}

pub(crate) fn bridge_jar_path() -> Result<PathBuf> {
    option_env!("GHIDRA_BRIDGE_JAR")
        .map(PathBuf::from)
        .ok_or_else(|| {
            Error::invalid_input(
                "Ghidra bridge jar was not built; set GHIDRA_INSTALL_DIR at build time",
            )
        })
}

pub(crate) fn path_string(path: &Path) -> String {
    path.display().to_string()
}

pub(crate) fn java_int(value: u64, field: &'static str) -> Result<i32> {
    i32::try_from(value)
        .map_err(|_| Error::invalid_input(format!("{field} must fit in a Java int")))
}

pub(crate) fn java_monitor_timeout_seconds(options: &TaskMonitorOptions) -> Result<i32> {
    match options.timeout_seconds()? {
        Some(seconds) => java_int(seconds, "task monitor timeout"),
        None => Ok(-1),
    }
}

pub(crate) fn jni_error(
    env: &mut jni::Env<'_>,
    operation: &'static str,
    error: jni::errors::Error,
) -> Error {
    if env.exception_check() {
        let Some(exception) = env.exception_occurred() else {
            return Error::Jni(error);
        };
        env.exception_clear();
        return java_exception(env, operation, exception).unwrap_or_else(|diagnostic_error| {
            Error::JavaException {
                operation,
                class_name: "java.lang.Throwable".to_string(),
                message: format!(
                    "failed to inspect Java exception: {diagnostic_error}; original JNI error: {error}"
                ),
                stack_trace: String::new(),
            }
        });
    }
    Error::Jni(error)
}

pub(crate) fn find_bridge_class<'local>(env: &mut jni::Env<'local>) -> Result<JClass<'local>> {
    let loader = env
        .call_static_method(
            jni_str!("java/lang/ClassLoader"),
            jni_str!("getSystemClassLoader"),
            jni_sig!("()Ljava/lang/ClassLoader;"),
            &[],
        )?
        .l()?;
    let name = env.new_string("org.ghidra.api.GhidraApiBridge")?;
    let name = JObject::from(name);
    let class = match env.call_static_method(
        jni_str!("java/lang/Class"),
        jni_str!("forName"),
        jni_sig!("(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;"),
        &[
            JValue::Object(&name),
            JValue::Bool(true),
            JValue::Object(&loader),
        ],
    ) {
        Ok(class) => class,
        Err(error) => {
            return Err(jni_error(
                env,
                "failed to load org.ghidra.api.GhidraApiBridge",
                error,
            ));
        }
    };
    env.cast_local::<JClass>(class.l()?).map_err(Error::from)
}

fn java_exception(
    env: &mut jni::Env<'_>,
    operation: &'static str,
    exception: JThrowable<'_>,
) -> Result<Error> {
    let class = env
        .call_method(
            &exception,
            jni_str!("getClass"),
            jni_sig!("()Ljava/lang/Class;"),
            &[],
        )?
        .l()?;
    let class_name = env
        .call_method(
            &class,
            jni_str!("getName"),
            jni_sig!("()Ljava/lang/String;"),
            &[],
        )?
        .l()?;
    let class_name = java_string(env, class_name)?;
    let message = env
        .call_method(
            &exception,
            jni_str!("getMessage"),
            jni_sig!("()Ljava/lang/String;"),
            &[],
        )?
        .l()?;
    let message = java_optional_string(env, message)?.unwrap_or_default();
    let stack_trace = stack_trace(env, &exception)?;
    Ok(Error::JavaException {
        operation,
        class_name,
        message,
        stack_trace,
    })
}

fn stack_trace(env: &mut jni::Env<'_>, exception: &JThrowable<'_>) -> Result<String> {
    let writer = env.new_object(jni_str!("java/io/StringWriter"), jni_sig!("()V"), &[])?;
    let print_writer = env.new_object(
        jni_str!("java/io/PrintWriter"),
        jni_sig!("(Ljava/io/Writer;)V"),
        &[JValue::Object(&writer)],
    )?;
    env.call_method(
        exception,
        jni_str!("printStackTrace"),
        jni_sig!("(Ljava/io/PrintWriter;)V"),
        &[JValue::Object(&print_writer)],
    )?;
    let stack_trace = env
        .call_method(
            &writer,
            jni_str!("toString"),
            jni_sig!("()Ljava/lang/String;"),
            &[],
        )?
        .l()?;
    java_string(env, stack_trace)
}
