use std::path::Path;

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

use crate::{
    AnalysisOptions, AnalysisReport, ProgramLoadInfo, ProgramLoadOptions, ProgramPath, Result,
    runtime::GhidraRuntime,
};

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

pub(crate) fn open_or_create_project(
    runtime: &GhidraRuntime,
    project_location: &Path,
    project_name: &str,
) -> Result<JavaHandle> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<JavaHandle> {
        let project_location = env.new_string(path_string(project_location))?;
        let project_name = env.new_string(project_name)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("openOrCreateProject"),
            jni_sig!("(Ljava/lang/String;Ljava/lang/String;)J"),
            &[
                JValue::Object(&JObject::from(project_location)),
                JValue::Object(&JObject::from(project_name)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => {
                return Err(jni_error(
                    env,
                    "failed to open or create Ghidra project",
                    error,
                ));
            }
        };
        Ok(value.j()?)
    })
}

pub(crate) struct BridgeLoadedProgram {
    pub(crate) handle: JavaHandle,
    pub(crate) info: ProgramLoadInfo,
}

#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct BridgeLoadedProgramJson {
    handle: JavaHandle,
    info: ProgramLoadInfo,
}

pub(crate) fn open_program(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    path: &ProgramPath,
) -> Result<JavaHandle> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<JavaHandle> {
        let folder = env.new_string(path.folder())?;
        let name = env.new_string(path.name())?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("openProgram"),
            jni_sig!("(JLjava/lang/String;Ljava/lang/String;)J"),
            &[
                JValue::Long(project),
                JValue::Object(&JObject::from(folder)),
                JValue::Object(&JObject::from(name)),
            ],
        ) {
            Ok(value) => value,
            Err(error) => {
                return Err(jni_error(env, "failed to open Ghidra program", error));
            }
        };
        Ok(value.j()?)
    })
}

pub(crate) fn open_or_import_program(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    options: &ProgramLoadOptions,
) -> Result<BridgeLoadedProgram> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<BridgeLoadedProgram> {
        let binary = JObject::from(env.new_string(path_string(&options.binary))?);
        let folder = JObject::from(env.new_string(options.path.folder())?);
        let name = JObject::from(env.new_string(options.path.name())?);
        let loader = optional_string(env, options.loader.as_deref())?;
        let language = optional_string(env, options.language.as_deref())?;
        let compiler = optional_string(env, options.compiler.as_deref())?;
        let monitor_timeout_seconds = java_monitor_timeout_seconds(&options.monitor)?;
        let loader_args = string_array(
            env,
            options
                .loader_args
                .iter()
                .flat_map(|arg| [arg.name.as_str(), arg.value.as_str()]),
        )?;
        let loader_args = JObject::from(loader_args);
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("openOrImportProgram"),
            jni_sig!(
                "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;I)Ljava/lang/String;"
            ),
            &[
                JValue::Long(project),
                JValue::Object(&binary),
                JValue::Object(&folder),
                JValue::Object(&name),
                JValue::Object(&loader),
                JValue::Object(&language),
                JValue::Object(&compiler),
                JValue::Object(&loader_args),
                JValue::Int(monitor_timeout_seconds),
            ],
        ) {
            Ok(value) => value,
            Err(error) => {
                return Err(jni_error(
                    env,
                    "failed to open or import Ghidra program",
                    error,
                ));
            }
        };
        let json = java_string(env, value.l()?)?;
        let loaded = serde_json::from_str::<BridgeLoadedProgramJson>(&json).map_err(|source| {
            crate::Error::json("failed to parse Ghidra loaded program metadata", source)
        })?;
        Ok(BridgeLoadedProgram {
            handle: loaded.handle,
            info: loaded.info,
        })
    })
}

pub(crate) fn analyze_program(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    program: JavaHandle,
    options: AnalysisOptions,
) -> Result<AnalysisReport> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<AnalysisReport> {
        let mode = env.new_string(options.mode.as_bridge_str())?;
        let monitor_timeout_seconds = java_monitor_timeout_seconds(&options.monitor)?;
        let class = find_bridge_class(env)?;
        let value = match env.call_static_method(
            class,
            jni_str!("analyzeProgram"),
            jni_sig!("(JJLjava/lang/String;I)Ljava/lang/String;"),
            &[
                JValue::Long(project),
                JValue::Long(program),
                JValue::Object(&JObject::from(mode)),
                JValue::Int(monitor_timeout_seconds),
            ],
        ) {
            Ok(value) => value,
            Err(error) => return Err(jni_error(env, "failed to analyze Ghidra program", error)),
        };
        let json = java_string(env, value.l()?)?;
        serde_json::from_str(&json)
            .map_err(|source| crate::Error::json("failed to parse Ghidra analysis report", source))
    })
}

pub(crate) fn save_program(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    program: JavaHandle,
) -> Result<()> {
    program_operation(
        runtime,
        project,
        program,
        jni_str!("saveProgram"),
        "failed to save Ghidra program",
    )
}

fn optional_string<'local>(
    env: &mut jni::Env<'local>,
    value: Option<&str>,
) -> Result<JObject<'local>> {
    match value {
        Some(value) => Ok(JObject::from(env.new_string(value)?)),
        None => Ok(JObject::null()),
    }
}

fn string_array<'local, I>(
    env: &mut jni::Env<'local>,
    values: I,
) -> Result<jni::objects::JObjectArray<'local>>
where
    I: IntoIterator,
    I::Item: AsRef<str>,
{
    let values = values.into_iter().collect::<Vec<_>>();
    let array = env.new_object_array(
        values.len() as i32,
        jni_str!("java/lang/String"),
        JObject::null(),
    )?;
    for (index, value) in values.iter().enumerate() {
        let value = env.new_string(value.as_ref())?;
        array.set_element(env, index, &value)?;
    }
    Ok(array)
}

pub(crate) fn close_program(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    program: JavaHandle,
) -> Result<()> {
    program_operation(
        runtime,
        project,
        program,
        jni_str!("closeProgram"),
        "failed to close Ghidra program",
    )
}

pub(crate) fn close_project(runtime: &GhidraRuntime, project: JavaHandle) -> Result<()> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<()> {
        let class = find_bridge_class(env)?;
        match env.call_static_method(
            class,
            jni_str!("closeProject"),
            jni_sig!("(J)V"),
            &[JValue::Long(project)],
        ) {
            Ok(_) => Ok(()),
            Err(error) => Err(jni_error(env, "failed to close Ghidra project", error)),
        }
    })
}

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

fn program_operation(
    runtime: &GhidraRuntime,
    project: JavaHandle,
    program: JavaHandle,
    method: &'static jni::strings::JNIStr,
    context: &'static str,
) -> Result<()> {
    attach_bridge(runtime)?;
    runtime.with_attached(|env| -> Result<()> {
        let class = find_bridge_class(env)?;
        match env.call_static_method(
            class,
            method,
            jni_sig!("(JJ)V"),
            &[JValue::Long(project), JValue::Long(program)],
        ) {
            Ok(_) => Ok(()),
            Err(error) => Err(jni_error(env, context, error)),
        }
    })
}

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