//! MATLAB-compatible `exist` builtin for RunMat.
//!
//! Mirrors MATLAB semantics for checking whether a variable, file, folder,
//! builtin, class, or other entity is available in the current session.

use runmat_builtins::{builtin_functions, lookup_method, Value};
use runmat_macros::runtime_builtin;

use crate::builtins::common::fs::contains_wildcards;
use crate::builtins::common::path_search::{
    class_file_exists as path_class_file_exists,
    class_folder_candidates as path_class_folder_candidates,
    directory_candidates as path_directory_candidates,
    find_file_with_extensions as path_find_file_with_extensions, path_is_directory,
    CLASS_M_FILE_EXTENSIONS, GENERAL_FILE_EXTENSIONS, LIB_EXTENSIONS, MEX_EXTENSIONS,
    PCODE_EXTENSIONS, SIMULINK_EXTENSIONS, THUNK_EXTENSIONS,
};
use crate::builtins::common::spec::{
    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
    ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};

const ERROR_NAME_ARG: &str = "exist: name must be a character vector or string scalar";
const ERROR_TYPE_ARG: &str = "exist: type must be a character vector or string scalar";
const ERROR_INVALID_TYPE: &str = "exist: invalid type. Type must be one of 'var', 'variable', 'file', 'dir', 'directory', 'folder', 'builtin', 'built-in', 'class', 'handle', 'method', 'mex', 'pcode', 'simulink', 'thunk', 'lib', 'library', or 'java'";

#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::exist")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
    name: "exist",
    op_kind: GpuOpKind::Custom("io"),
    supported_precisions: &[],
    broadcast: BroadcastSemantics::None,
    provider_hooks: &[],
    constant_strategy: ConstantStrategy::InlineLiteral,
    residency: ResidencyPolicy::GatherImmediately,
    nan_mode: ReductionNaN::Include,
    two_pass_threshold: None,
    workgroup_size: None,
    accepts_nan_mode: false,
    notes: "Filesystem and workspace lookup run on the host; arguments are gathered from the GPU when necessary.",
};

#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::exist")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
    name: "exist",
    shape: ShapeRequirements::Any,
    constant_strategy: ConstantStrategy::InlineLiteral,
    elementwise: None,
    reduction: None,
    emits_nan: false,
    notes: "I/O builtins are not eligible for fusion; metadata registered for completeness.",
};

const BUILTIN_NAME: &str = "exist";

fn exist_error(message: impl Into<String>) -> RuntimeError {
    build_runtime_error(message)
        .with_builtin(BUILTIN_NAME)
        .build()
}

fn map_control_flow(err: RuntimeError) -> RuntimeError {
    let identifier = err.identifier().map(str::to_string);
    let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
        .with_builtin(BUILTIN_NAME)
        .with_source(err);
    if let Some(identifier) = identifier {
        builder = builder.with_identifier(identifier);
    }
    builder.build()
}

