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

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

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

pub(crate) fn list_functions(runtime: &GhidraRuntime, program: JavaHandle) -> Result<String> {
    string_operation(
        runtime,
        program,
        jni_str!("listFunctions"),
        "failed to list Ghidra functions",
    )
}

pub(crate) fn parse_address(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        address,
        jni_str!("parseAddress"),
        "failed to parse Ghidra address",
    )
}

pub(crate) fn function_at(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        address,
        jni_str!("functionAt"),
        "failed to find Ghidra function at address",
    )
}

pub(crate) fn function_containing(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        address,
        jni_str!("functionContaining"),
        "failed to find Ghidra function containing address",
    )
}

pub(crate) fn symbols(runtime: &GhidraRuntime, program: JavaHandle) -> Result<String> {
    string_operation(
        runtime,
        program,
        jni_str!("symbols"),
        "failed to list Ghidra symbols",
    )
}

pub(crate) fn symbols_at(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        address,
        jni_str!("symbolsAt"),
        "failed to list Ghidra symbols at address",
    )
}

pub(crate) fn find_symbols(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    name: &str,
    case_sensitive: bool,
) -> Result<String> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<String> {
        let name = env.new_string(name)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("findSymbols"),
            jni_sig!("(JLjava/lang/String;Z)Ljava/lang/String;"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(name)),
                JValue::Bool(case_sensitive),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, "failed to find Ghidra symbols", error)),
        };
        java_string(env, value.l()?)
    })
}

pub(crate) fn references_from(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        address,
        jni_str!("referencesFrom"),
        "failed to list Ghidra references from address",
    )
}

pub(crate) fn references_to(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        address,
        jni_str!("referencesTo"),
        "failed to list Ghidra references to address",
    )
}

pub(crate) fn memory_blocks(runtime: &GhidraRuntime, program: JavaHandle) -> Result<String> {
    string_operation(
        runtime,
        program,
        jni_str!("memoryBlocks"),
        "failed to list Ghidra memory blocks",
    )
}

pub(crate) fn control_flow_graph(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<String> {
    string_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("controlFlowGraph"),
        "failed to extract Ghidra control-flow graph",
    )
}

pub(crate) fn control_flow_graphs(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_addresses: &[&str],
) -> Result<String> {
    string_array_string_operation(
        runtime,
        program,
        function_addresses,
        jni_str!("controlFlowGraphs"),
        "failed to extract Ghidra control-flow graphs",
        "control-flow graph batch is too large",
    )
}

pub(crate) fn call_graph(runtime: &GhidraRuntime, program: JavaHandle) -> Result<String> {
    string_operation(
        runtime,
        program,
        jni_str!("callGraph"),
        "failed to extract Ghidra call graph",
    )
}

pub(crate) fn metadata(runtime: &GhidraRuntime, program: JavaHandle) -> Result<String> {
    string_operation(
        runtime,
        program,
        jni_str!("metadata"),
        "failed to extract Ghidra program metadata",
    )
}

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

pub(crate) fn function_summaries(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_addresses: &[&str],
) -> Result<String> {
    string_array_string_operation(
        runtime,
        program,
        function_addresses,
        jni_str!("functionSummaries"),
        "failed to summarize Ghidra functions",
        "function summary batch is too large",
    )
}

fn string_array_string_operation(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    strings: &[&str],
    method: &'static jni::strings::JNIStr,
    context: &'static str,
    too_large_message: &'static str,
) -> Result<String> {
    attach_bridge(runtime)?;
    let length =
        i32::try_from(strings.len()).map_err(|_| Error::invalid_input(too_large_message))?;
    runtime.with_attached(|env| -> Result<String> {
        let values = env.new_object_array(length, jni_str!("java/lang/String"), JObject::null())?;
        for (index, value) in strings.iter().enumerate() {
            let value = JObject::from(env.new_string(*value)?);
            values.set_element(env, index, &value)?;
        }
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            method,
            jni_sig!("(J[Ljava/lang/String;)Ljava/lang/String;"),
            &[JValue::Long(program), JValue::Object(values.as_ref())],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, context, error)),
        };
        java_string(env, value.l()?)
    })
}

pub(crate) fn basic_block_count(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<u64> {
    long_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("basicBlockCount"),
        "failed to count Ghidra function basic blocks",
    )
}

pub(crate) fn instruction_count(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<u64> {
    long_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("instructionCount"),
        "failed to count Ghidra function instructions",
    )
}

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

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

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

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

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

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

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

pub(crate) fn source_map(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("sourceMap"),
        "failed to read Ghidra function source map",
    )
}

pub(crate) fn start_transaction(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    description: &str,
) -> Result<JavaHandle> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<JavaHandle> {
        let description = env.new_string(description)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("startTransaction"),
            jni_sig!("(JLjava/lang/String;)J"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(description)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, "failed to start Ghidra transaction", error)),
        };
        Ok(value.j()?)
    })
}

pub(crate) fn end_transaction(
    runtime: &GhidraRuntime,
    transaction: JavaHandle,
    commit: bool,
) -> Result<()> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<()> {
        let class = find_bridge_class(env)?;
        match env.call_static_method(
            class,
            jni_str!("endTransaction"),
            jni_sig!("(JZ)V"),
            &[JValue::Long(transaction), JValue::Bool(commit)],
        ) {
            Ok(_) => Ok(()),
            Err(error) => Err(jni_error(env, "failed to end Ghidra transaction", error)),
        }
    })
}

pub(crate) fn set_function_plate_comment(
    runtime: &GhidraRuntime,
    transaction: JavaHandle,
    function_address: &str,
    comment: &str,
) -> Result<()> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<()> {
        let function_address = env.new_string(function_address)?;
        let comment = env.new_string(comment)?;
        let class = find_bridge_class(env)?;
        match env.call_static_method(
            class,
            jni_str!("setFunctionPlateComment"),
            jni_sig!("(JLjava/lang/String;Ljava/lang/String;)V"),
            &[
                JValue::Long(transaction),
                JValue::Object(&JObject::from(function_address)),
                JValue::Object(&JObject::from(comment)),
            ],
        ) {
            Ok(_) => Ok(()),
            Err(error) => Err(jni_error(
                env,
                "failed to set Ghidra function plate comment",
                error,
            )),
        }
    })
}

pub(crate) fn function_plate_comment(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    function_address: &str,
) -> Result<Option<String>> {
    optional_string_string_operation(
        runtime,
        program,
        function_address,
        jni_str!("functionPlateComment"),
        "failed to read Ghidra function plate comment",
    )
}

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

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()?)
    })
}

fn long_string_operation(
    runtime: &GhidraRuntime,
    program: JavaHandle,
    argument: &str,
    method: &'static jni::strings::JNIStr,
    context: &'static str,
) -> Result<u64> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<u64> {
        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;)J"),
            &[
                JValue::Long(program),
                JValue::Object(&JObject::from(argument)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, context, error)),
        };
        u64::try_from(value.j()?)
            .map_err(|_| Error::invalid_input("Java long result must not be negative"))
    })
}
