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

use crate::{Result, runtime::GhidraRuntime};

use super::{
    lifecycle::attach_bridge,
    support::{JavaHandle, find_bridge_class, java_optional_string, java_string, jni_error},
};

pub(crate) fn instructions_for_function(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("instructionsForFunction"),
        "failed to list Ghidra function instructions",
    )
}

pub(crate) fn instructions_in_range(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    start: &str,
    end: &str,
) -> Result<String> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<String> {
        let start = env.new_string(start)?;
        let end = env.new_string(end)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("instructionsInRange"),
            jni_sig!("(JLjava/lang/String;Ljava/lang/String;)Ljava/lang/String;"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(start)),
                JValue::Object(&JObject::from(end)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => {
                return Err(jni_error(
                    env,
                    "failed to list Ghidra instructions in range",
                    error,
                ));
            }
        };
        java_string(env, value.l()?)
    })
}

pub(crate) fn instruction_at(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        address,
        jni_str!("instructionAt"),
        "failed to read Ghidra instruction at address",
    )
}

pub(crate) fn data_at(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        address,
        jni_str!("dataAt"),
        "failed to read Ghidra data at address",
    )
}

pub(crate) fn data_containing(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        address,
        jni_str!("dataContaining"),
        "failed to read Ghidra data containing address",
    )
}

fn string_string_operation(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    argument: &str,
    method: &'static jni::strings::JNIStr,
    context: &'static str,
) -> Result<String> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<String> {
        let argument = env.new_string(argument)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            method,
            jni_sig!("(JLjava/lang/String;)Ljava/lang/String;"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(argument)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, context, error)),
        };
        java_string(env, value.l()?)
    })
}

fn optional_string_string_operation(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    argument: &str,
    method: &'static jni::strings::JNIStr,
    context: &'static str,
) -> Result<Option<String>> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<Option<String>> {
        let argument = env.new_string(argument)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            method,
            jni_sig!("(JLjava/lang/String;)Ljava/lang/String;"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(argument)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, context, error)),
        };
        java_optional_string(env, value.l()?)
    })
}