#[runtime_builtin(
    name = "exist",
    category = "io/repl_fs",
    summary = "Determine whether a variable, file, folder, built-in, or class exists.",
    keywords = "exist,file,dir,var,builtin,class",
    accel = "cpu",
    type_resolver(crate::builtins::io::type_resolvers::exist_type),
    builtin_path = "crate::builtins::io::repl_fs::exist"
)]
async fn exist_builtin(name: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
    if rest.len() > 1 {
        return Err(exist_error("exist: too many input arguments"));
    }

    let name_host = gather_if_needed_async(&name)
        .await
        .map_err(map_control_flow)?;
    let type_value = match rest.first() {
        Some(value) => Some(
            gather_if_needed_async(value)
                .await
                .map_err(map_control_flow)?,
        ),
        None => None,
    };

    let query = type_value
        .as_ref()
        .map(parse_type_argument)
        .transpose()?
        .unwrap_or(ExistQuery::Any);

    let result = match query {
        ExistQuery::Handle => exist_handle(&name_host),
        _ => {
            let text = value_to_string(&name_host).ok_or_else(|| exist_error(ERROR_NAME_ARG))?;
            exist_for_query(&text, query).await?
        }
    };

    Ok(Value::Num(result.code()))
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ExistQuery {
    Any,
    Var,
    File,
    Dir,
    Builtin,
    Class,
    Mex,
    Pcode,
    Method,
    Handle,
    Simulink,
    Thunk,
    Lib,
    Java,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ExistResultKind {
    NotFound,
    Variable,
    File,
    Mex,
    Simulink,
    Builtin,
    Pcode,
    Directory,
    Class,
}

impl ExistResultKind {
    fn code(self) -> f64 {
        match self {
            ExistResultKind::NotFound => 0.0,
            ExistResultKind::Variable => 1.0,
            ExistResultKind::File => 2.0,
            ExistResultKind::Mex => 3.0,
            ExistResultKind::Simulink => 4.0,
            ExistResultKind::Builtin => 5.0,
            ExistResultKind::Pcode => 6.0,
            ExistResultKind::Directory => 7.0,
            ExistResultKind::Class => 8.0,
        }
    }
}

fn parse_type_argument(value: &Value) -> BuiltinResult<ExistQuery> {
    let text = value_to_string(value).ok_or_else(|| exist_error(ERROR_TYPE_ARG))?;
    match text.trim().to_ascii_lowercase().as_str() {
        "" => Ok(ExistQuery::Any),
        "var" | "variable" => Ok(ExistQuery::Var),
        "file" => Ok(ExistQuery::File),
        "dir" | "directory" | "folder" => Ok(ExistQuery::Dir),
        "builtin" | "built-in" => Ok(ExistQuery::Builtin),
        "class" => Ok(ExistQuery::Class),
        "mex" => Ok(ExistQuery::Mex),
        "pcode" | "p" => Ok(ExistQuery::Pcode),
        "handle" => Ok(ExistQuery::Handle),
        "method" => Ok(ExistQuery::Method),
        "simulink" => Ok(ExistQuery::Simulink),
        "thunk" => Ok(ExistQuery::Thunk),
        "lib" | "library" => Ok(ExistQuery::Lib),
        "java" => Ok(ExistQuery::Java),
        _ => Err(exist_error(ERROR_INVALID_TYPE)),
    }
}

async fn exist_for_query(name: &str, query: ExistQuery) -> BuiltinResult<ExistResultKind> {
    if contains_wildcards(name) {
        return Ok(ExistResultKind::NotFound);
    }

    match query {
        ExistQuery::Any => evaluate_default(name).await,
        ExistQuery::Var => Ok(if variable_exists(name) {
            ExistResultKind::Variable
        } else {
            ExistResultKind::NotFound
        }),
        ExistQuery::Builtin => Ok(if builtin_exists(name) {
            ExistResultKind::Builtin
        } else {
            ExistResultKind::NotFound
        }),
        ExistQuery::Class => Ok(if class_exists(name).await? {
            ExistResultKind::Class
        } else {
            ExistResultKind::NotFound
        }),
        ExistQuery::Dir => Ok(if directory_exists(name).await? {
            ExistResultKind::Directory
        } else {
            ExistResultKind::NotFound
        }),
        ExistQuery::File => Ok(detect_file_kind(name)
            .await?
            .unwrap_or(ExistResultKind::NotFound)),
        ExistQuery::Mex => Ok(
            if path_find_file_with_extensions(name, MEX_EXTENSIONS, "exist")
                .await
                .map_err(exist_error)?
                .is_some()
            {
                ExistResultKind::Mex
            } else {
                ExistResultKind::NotFound
            },
        ),
        ExistQuery::Pcode => Ok(
            if path_find_file_with_extensions(name, PCODE_EXTENSIONS, "exist")
                .await
                .map_err(exist_error)?
                .is_some()
            {
                ExistResultKind::Pcode
            } else {
                ExistResultKind::NotFound
            },
        ),
        ExistQuery::Method => Ok(if method_exists(name) {
            ExistResultKind::Builtin
        } else {
            ExistResultKind::NotFound
        }),
        ExistQuery::Simulink => Ok(
            if path_find_file_with_extensions(name, SIMULINK_EXTENSIONS, "exist")
                .await
                .map_err(exist_error)?
                .is_some()
            {
                ExistResultKind::Simulink
            } else {
                ExistResultKind::NotFound
            },
        ),
        ExistQuery::Thunk => Ok(
            if path_find_file_with_extensions(name, THUNK_EXTENSIONS, "exist")
                .await
                .map_err(exist_error)?
                .is_some()
            {
                ExistResultKind::File
            } else {
                ExistResultKind::NotFound
            },
        ),
        ExistQuery::Lib => Ok(
            if path_find_file_with_extensions(name, LIB_EXTENSIONS, "exist")
                .await
                .map_err(exist_error)?
                .is_some()
            {
                ExistResultKind::File
            } else {
                ExistResultKind::NotFound
            },
        ),
        ExistQuery::Java => Ok(ExistResultKind::NotFound),
        ExistQuery::Handle => unreachable!("handle queries handled separately"),
    }
}

async fn evaluate_default(name: &str) -> BuiltinResult<ExistResultKind> {
    if variable_exists(name) {
        return Ok(ExistResultKind::Variable);
    }
    if builtin_exists(name) {
        return Ok(ExistResultKind::Builtin);
    }
    if class_exists(name).await? {
        return Ok(ExistResultKind::Class);
    }
    if let Some(kind) = detect_file_kind(name).await? {
        return Ok(kind);
    }
    if directory_exists(name).await? {
        return Ok(ExistResultKind::Directory);
    }
    Ok(ExistResultKind::NotFound)
}

fn exist_handle(value: &Value) -> ExistResultKind {
    match value {
        Value::HandleObject(handle) => {
            if handle.valid {
                ExistResultKind::Variable
            } else {
                ExistResultKind::NotFound
            }
        }
        Value::Listener(listener) => {
            if listener.valid {
                ExistResultKind::Variable
            } else {
                ExistResultKind::NotFound
            }
        }
        _ => ExistResultKind::NotFound,
    }
}

fn variable_exists(name: &str) -> bool {
    crate::workspace::lookup(name).is_some()
}

fn builtin_exists(name: &str) -> bool {
    let lowered = name.to_ascii_lowercase();
    builtin_functions()
        .into_iter()
        .any(|b| b.name.eq_ignore_ascii_case(&lowered))
}

async fn class_exists(name: &str) -> BuiltinResult<bool> {
    if runmat_builtins::get_class(name).is_some() {
        return Ok(true);
    }
    if class_folder_exists(name).await? {
        return Ok(true);
    }
    if class_file_exists(name).await? {
        return Ok(true);
    }
    Ok(false)
}

async fn class_folder_exists(name: &str) -> BuiltinResult<bool> {
    for path in path_class_folder_candidates(name, "exist").map_err(exist_error)? {
        if path_is_directory(&path).await {
            return Ok(true);
        }
    }
    Ok(false)
}

async fn class_file_exists(name: &str) -> BuiltinResult<bool> {
    path_class_file_exists(name, CLASS_M_FILE_EXTENSIONS, "classdef", "exist")
        .await
        .map_err(exist_error)
}

fn method_exists(name: &str) -> bool {
    if let Some((class_name, method_name)) = split_method_name(name) {
        lookup_method(&class_name, &method_name).is_some()
    } else {
        false
    }
}

async fn directory_exists(name: &str) -> BuiltinResult<bool> {
    for path in path_directory_candidates(name, "exist").map_err(exist_error)? {
        if path_is_directory(&path).await {
            return Ok(true);
        }
    }
    Ok(false)
}

async fn detect_file_kind(name: &str) -> BuiltinResult<Option<ExistResultKind>> {
    if path_find_file_with_extensions(name, MEX_EXTENSIONS, "exist")
        .await
        .map_err(exist_error)?
        .is_some()
    {
        return Ok(Some(ExistResultKind::Mex));
    }
    if path_find_file_with_extensions(name, PCODE_EXTENSIONS, "exist")
        .await
        .map_err(exist_error)?
        .is_some()
    {
        return Ok(Some(ExistResultKind::Pcode));
    }
    if path_find_file_with_extensions(name, SIMULINK_EXTENSIONS, "exist")
        .await
        .map_err(exist_error)?
        .is_some()
    {
        return Ok(Some(ExistResultKind::Simulink));
    }
    if path_find_file_with_extensions(name, GENERAL_FILE_EXTENSIONS, "exist")
        .await
        .map_err(exist_error)?
        .is_some()
    {
        return Ok(Some(ExistResultKind::File));
    }
    Ok(None)
}

fn value_to_string(value: &Value) -> Option<String> {
    match value {
        Value::String(text) => Some(text.clone()),
        Value::CharArray(array) if array.rows == 1 => Some(array.data.iter().collect()),
        Value::StringArray(array) if array.data.len() == 1 => Some(array.data[0].clone()),
        _ => None,
    }
}

fn split_method_name(name: &str) -> Option<(String, String)> {
    let mut parts: Vec<&str> = name.split('.').collect();
    if parts.len() < 2 {
        return None;
    }
    let method = parts.pop()?.to_string();
    if method.is_empty() {
        return None;
    }
    let class = parts.join(".");
    if class.is_empty() {
        return None;
    }
    Some((class, method))
}

#[cfg(test)]
pub(crate) mod tests {
    use super::super::REPL_FS_TEST_LOCK;
    use super::*;
    use runmat_builtins::Value;
    use runmat_filesystem as vfs;
    use runmat_thread_local::runmat_thread_local;
    use std::cell::RefCell;
    use std::collections::HashMap;
    use std::env;
    use std::fs::File;
    use std::io::Write;
    use std::path::PathBuf;
    use tempfile::tempdir;

    fn exist_builtin(name: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
        futures::executor::block_on(super::exist_builtin(name, rest))
    }

    fn workspace_guard() -> std::sync::MutexGuard<'static, ()> {
        crate::workspace::test_guard()
    }

    fn test_guard() -> (
        std::sync::MutexGuard<'static, ()>,
        std::sync::MutexGuard<'static, ()>,
    ) {
        let workspace = workspace_guard();
        let fs_lock = REPL_FS_TEST_LOCK
            .lock()
            .unwrap_or_else(|poison| poison.into_inner());
        (workspace, fs_lock)
    }

    runmat_thread_local! {
        static TEST_WORKSPACE: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
    }

    fn ensure_test_resolver() {
        crate::workspace::register_workspace_resolver(crate::workspace::WorkspaceResolver {
            lookup: |name| TEST_WORKSPACE.with(|slot| slot.borrow().get(name).cloned()),
            snapshot: || {
                let mut entries: Vec<(String, Value)> =
                    TEST_WORKSPACE.with(|slot| slot.borrow().clone().into_iter().collect());
                entries.sort_by(|a, b| a.0.cmp(&b.0));
                entries
            },
            globals: || Vec::new(),
            assign: None,
            clear: None,
            remove: None,
        });
    }

    fn set_workspace(entries: &[(&str, Value)]) {
        TEST_WORKSPACE.with(|slot| {
            let mut map = slot.borrow_mut();
            map.clear();
            for (name, value) in entries {
                map.insert((*name).to_string(), value.clone());
            }
        });
    }

    struct DirGuard {
        original: PathBuf,
    }

    impl DirGuard {
        fn new() -> Self {
            let original = env::current_dir().expect("current dir");
            Self { original }
        }
    }

    impl Drop for DirGuard {
        fn drop(&mut self) {
            let _ = env::set_current_dir(&self.original);
        }
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_detects_workspace_variables() {
        let (_guard, _lock) = test_guard();
        ensure_test_resolver();
        set_workspace(&[("alpha", Value::Num(1.0))]);

        let value = exist_builtin(Value::from("alpha"), Vec::new()).expect("exist");
        assert_eq!(value, Value::Num(1.0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_detects_builtins() {
        let (_guard, _lock) = test_guard();

        let value = exist_builtin(Value::from("sin"), Vec::new()).expect("exist");
        assert_eq!(value, Value::Num(5.0));

        let builtin =
            exist_builtin(Value::from("sin"), vec![Value::from("builtin")]).expect("exist");
        assert_eq!(builtin, Value::Num(5.0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_detects_files_and_mex() {
        let (_guard, _lock) = test_guard();
        ensure_test_resolver();

        let temp = tempdir().expect("tempdir");
        let _guard = DirGuard::new();
        env::set_current_dir(temp.path()).expect("set temp");

        File::create("script.m").expect("create m-file");
        File::create("fastfft.mexw64").expect("create mex");

        let script =
            exist_builtin(Value::from("script"), vec![Value::from("file")]).expect("exist");
        assert_eq!(script, Value::Num(2.0));

        let mex = exist_builtin(Value::from("fastfft"), vec![Value::from("file")]).expect("exist");
        assert_eq!(mex, Value::Num(3.0));

        let mex_specific =
            exist_builtin(Value::from("fastfft"), vec![Value::from("mex")]).expect("exist");
        assert_eq!(mex_specific, Value::Num(3.0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_detects_directories() {
        let (_guard, _lock) = test_guard();

        let temp = tempdir().expect("tempdir");
        let _guard = DirGuard::new();
        env::set_current_dir(temp.path()).expect("set temp");
        futures::executor::block_on(vfs::create_dir_async("data")).expect("mkdir data");

        let dir = exist_builtin(Value::from("data"), vec![Value::from("dir")]).expect("exist");
        assert_eq!(dir, Value::Num(7.0));

        let any = exist_builtin(Value::from("data"), Vec::new()).expect("exist");
        assert_eq!(any, Value::Num(7.0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_detects_class_files_and_packages() {
        let (_guard, _lock) = test_guard();

        let temp = tempdir().expect("tempdir");
        let _guard = DirGuard::new();
        env::set_current_dir(temp.path()).expect("set temp");

        let mut file = File::create("Widget.m").expect("create class file");
        writeln!(
            file,
            "classdef Widget\n    methods\n        function obj = Widget()\n        end\n    end\nend"
        )
        .expect("write classdef");

        futures::executor::block_on(vfs::create_dir_all_async("+pkg/@Gizmo"))
            .expect("create package class folder");

        let widget =
            exist_builtin(Value::from("Widget"), vec![Value::from("class")]).expect("exist");
        assert_eq!(widget, Value::Num(8.0));

        let gizmo =
            exist_builtin(Value::from("pkg.Gizmo"), vec![Value::from("class")]).expect("exist pkg");
        assert_eq!(gizmo, Value::Num(8.0));
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_invalid_type_raises_error() {
        let (_guard, _lock) = test_guard();

        let err = exist_builtin(Value::from("foo"), vec![Value::from("unknown")])
            .expect_err("expected error");
        assert_eq!(err.message(), ERROR_INVALID_TYPE);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_errors_on_non_text_name() {
        let (_guard, _lock) = test_guard();

        let err = exist_builtin(Value::Num(5.0), Vec::new()).expect_err("expected error");
        assert_eq!(err.message(), ERROR_NAME_ARG);
    }

    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
    #[test]
    fn exist_handle_returns_zero_for_non_handle() {
        let (_guard, _lock) = test_guard();

        let value =
            exist_builtin(Value::Num(17.0), vec![Value::from("handle")]).expect("exist handle");
        assert_eq!(value, Value::Num(0.0));
    }
}
