// Functions for storing and retrieving function information. These functions also take care of
// autoloading functions in the $fish_function_path. Actual function evaluation is taken care of by
// the parser and to some degree the builtin handling library.

use crate::ast::{self, Node};
use crate::autoload::Autoload;
use crate::common::{assert_sync, escape, valid_func_name, FilenameRef};
use crate::complete::complete_wrap_map;
use crate::env::{EnvStack, Environment};
use crate::event::{self, EventDescription};
use crate::global_safety::RelaxedAtomicBool;
use crate::parse_tree::NodeRef;
use crate::parser::Parser;
use crate::parser_keywords::parser_keywords_is_reserved;
use crate::wchar::prelude::*;
use crate::wutil::{dir_iter::DirIter, sprintf};
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex};

#[derive(Clone)]
pub struct FunctionProperties {
    /// Reference to the node, along with the parsed source.
    pub func_node: NodeRef<ast::BlockStatement>,

    /// List of all named arguments for this function.
    pub named_arguments: Vec<WString>,

    /// Description of the function.
    pub description: LocalizableString,

    /// Mapping of all variables that were inherited from the function definition scope to their
    /// values, as (key, values) pairs.
    pub inherit_vars: Box<[(WString, Vec<WString>)]>,

    /// Set to true if invoking this function shadows the variables of the underlying function.
    pub shadow_scope: bool,

    /// Whether the function was autoloaded.
    /// This is the only field which is mutated after the properties are created.
    pub is_autoload: RelaxedAtomicBool,

    /// The file from which the function was created, or None if not from a file.
    pub definition_file: Option<FilenameRef>,

    /// Whether the function was copied.
    pub is_copy: bool,

    /// The file from which the function was copied, or None if not from a file.
    pub copy_definition_file: Option<FilenameRef>,

    /// The 1-based line number where the specified function was copied.
    pub copy_definition_lineno: Option<NonZeroU32>,
}

/// FunctionProperties are safe to share between threads.
const _: () = assert_sync::<FunctionProperties>();

/// Type wrapping up the set of all functions.
/// There's only one of these; it's managed by a lock.
struct FunctionSet {
    /// The map of all functions by name.
    funcs: HashMap<WString, Arc<FunctionProperties>>,

    /// Tombstones for functions that should no longer be autoloaded.
    autoload_tombstones: HashSet<WString>,

    /// The autoloader for our functions.
    autoloader: Autoload,
}

impl FunctionSet {
    /// Remove a function.
    /// Return true if successful, false if it doesn't exist.
    fn remove(&mut self, name: &wstr) -> bool {
        if self.funcs.remove(name).is_some() {
            event::remove_function_handlers(name);
            true
        } else {
            false
        }
    }

    /// Get the properties for a function, or None if none.
    fn get_props(&self, name: &wstr) -> Option<Arc<FunctionProperties>> {
        self.funcs.get(name).cloned()
    }

    /// Return true if we should allow autoloading a given function.
    fn allow_autoload(&self, name: &wstr) -> bool {
        // Prohibit autoloading if we have a non-autoload (explicit) function, or if the function is
        // tombstoned.
        let props = self.get_props(name);
        let has_explicit_func =
            props.is_some_and(|p: Arc<FunctionProperties>| !p.is_autoload.load());
        let tombstoned = self.autoload_tombstones.contains(name);
        !has_explicit_func && !tombstoned
    }
}

/// The big set of all functions.
static FUNCTION_SET: Lazy<Mutex<FunctionSet>> = Lazy::new(|| {
    Mutex::new(FunctionSet {
        funcs: HashMap::new(),
        autoload_tombstones: HashSet::new(),
        autoloader: Autoload::new(L!("fish_function_path")),
    })
});

/// Make sure that if the specified function is a dynamically loaded function, it has been fully
/// loaded. Note this executes fish script code.
pub fn load(name: &wstr, parser: &Parser) -> bool {
    let mut path_to_autoload: Option<_> = None;
    // Note we can't autoload while holding the funcset lock.
    // Lock around a local region.
    {
        let mut funcset: std::sync::MutexGuard<FunctionSet> = FUNCTION_SET.lock().unwrap();
        if funcset.allow_autoload(name) {
            if let Some(path) = funcset
                .autoloader
                .resolve_command(name, EnvStack::globals())
            {
                path_to_autoload = Some(path);
            }
        }
    }

    // Release the lock and perform any autoload, then reacquire the lock and clean up.
    if let Some(path_to_autoload) = path_to_autoload.as_ref() {
        // Crucially, the lock is acquired after perform_autoload().
        Autoload::perform_autoload(path_to_autoload, parser);
        FUNCTION_SET
            .lock()
            .unwrap()
            .autoloader
            .mark_autoload_finished(name);
    }
    path_to_autoload.is_some()
}

