//
// The main loop of the fish program.
/*
Copyright (C) 2005-2008 Axel Liljencrantz

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/

use fish::{
    ast,
    builtins::{
        error::Error,
        fish_indent, fish_key_reader,
        shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_CMD_UNKNOWN, VERSION_STRING_TEMPLATE},
    },
    common::{PACKAGE_NAME, PROFILING_ACTIVE, PROGRAM_NAME},
    env::{
        EnvMode, Statuses,
        config_paths::ConfigPaths,
        environment::{EnvStack, Environment as _, env_init},
    },
    eprintf, err_fmt,
    event::{self, Event},
    fds::heightenize_fd,
    flog::{self, activate_flog_categories_by_pattern, flog, flogf, set_flog_file_fd},
    fprintf, function,
    history::{self, start_private_mode},
    io::{FdOutputStream, IoChain, OutputStream},
    locale::set_libc_locales,
    nix::isatty,
    panic::panic_handler,
    parse_constants::{ParseErrorList, ParseTreeFlags},
    parse_tree::ParsedSource,
    parse_util::detect_parse_errors_in_ast,
    parser::{BlockType, CancelBehavior, Parser, ParserEnvSetMode},
    path::path_get_config,
    prelude::*,
    printf,
    proc::{
        Pid, get_login, is_interactive_session, mark_login, mark_no_exec, proc_init,
        set_interactive_session,
    },
    reader::{reader_exit_signal, reader_init, reader_read, term_copy_modes},
    signal::{signal_clear_cancel, signal_unblock_all},
    threads::{self},
    topic_monitor,
    wutil::waccess,
};
use fish_common::{escape, save_term_foreground_process_group};
use fish_widestring::{bytes2wcstring, osstr2wcstring, wcs2bytes};
use libc::{STDERR_FILENO, STDIN_FILENO};
use nix::{
    sys::resource::{UsageWho, getrusage},
    unistd::{AccessFlags, getpid},
};
use std::{
    env,
    ffi::{OsStr, OsString},
    fs::File,
    ops::ControlFlow,
    os::unix::prelude::*,
    path::Path,
    sync::{Arc, atomic::Ordering},
};

/// container to hold the options specified within the command line
#[derive(Default, Debug)]
struct FishCmdOpts {
    /// Future feature flags values string
    features: WString,
    /// File path for debug output.
    debug_output: Option<OsString>,
    /// File path for profiling output, or empty for none.
    profile_output: Option<OsString>,
    profile_startup_output: Option<OsString>,
    /// Commands to be executed in place of interactive shell.
    batch_cmds: Vec<OsString>,
    /// Commands to execute after the shell's config has been read.
    postconfig_cmds: Vec<OsString>,
    /// Whether to print rusage-self stats after execution.
    print_rusage_self: bool,
    /// Whether no-config is set.
    no_config: bool,
    /// Whether no-exec is set.
    no_exec: bool,
    /// Whether this is a login shell.
    is_login: bool,
    /// Whether this is an interactive session.
    is_interactive_session: bool,
    /// Whether to enable private mode.
    enable_private_mode: bool,
}

/// Return a timeval converted to milliseconds.
#[allow(clippy::unnecessary_cast)]
fn nix_tv_to_ms(tv: nix::sys::time::TimeVal) -> i64 {
    // milliseconds per second
    let mut ms = tv.tv_sec() as i64 * 1000;
    // microseconds per millisecond
    ms += tv.tv_usec() as i64 / 1000;
    ms
}

fn print_rusage_self() {
    // `getrusage` should never fail with this usage.
    // If it does, it suggests a non-POSIX-compliant OS.
    let usage = getrusage(UsageWho::RUSAGE_SELF).unwrap();
    #[allow(non_snake_case)]
    let rss_KiB = if cfg!(apple) {
        // Macs use bytes,
        // even though docs at https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrusage.2.html say otherwise.
        usage.max_rss() / 1024
    } else {
        usage.max_rss()
    };
    let user_time = nix_tv_to_ms(usage.user_time());
    let sys_time = nix_tv_to_ms(usage.system_time());
    let total_time = user_time + sys_time;
    let signals = usage.signals();

    eprintf!("  rusage self:\n");
    eprintf!("      user time: %s ms\n", sys_time.to_string());
    eprintf!("       sys time: %s ms\n", user_time.to_string());
    eprintf!("     total time: %s ms\n", total_time.to_string());
    eprintf!("        max rss: %s KiB\n", rss_KiB.to_string());
    eprintf!("        signals: %s\n", signals.to_string());
}

// Source the file config.fish in the given directory.
// Returns true if successful, false if not.
fn source_config_in_directory(parser: &mut Parser, dir: &wstr) -> bool {
    // If the config.fish file doesn't exist or isn't readable silently return. Fish versions up
    // thru 2.2.0 would instead try to source the file with stderr redirected to /dev/null to deal
    // with that possibility.
    //
    // This introduces a race condition since the readability of the file can change between this
    // test and the execution of the 'source' command. However, that is not a security problem in
    // this context so we ignore it.
    let config_pathname = dir.to_owned() + L!("/config.fish");
    let escaped_pathname = escape(dir) + L!("/config.fish");
    if waccess(&config_pathname, AccessFlags::R_OK).is_err() {
        flogf!(
            config,
            "not sourcing %s (not readable or does not exist)",
            escaped_pathname
        );
        return false;
    }
    flog!(config, "sourcing", escaped_pathname);

    let cmd: WString = L!("builtin source ").to_owned() + escaped_pathname.as_utfstr();

    parser.libdata_mut().within_fish_init = true;
    let _ = parser.eval(&cmd, &IoChain::new());
    parser.libdata_mut().within_fish_init = false;
    true
}

/// Parse init files. exec_path is the path of fish executable as determined by argv[0].
fn read_init(parser: &mut Parser, paths: &ConfigPaths) {
    use fish::autoload::Asset;
    let emfile = Asset::get("config.fish").expect("Embedded file not found");
    let src = bytes2wcstring(&emfile.data);
    parser.libdata_mut().within_fish_init = true;
    let fname: Arc<WString> = Arc::new(L!("embedded:config.fish").into());
    let ret = parser.eval_file_wstr(src, fname, &IoChain::new(), None);
    parser.libdata_mut().within_fish_init = false;
    if let Err(msg) = ret {
        eprintf!("%s", msg);
    }

    source_config_in_directory(parser, &osstr2wcstring(&paths.sysconf));

    // We need to get the configuration directory before we can source the user configuration file.
    // If path_get_config returns false then we have no configuration directory and no custom config
    // to load.
    if let Some(config_dir) = path_get_config() {
        source_config_in_directory(parser, &config_dir);
    }
}

fn run_command_list(parser: &mut Parser, cmds: &[OsString]) -> Result<(), libc::c_int> {
    let mut retval = Ok(());
    for cmd in cmds {
        let cmd_wcs = osstr2wcstring(cmd);

        let mut errors = ParseErrorList::new();
        let ast = ast::parse(&cmd_wcs, ParseTreeFlags::default(), Some(&mut errors));
        let errored = ast.errored() || {
            detect_parse_errors_in_ast(&ast, &cmd_wcs, Some(&mut errors)).is_err()
        };

        if !errored {
            // Construct a parsed source ref.
            let ps = Arc::new(ParsedSource::new(cmd_wcs, ast));
            let _ = parser.eval_parsed_source(&ps, &IoChain::new(), None, BlockType::Top);
            retval = Ok(());
        } else {
            let backtrace = parser.get_backtrace(&cmd_wcs, &errors);
            eprintf!("%s", backtrace);
            // XXX: Why is this the return for "unknown command"?
            retval = Err(STATUS_CMD_UNKNOWN);
        }
    }

    retval
}

fn fish_parse_opt(args: &mut [WString], opts: &mut FishCmdOpts) -> ControlFlow<i32, usize> {
    use fish_wgetopt::{ArgType::*, WGetopter, WOption, wopt};

    const RUSAGE_ARG: char = 1 as char;
    const PRINT_DEBUG_CATEGORIES_ARG: char = 2 as char;
    const PROFILE_STARTUP_ARG: char = 3 as char;

    const SHORT_OPTS: &wstr = L!("+hPilNnvc:C:p:d:f:D:o:");
    const LONG_OPTS: &[WOption<'static>] = &[
        wopt(L!("command"), RequiredArgument, 'c'),
        wopt(L!("init-command"), RequiredArgument, 'C'),
        wopt(L!("features"), RequiredArgument, 'f'),
        wopt(L!("debug"), RequiredArgument, 'd'),
        wopt(L!("debug-output"), RequiredArgument, 'o'),
        wopt(L!("debug-stack-frames"), RequiredArgument, 'D'),
        wopt(L!("interactive"), NoArgument, 'i'),
        wopt(L!("login"), NoArgument, 'l'),
        wopt(L!("no-config"), NoArgument, 'N'),
        wopt(L!("no-execute"), NoArgument, 'n'),
        wopt(L!("print-rusage-self"), NoArgument, RUSAGE_ARG),
        wopt(
            L!("print-debug-categories"),
            NoArgument,
            PRINT_DEBUG_CATEGORIES_ARG,
        ),
        wopt(L!("profile"), RequiredArgument, 'p'),
        wopt(L!("profile-startup"), RequiredArgument, PROFILE_STARTUP_ARG),
        wopt(L!("private"), NoArgument, 'P'),
        wopt(L!("help"), NoArgument, 'h'),
        wopt(L!("version"), NoArgument, 'v'),
    ];

    let mut shim_args: Vec<&wstr> = args.iter().map(|s| s.as_ref()).collect();
    let mut w = WGetopter::new(SHORT_OPTS, LONG_OPTS, &mut shim_args);
    while let Some(c) = w.next_opt() {
        match c {
            'c' => opts
                .batch_cmds
                .push(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
            'C' => opts
                .postconfig_cmds
                .push(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
            'd' => {
                activate_flog_categories_by_pattern(w.woptarg.unwrap());
                for cat in flog::categories::all_categories() {
                    if cat.enabled.load(Ordering::Relaxed) {
                        printf!("Debug enabled for category: %s\n", cat.name);
                    }
                }
            }
            'o' => opts.debug_output = Some(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
            'f' => opts.features = w.woptarg.unwrap().to_owned(),
            'h' => opts.batch_cmds.push("__fish_print_help fish".into()),
            'i' => opts.is_interactive_session = true,
            'l' => opts.is_login = true,
            'N' => {
                opts.no_config = true;
                // --no-config implies private mode, we won't be saving history
                opts.enable_private_mode = true;
            }
            'n' => opts.no_exec = true,
            RUSAGE_ARG => opts.print_rusage_self = true,
            PRINT_DEBUG_CATEGORIES_ARG => {
                let cats = flog::categories::all_categories();
                // Compute width of longest name.
                let mut name_width = 0;
                for cat in cats.iter() {
                    name_width = usize::max(name_width, cat.name.len());
                }
                // A little extra space.
                name_width += 2;
                for cat in cats.iter() {
                    let desc = cat.description.localize();
                    // this is left-justified
                    printf!("%-*s %s\n", name_width, cat.name, desc);
                }
                return ControlFlow::Break(0);
            }
            // "--profile" - this does not activate profiling right away,
            // rather it's done after startup is finished.
            'p' => opts.profile_output = Some(OsString::from_vec(wcs2bytes(w.woptarg.unwrap()))),
            PROFILE_STARTUP_ARG => {
                // With "--profile-startup" we immediately turn profiling on.
                opts.profile_startup_output =
                    Some(OsString::from_vec(wcs2bytes(w.woptarg.unwrap())));
                PROFILING_ACTIVE.store(true);
            }
            'P' => opts.enable_private_mode = true,
            'v' => {
                printf!(
                    "%s\n",
                    wgettext_fmt!(VERSION_STRING_TEMPLATE, PACKAGE_NAME, fish::BUILD_VERSION)
                );
                return ControlFlow::Break(0);
            }
            'D' => {
                // TODO: Option is currently useless.
                // Either remove it or make it work with flog.
            }
            '?' => {
                err_fmt!(Error::UNKNOWN_OPT, args[w.wopt_index - 1])
                    .cmd(L!("fish"))
                    .append_to_msg('\n')
                    .write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
                return ControlFlow::Break(1);
            }
            ':' => {
                err_fmt!(Error::MISSING_OPT_ARG, args[w.wopt_index - 1])
                    .cmd(L!("fish"))
                    .append_to_msg('\n')
                    .write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
                return ControlFlow::Break(1);
            }
            ';' => {
                err_fmt!(Error::UNEXP_OPT_ARG, args[w.wopt_index - 1])
                    .cmd(L!("fish"))
                    .append_to_msg('\n')
                    .write_to(&mut OutputStream::Fd(FdOutputStream::new(STDERR_FILENO)));
                return ControlFlow::Break(1);
            }
            _ => panic!("unexpected retval from WGetopter"),
        }
    }
    let optind = w.wopt_index;

    // If our command name begins with a dash that implies we're a login shell.
    opts.is_login |= args[0].char_at(0) == '-';

    // We are an interactive session if we have not been given an explicit
    // command or file to execute and stdin is a tty. Note that the -i or
    // --interactive options also force interactive mode.
    if opts.batch_cmds.is_empty() && optind == args.len() && isatty(STDIN_FILENO) {
        set_interactive_session(true);
    }

    ControlFlow::Continue(optind)
}

fn main() {
    // If we are called as "/path/to/fish_key_reader", become fish_key_reader.
    if let Some(name) = env::args_os().next() {
        let p = Path::new(&name).file_name().and_then(|x| x.to_str());
        if p == Some("fish_key_reader") {
            return fish_key_reader::main();
        } else if p == Some("fish_indent") {
            return fish_indent::main();
        }
    }
    PROGRAM_NAME.set(L!("fish")).unwrap();
    if !cfg!(small_main_stack) {
        panic_handler(throwing_main);
    } else {
        // Create a new thread with a decent stack size to be our main thread
        std::thread::scope(|scope| {
            scope.spawn(|| panic_handler(throwing_main));
        });
    }
}

fn throwing_main() -> i32 {
    let mut res = Err(STATUS_CMD_ERROR);

    signal_unblock_all();
    topic_monitor::topic_monitor_init();
    threads::init();

    // Safety: single-threaded.
    unsafe {
        set_libc_locales(/*log_ok=*/ false)
    };

    #[cfg(feature = "localize-messages")]
    fish::localization::initialize_localization();

    // Enable debug categories set in FISH_DEBUG.
    // This is in *addition* to the ones given via --debug.
    if let Some(debug_categories) = env::var_os("FISH_DEBUG") {
        let s = osstr2wcstring(debug_categories);
        activate_flog_categories_by_pattern(&s);
    }

    let mut args: Vec<WString> = env::args_os().map(osstr2wcstring).collect();
    let mut opts = FishCmdOpts::default();
    let mut my_optind = match fish_parse_opt(&mut args, &mut opts) {
        ControlFlow::Continue(optind) => optind,
        ControlFlow::Break(status) => return status,
    };

    // Direct any debug output right away.
    // --debug-output takes precedence, otherwise $FISH_DEBUG_OUTPUT is used.
    // PORTING: this is a slight difference from C++, we now skip reading the env var if the argument is an empty string
    if opts.debug_output.is_none() {
        opts.debug_output = env::var_os("FISH_DEBUG_OUTPUT");
    }

    if let Some(debug_path) = opts.debug_output {
        match File::options()
            .write(true)
            .truncate(true)
            .create(true)
            .open(&debug_path)
        {
            Ok(dbg_file) => {
                // Rust sets O_CLOEXEC by default
                // https://github.com/rust-lang/rust/blob/07438b0928c6691d6ee734a5a77823ec143be94d/library/std/src/sys/unix/fs.rs#L1059
                set_flog_file_fd(dbg_file.into_raw_fd());
            }
            Err(e) => {
                let debug_path_string = format!("{debug_path:?}");
                // TODO: should be translated
                eprintf!("Could not open file %s\n", debug_path_string);
                eprintf!("%s\n", e);
                return 1;
            }
        }
    }

    // No-exec is prohibited when in interactive mode.
    if opts.is_interactive_session && opts.no_exec {
        flog!(
            warning,
            wgettext!("Can not use the no-execute mode when running an interactive session")
        );
        opts.no_exec = false;
    }

    // Apply our options
    if opts.is_login {
        mark_login();
    }
    if opts.no_exec {
        mark_no_exec();
    }
    if opts.is_interactive_session {
        set_interactive_session(true);
    }
    if opts.enable_private_mode {
        start_private_mode(EnvStack::globals());
    }

    // Only save (and therefore restore) the fg process group if we are interactive. See issues
    // #197 and #1002.
    if is_interactive_session() {
        save_term_foreground_process_group();
    }

    // If we're not executing, there's no need to find the config.
    let config_paths = if !opts.no_exec {
        let config_paths = ConfigPaths::new();
        env_init(
            Some(&config_paths),
            /* do uvars */ !opts.no_config,
            /* default paths */ opts.no_config,
        );
        Some(config_paths)
    } else {
        None
    };

    // Set features early in case other initialization depends on them.
    // Start with the ones set in the environment, then those set on the command line (so the
    // command line takes precedence).
    if let Some(features_var) = EnvStack::globals().get(L!("fish_features")) {
        for s in features_var.as_list() {
            fish_feature_flags::set_from_string(s.as_utfstr());
        }
    }
    fish_feature_flags::set_from_string(opts.features.as_utfstr());
    proc_init();
    reader_init(true);

    // Construct the root parser!
    let env = EnvStack::globals().create_child(true /* dispatches_var_changes */);
    let parser = &mut Parser::new(env, CancelBehavior::Clear);
    parser.set_syncs_uvars(!opts.no_config);

    if !opts.no_exec && !opts.no_config {
        read_init(parser, config_paths.as_ref().unwrap());
    }

    if is_interactive_session() && opts.no_config && !opts.no_exec {
        // If we have no config, we default to the default key bindings.
        parser.set_one(
            L!("fish_key_bindings"),
            ParserEnvSetMode::new(EnvMode::UNEXPORT),
            L!("fish_default_key_bindings").to_owned(),
        );
        if function::exists(L!("fish_default_key_bindings"), parser) {
            let _ = run_command_list(parser, &[OsString::from("fish_default_key_bindings")]);
        }
    }

    // Re-read the terminal modes after config, it might have changed them.
    term_copy_modes();

    // Stomp the exit status of any initialization commands (issue #635).
    parser.set_last_statuses(Statuses::just(STATUS_CMD_OK));

    // TODO(MSRV>=1.88): feature(let_chains)
    if let Some(path) = &opts.profile_startup_output {
        if opts.profile_startup_output != opts.profile_output {
            parser.flush_profiling(path);
        }
    }

    PROFILING_ACTIVE.store(opts.profile_output.is_some());

    // Run post-config commands specified as arguments, if any.
    if !opts.postconfig_cmds.is_empty() {
        res = run_command_list(parser, &opts.postconfig_cmds);
    }

    // Clear signals in case we were interrupted (#9024).
    signal_clear_cancel();

    if !opts.batch_cmds.is_empty() {
        // Run the commands specified as arguments, if any.
        if get_login() {
            // Do something nasty to support OpenSUSE assuming we're bash. This may modify cmds.
            fish_xdm_login_hack_hack_hack_hack(&mut opts.batch_cmds, &args[my_optind..]);
        }

        // Pass additional args as $argv.
        // Note that we *don't* support setting argv[0]/$0, unlike e.g. bash.
        let list = &args[my_optind..];
        parser.set_var(
            L!("argv"),
            ParserEnvSetMode::default(),
            list.iter().map(|s| s.to_owned()).collect(),
        );
        res = run_command_list(parser, &opts.batch_cmds);
        parser.libdata_mut().exit_current_script = false;
    } else if my_optind == args.len() {
        // Implicitly interactive mode.
        if opts.no_exec && isatty(libc::STDIN_FILENO) {
            flog!(
                error,
                "no-execute mode enabled and no script given. Exiting"
            );
            // above line should always exit
            return libc::EXIT_FAILURE;
        }
        res = reader_read(parser, libc::STDIN_FILENO, &IoChain::new());
    } else {
        let filename = &args[my_optind];
        let n = wcs2bytes(filename);
        let path = OsStr::from_bytes(&n);
        my_optind += 1;
        // Rust sets cloexec by default, see above
        match File::open(path) {
            Err(e) => {
                flogf!(
                    error,
                    wgettext!("Error reading script file '%s':"),
                    path.to_string_lossy()
                );
                eprintf!("%s\n", e);
            }
            Ok(f) => {
                if let Ok(f) = heightenize_fd(f.into(), true).map(File::from) {
                    let list = &args[my_optind..];
                    parser.set_var(
                        L!("argv"),
                        ParserEnvSetMode::default(),
                        list.iter().map(|s| s.to_owned()).collect(),
                    );
                    let _filename_push = parser
                        .current_filename
                        .scoped_replace(Some(Arc::new(filename.to_owned())));
                    res = reader_read(parser, f.as_raw_fd(), &IoChain::new());
                    if res.is_err() {
                        flog!(
                            warning,
                            wgettext_fmt!("Error while reading file %s", path.to_string_lossy())
                        );
                    }
                }
            }
        }
    }

    let exit_status = if res.is_err() {
        STATUS_CMD_UNKNOWN
    } else {
        parser.last_status()
    };

    event::fire(
        parser,
        Event::process_exit(Pid::from_nix_pid_unchecked(getpid()), exit_status),
    );

    // Trigger any exit handlers.
    event::fire_generic(
        parser,
        L!("fish_exit").to_owned(),
        vec![exit_status.to_wstring()],
    );

    if let Some(profile_output) = opts.profile_output {
        parser.flush_profiling(&profile_output);
    }

    history::save_all();

    // If we deferred a fatal signal, re-raise it now so the parent sees WIFSIGNALED.
    let exit_sig = reader_exit_signal();
    if exit_sig != 0 {
        unsafe {
            libc::signal(exit_sig, libc::SIG_DFL);
            libc::raise(exit_sig);
        }
    }

    if opts.print_rusage_self {
        print_rusage_self();
    }

    exit_status
}

// https://github.com/fish-shell/fish-shell/issues/367
fn escape_single_quoted_hack_hack_hack_hack(s: &wstr) -> OsString {
    let mut result = OsString::with_capacity(s.len() + 2);
    result.push("\'");
    for c in s.chars() {
        // Escape backslashes and single quotes only.
        if matches!(c, '\\' | '\'') {
            result.push("\\");
        }
        result.push(c.to_string());
    }
    result.push("\'");
    result
}

fn fish_xdm_login_hack_hack_hack_hack(cmds: &mut [OsString], args: &[WString]) -> bool {
    if cmds.len() != 1 {
        return false;
    }

    let cmd = &cmds[0];
    if cmd == "exec \"${@}\"" || cmd == "exec \"$@\"" {
        // We're going to construct a new command that starts with exec, and then has the
        // remaining arguments escaped.
        let mut new_cmd = OsString::from("exec");
        for arg in &args[1..] {
            new_cmd.push(" ");
            new_cmd.push(escape_single_quoted_hack_hack_hack_hack(arg));
        }

        cmds[0] = new_cmd;
        true
    } else {
        false
    }
}