/// Insert a list of all dynamically loaded functions into the specified list.
fn autoload_names(names: &mut HashSet<WString>, vars: &dyn Environment, get_hidden: bool) {
    let Some(path_var) = vars.get_unless_empty(L!("fish_function_path")) else {
        return;
    };
    let path_list = path_var.as_list();
    for ndir_str in path_list {
        let Ok(mut dir) = DirIter::new(ndir_str) else {
            continue;
        };
        while let Some(entry) = dir.next() {
            let Ok(entry) = entry else {
                continue;
            };
            let func: &WString = &entry.name;
            if !get_hidden && func.char_at(0) == '_' {
                continue;
            }
            let suffix: Option<usize> = func.chars().rposition(|x| x == '.');
            // We need a ".fish" *suffix*, it can't be the entire name.
            if let Some(suffix) = suffix {
                if suffix > 0 && entry.name.slice_from(suffix) == ".fish" {
                    // Also ignore directories.
                    if !entry.is_dir() {
                        let name = entry.name.slice_to(suffix).to_owned();
                        names.insert(name);
                    }
                }
            }
        }
    }
}

/// Add a function. This may mutate `props` to set is_autoload.
pub fn add(name: WString, props: Arc<FunctionProperties>) {
    let mut funcset = FUNCTION_SET.lock().unwrap();

    // Historical check. TODO: rationalize this.
    if name.is_empty() {
        return;
    }

    // Remove the old function.
    funcset.remove(&name);

    // Check if this is a function that we are autoloading.
    props
        .is_autoload
        .store(funcset.autoloader.autoload_in_progress(&name));

    // Create and store a new function.
    let existing = funcset.funcs.insert(name, props);
    assert!(
        existing.is_none(),
        "Function should not already be present in the table"
    );
}

/// Return the properties for a function, or None. This does not trigger autoloading.
pub fn get_props(name: &wstr) -> Option<Arc<FunctionProperties>> {
    if parser_keywords_is_reserved(name) {
        None
    } else {
        FUNCTION_SET.lock().unwrap().get_props(name)
    }
}

/// Return the properties for a function, or None, perhaps triggering autoloading.
pub fn get_props_autoload(name: &wstr, parser: &Parser) -> Option<Arc<FunctionProperties>> {
    if parser_keywords_is_reserved(name) {
        return None;
    }
    load(name, parser);
    get_props(name)
}

/// Returns true if the function named `cmd` exists.
/// This may autoload.
pub fn exists(cmd: &wstr, parser: &Parser) -> bool {
    if !valid_func_name(cmd) {
        return false;
    }
    get_props_autoload(cmd, parser).is_some()
}

/// Returns true if the function `cmd` either is loaded, or exists on disk in an autoload
/// directory.
pub fn exists_no_autoload(cmd: &wstr) -> bool {
    if !valid_func_name(cmd) {
        return false;
    }
    if parser_keywords_is_reserved(cmd) {
        return false;
    }
    let mut funcset = FUNCTION_SET.lock().unwrap();
    // Check if we either have the function, or it could be autoloaded.
    let tombstoned = funcset.autoload_tombstones.contains(cmd);
    if funcset.funcs.contains_key(cmd) || (!tombstoned && funcset.autoloader.can_autoload(cmd)) {
        return true;
    }

    let narrow = crate::common::wcs2string(cmd);
    if let Ok(cmdstr) = std::str::from_utf8(&narrow) {
        let cmd = "functions/".to_owned() + cmdstr + ".fish";
        crate::autoload::has_asset(&cmd)
    } else {
        false
    }
}

/// Remove the function with the specified name.
pub fn remove(name: &wstr) {
    let mut funcset = FUNCTION_SET.lock().unwrap();
    funcset.remove(name);
    // Prevent (re-)autoloading this function.
    funcset.autoload_tombstones.insert(name.to_owned());
}

// Return the body of a function (everything after the header, up to but not including the 'end').
fn get_function_body_source(props: &FunctionProperties) -> &wstr {
    // We want to preserve comments that the AST attaches to the header (#5285).
    // Take everything from the end of the header to the 'end' keyword.
    let Some(header_source) = props.func_node.header.try_source_range() else {
        return L!("");
    };
    let Some(end_kw_source) = props.func_node.end.try_source_range() else {
        return L!("");
    };
    let body_start = header_source.start as usize + header_source.length as usize;
    let body_end = end_kw_source.start as usize;
    assert!(
        body_start <= body_end,
        "end keyword should come after header"
    );
    props
        .func_node
        .parsed_source()
        .src
        .slice_to(body_end)
        .slice_from(body_start)
}

/// Sets the description of the function with the name \c name.
/// This triggers autoloading.
pub(crate) fn set_desc(name: &wstr, desc: WString, parser: &Parser) {
    load(name, parser);
    let mut funcset = FUNCTION_SET.lock().unwrap();
    if let Some(props) = funcset.funcs.get(name) {
        // Note the description is immutable, as it may be accessed on another thread, so we copy
        // the properties to modify it.
        let mut new_props = props.as_ref().clone();
        // Translations will only be available if the function description has been extracted into
        // the translation files available to fish.
        new_props.description = LocalizableString::from_external_source(desc);
        funcset.funcs.insert(name.to_owned(), Arc::new(new_props));
    }
}

/// Creates a new function using the same definition as the specified function. Returns true if copy
/// is successful.
pub fn copy(name: &wstr, new_name: WString, parser: &Parser) -> bool {
    let filename = parser.current_filename();
    let lineno = parser.get_lineno();

    let mut funcset = FUNCTION_SET.lock().unwrap();
    let Some(props) = funcset.get_props(name) else {
        // No such function.
        return false;
    };
    // Copy the function's props.
    let mut new_props = props.as_ref().clone();
    new_props.is_autoload.store(false);
    new_props.is_copy = true;
    new_props.copy_definition_file = filename;
    new_props.copy_definition_lineno = lineno;

    // Note this will NOT overwrite an existing function with the new name.
    // TODO: rationalize if this behavior is desired.
    funcset.funcs.entry(new_name).or_insert(Arc::new(new_props));
    return true;
}

/// Returns all function names.
///
/// \param get_hidden whether to include hidden functions, i.e. ones starting with an underscore.
pub fn get_names(get_hidden: bool, vars: &dyn Environment) -> Vec<WString> {
    let mut names = HashSet::<WString>::new();
    let funcset = FUNCTION_SET.lock().unwrap();
    autoload_names(&mut names, vars, get_hidden);
    for name in funcset.funcs.keys() {
        // Maybe skip hidden.
        if !get_hidden && (name.is_empty() || name.char_at(0) == '_') {
            continue;
        }
        names.insert(name.clone());
    }

    #[cfg(feature = "embed-data")]
    for name in crate::autoload::Asset::iter() {
        let Some(bname) = name.strip_prefix("functions/") else {
            continue;
        };
        if !get_hidden && (bname.is_empty() || bname.starts_with('_')) {
            continue;
        };
        let Some(fname) = bname.strip_suffix(".fish") else {
            continue;
        };
        names.insert(fname.into());
    }

    names.into_iter().collect()
}

/// Observes that fish_function_path has changed.
pub fn invalidate_path() {
    // Remove all autoloaded functions and update the autoload path.
    let mut funcset = FUNCTION_SET.lock().unwrap();
    funcset.funcs.retain(|_, props| !props.is_autoload.load());
    funcset.autoloader.clear();
}

impl FunctionProperties {
    /// Return true if this function is a copy.
    pub fn is_copy(&self) -> bool {
        self.is_copy
    }

    /// Return a reference to the function's definition file, or None if it was defined interactively or copied.
    pub fn definition_file(&self) -> Option<&wstr> {
        self.definition_file.as_ref().map(|f| f.as_utfstr())
    }

    /// Return a reference to the vars that this function has inherited from its definition scope.
    pub fn inherit_vars(&self) -> &[(WString, Vec<WString>)] {
        &self.inherit_vars
    }

    /// If this function is a copy, return a reference to the original definition file, or None if it was defined interactively or copied.
    pub fn copy_definition_file(&self) -> Option<&wstr> {
        self.copy_definition_file.as_ref().map(|f| f.as_utfstr())
    }

    /// Return the 1-based line number of the function's definition.
    pub fn definition_lineno(&self) -> i32 {
        // Return one plus the number of newlines at offsets less than the start of our function's
        // statement (which includes the header).
        // TODO: merge with line_offset_of_character_at_offset?
        let Some(source_range) = self.func_node.try_source_range() else {
            panic!("Function has no source range");
        };
        let func_start = source_range.start as usize;
        let source = &self.func_node.parsed_source().src;
        assert!(
            func_start <= source.char_count(),
            "function start out of bounds"
        );
        1 + source
            .slice_to(func_start)
            .chars()
            .filter(|&c| c == '\n')
            .count() as i32
    }

    /// If this function is a copy, return the original 1-based line number. Otherwise, return 0.
    pub fn copy_definition_lineno(&self) -> u32 {
        self.copy_definition_lineno
            .map(|val| val.get())
            .unwrap_or(0)
    }

    /// Return a definition of the function, annotated with properties like event handlers and wrap
    /// targets. This is to support the 'functions' builtin.
    /// Note callers must provide the function name, since the function does not know its own name.
    pub fn annotated_definition(&self, name: &wstr) -> WString {
        let mut out = WString::new();
        let desc = self.description.localize();
        let def = get_function_body_source(self);
        let handlers = event::get_function_handlers(name);

        out.push_str("function ");
        // Typically we prefer to specify the function name first, e.g. "function foo --description bar"
        // But if the function name starts with a -, we'll need to output it after all the options.
        let defer_function_name = name.char_at(0) == '-';
        if !defer_function_name {
            out.push_utfstr(&escape(name));
        }

        // Output wrap targets.
        if let Some(wrapped_cmds) = complete_wrap_map().get(name) {
            for wrapped_cmd in wrapped_cmds {
                out.push_str(" --wraps=");
                out.push_utfstr(&escape(wrapped_cmd));
            }
        }

        if !desc.is_empty() {
            out.push_str(" --description ");
            out.push_utfstr(&escape(desc));
        }

        if !self.shadow_scope {
            out.push_str(" --no-scope-shadowing");
        }

        for handler in handlers {
            let d = &handler.desc;
            match d {
                EventDescription::Signal { signal } => {
                    sprintf!(=> &mut out, " --on-signal %ls", signal.name());
                }
                EventDescription::Variable { name } => {
                    sprintf!(=> &mut out, " --on-variable %ls", name);
                }
                EventDescription::ProcessExit { pid } => {
                    let pid = pid.map(|p| p.get()).unwrap_or(0);
                    sprintf!(=> &mut out, " --on-process-exit %d", pid)
                }
                EventDescription::JobExit { pid, .. } => {
                    let pid = pid.map(|p| p.get()).unwrap_or(0);
                    sprintf!(=> &mut out, " --on-job-exit %d", pid);
                }
                EventDescription::CallerExit { .. } => {
                    out.push_str(" --on-job-exit caller");
                }
                EventDescription::Generic { param } => {
                    sprintf!(=> &mut out, " --on-event %ls", param);
                }
                EventDescription::Any => {
                    panic!("Unexpected event handler type");
                }
            };
        }

        let named = &self.named_arguments;
        if !named.is_empty() {
            for name in named {
                sprintf!(=> &mut out, " --argument-names %ls", name);
            }
        }

        // Output the function name if we deferred it.
        if defer_function_name {
            out.push_str(" -- ");
            out.push_utfstr(&escape(name));
        }

        // Output any inherited variables as `set -l` lines.
        for (name, values) in self.inherit_vars.iter() {
            // We don't know what indentation style the function uses,
            // so we do what fish_indent would.
            sprintf!(=> &mut out, "\n    set -l %ls", name);
            for arg in values {
                out.push(' ');
                out.push_utfstr(&escape(arg));
            }
        }
        out.push('\n');
        out.push_utfstr(def);

        // Append a newline before the 'end', unless there already is one there.
        if !def.ends_with('\n') {
            out.push('\n');
        }
        out.push_str("end\n");
        out
    }
}
