use super::prelude::*;
use crate::common::{unescape_string, ScopeGuard, UnescapeFlags, UnescapeStringStyle};
use crate::complete::{complete_add_wrapper, complete_remove_wrapper, CompletionRequestOptions};
use crate::highlight::colorize;
use crate::highlight::highlight_shell;
use crate::nix::isatty;
use crate::operation_context::OperationContext;
use crate::parse_constants::ParseErrorList;
use crate::parse_util::parse_util_detect_errors_in_argument_list;
use crate::parse_util::{parse_util_detect_errors, parse_util_token_extent};
use crate::reader::{commandline_get_state, completion_apply_to_command_line};
use crate::wcstringutil::string_suffixes_string;
use crate::{
    common::str2wcstring,
    complete::{
        complete_add, complete_print, complete_remove, complete_remove_all, CompleteFlags,
        CompleteOptionType, CompletionMode,
    },
};
use libc::STDOUT_FILENO;

// builtin_complete_* are a set of rather silly looping functions that make sure that all the proper
// combinations of complete_add or complete_remove get called. This is needed since complete allows
// you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the
// complete_add function only accepts one short switch and one long switch.

/// Silly function.
fn builtin_complete_add2(
    cmd: &wstr,
    cmd_is_path: bool,
    short_opt: &wstr,
    gnu_opts: &[WString],
    old_opts: &[WString],
    result_mode: CompletionMode,
    condition: &[WString],
    comp: &wstr,
    desc: &wstr,
    flags: CompleteFlags,
) {
    for short_opt in short_opt.chars() {
        complete_add(
            cmd.to_owned(),
            cmd_is_path,
            WString::from(&[short_opt][..]),
            CompleteOptionType::Short,
            result_mode,
            condition.to_vec(),
            comp.to_owned(),
            desc.to_owned(),
            flags,
        );
    }

    for gnu_opt in gnu_opts {
        complete_add(
            cmd.to_owned(),
            cmd_is_path,
            gnu_opt.to_owned(),
            CompleteOptionType::DoubleLong,
            result_mode,
            condition.to_vec(),
            comp.to_owned(),
            desc.to_owned(),
            flags,
        );
    }

    for old_opt in old_opts {
        complete_add(
            cmd.to_owned(),
            cmd_is_path,
            old_opt.to_owned(),
            CompleteOptionType::SingleLong,
            result_mode,
            condition.to_vec(),
            comp.to_owned(),
            desc.to_owned(),
            flags,
        );
    }

    if old_opts.is_empty() && gnu_opts.is_empty() && short_opt.is_empty() {
        complete_add(
            cmd.to_owned(),
            cmd_is_path,
            WString::new(),
            CompleteOptionType::ArgsOnly,
            result_mode,
            condition.to_vec(),
            comp.to_owned(),
            desc.to_owned(),
            flags,
        );
    }
}

/// Sily function.
fn builtin_complete_add(
    cmds: &[WString],
    paths: &[WString],
    short_opt: &wstr,
    gnu_opt: &[WString],
    old_opt: &[WString],
    result_mode: CompletionMode,
    condition: &[WString],
    comp: &wstr,
    desc: &wstr,
    flags: CompleteFlags,
) {
    for cmd in cmds {
        builtin_complete_add2(
            cmd,
            false, /* not path */
            short_opt,
            gnu_opt,
            old_opt,
            result_mode,
            condition,
            comp,
            desc,
            flags,
        );
    }
    for path in paths {
        builtin_complete_add2(
            path,
            true, /* is path */
            short_opt,
            gnu_opt,
            old_opt,
            result_mode,
            condition,
            comp,
            desc,
            flags,
        );
    }
}

fn builtin_complete_remove_cmd(
    cmd: &WString,
    cmd_is_path: bool,
    short_opt: &wstr,
    gnu_opt: &[WString],
    old_opt: &[WString],
) {
    let mut removed = false;
    for s in short_opt.chars() {
        complete_remove(
            cmd.to_owned(),
            cmd_is_path,
            wstr::from_char_slice(&[s]),
            CompleteOptionType::Short,
        );
        removed = true;
    }

    for opt in old_opt {
        complete_remove(
            cmd.to_owned(),
            cmd_is_path,
            opt,
            CompleteOptionType::SingleLong,
        );
        removed = true;
    }

    for opt in gnu_opt {
        complete_remove(
            cmd.to_owned(),
            cmd_is_path,
            opt,
            CompleteOptionType::DoubleLong,
        );
        removed = true;
    }

    if !removed {
        // This means that all loops were empty.
        complete_remove_all(cmd.to_owned(), cmd_is_path, /*explicit=*/ true);
    }
}

fn builtin_complete_remove(
    cmds: &[WString],
    paths: &[WString],
    short_opt: &wstr,
    gnu_opt: &[WString],
    old_opt: &[WString],
) {
    for cmd in cmds {
        builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt);
    }

    for path in paths {
        builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt);
    }
}

fn builtin_complete_print(cmd: &wstr, streams: &mut IoStreams, parser: &Parser) {
    let repr = complete_print(cmd);

    // colorize if interactive
    if !streams.out_is_redirected && isatty(STDOUT_FILENO) {
        let mut colors = vec![];
        highlight_shell(&repr, &mut colors, &parser.context(), false, None);
        streams
            .out
            .append(str2wcstring(&colorize(&repr, &colors, parser.vars())));
    } else {
        streams.out.append(repr);
    }
}

/// Values used for long-only options.
const OPT_ESCAPE: char = '\x01';

/// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in
/// complete.rs for any heavy lifting.
pub fn complete(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
    let cmd = argv[0];
    let argc = argv.len();
    let mut result_mode = CompletionMode::default();
    let mut remove = false;
    let mut short_opt = WString::new();
    // todo!("these would be Vec<&wstr>");
    let mut gnu_opt = vec![];
    let mut old_opt = vec![];
    let mut subcommand = vec![];
    let mut comp = WString::new();
    let mut desc = WString::new();
    let mut condition = vec![];
    let mut do_complete = false;
    let mut do_complete_param = None;
    let mut cmd_to_complete = vec![];
    let mut path = vec![];
    let mut wrap_targets = vec![];
    let mut preserve_order = false;
    let mut unescape_output = true;

    const short_options: &wstr = L!(":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk");
    const long_options: &[WOption] = &[
        wopt(L!("exclusive"), ArgType::NoArgument, 'x'),
        wopt(L!("no-files"), ArgType::NoArgument, 'f'),
        wopt(L!("force-files"), ArgType::NoArgument, 'F'),
        wopt(L!("require-parameter"), ArgType::NoArgument, 'r'),
        wopt(L!("path"), ArgType::RequiredArgument, 'p'),
        wopt(L!("command"), ArgType::RequiredArgument, 'c'),
        wopt(L!("short-option"), ArgType::RequiredArgument, 's'),
        wopt(L!("long-option"), ArgType::RequiredArgument, 'l'),
        wopt(L!("old-option"), ArgType::RequiredArgument, 'o'),
        wopt(L!("subcommand"), ArgType::RequiredArgument, 'S'),
        wopt(L!("description"), ArgType::RequiredArgument, 'd'),
        wopt(L!("arguments"), ArgType::RequiredArgument, 'a'),
        wopt(L!("erase"), ArgType::NoArgument, 'e'),
        wopt(L!("unauthoritative"), ArgType::NoArgument, 'u'),
        wopt(L!("authoritative"), ArgType::NoArgument, 'A'),
        wopt(L!("condition"), ArgType::RequiredArgument, 'n'),
        wopt(L!("wraps"), ArgType::RequiredArgument, 'w'),
        wopt(L!("do-complete"), ArgType::OptionalArgument, 'C'),
        wopt(L!("help"), ArgType::NoArgument, 'h'),
        wopt(L!("keep-order"), ArgType::NoArgument, 'k'),
        wopt(L!("escape"), ArgType::NoArgument, OPT_ESCAPE),
    ];

    let mut have_x = false;

    let mut w = WGetopter::new(short_options, long_options, argv);
    while let Some(opt) = w.next_opt() {
        match opt {
            'x' => {
                result_mode.no_files = true;
                result_mode.requires_param = true;
                // Needed to print an error later;
                have_x = true;
            }
            'f' => {
                result_mode.no_files = true;
            }
            'F' => {
                result_mode.force_files = true;
            }
            'r' => {
                result_mode.requires_param = true;
            }
            'k' => {
                preserve_order = true;
            }
            'p' | 'c' => {
                if let Some(tmp) = unescape_string(
                    w.woptarg.unwrap(),
                    UnescapeStringStyle::Script(UnescapeFlags::SPECIAL),
                ) {
                    if opt == 'p' {
                        path.push(tmp);
                    } else {
                        cmd_to_complete.push(tmp);
                    }
                } else {
                    streams.err.append(wgettext_fmt!(
                        "%ls: Invalid token '%ls'\n",
                        cmd,
                        w.woptarg.unwrap()
                    ));
                    return Err(STATUS_INVALID_ARGS);
                }
            }
            'd' => {
                desc = w.woptarg.unwrap().to_owned();
            }
            'u' => {
                // This option was removed in commit 1911298 and is now a no-op.
            }
            'A' => {
                // This option was removed in commit 1911298 and is now a no-op.
            }
            's' => {
                let arg = w.woptarg.unwrap();
                short_opt.extend(arg.chars());
                if arg.is_empty() {
                    streams
                        .err
                        .append(wgettext_fmt!("%ls: -s requires a non-empty string\n", cmd,));
                    return Err(STATUS_INVALID_ARGS);
                }
            }
            'l' => {
                let arg = w.woptarg.unwrap();
                gnu_opt.push(arg.to_owned());
                if arg.is_empty() {
                    streams
                        .err
                        .append(wgettext_fmt!("%ls: -l requires a non-empty string\n", cmd,));
                    return Err(STATUS_INVALID_ARGS);
                }
            }
            'o' => {
                let arg = w.woptarg.unwrap();
                old_opt.push(arg.to_owned());
                if arg.is_empty() {
                    streams
                        .err
                        .append(wgettext_fmt!("%ls: -o requires a non-empty string\n", cmd,));
                    return Err(STATUS_INVALID_ARGS);
                }
            }
            'S' => {
                let arg = w.woptarg.unwrap();
                subcommand.push(arg.to_owned());
                if arg.is_empty() {
                    streams
                        .err
                        .append(wgettext_fmt!("%ls: -S requires a non-empty string\n", cmd,));
                    return Err(STATUS_INVALID_ARGS);
                }
            }
            'a' => {
                comp = w.woptarg.unwrap().to_owned();
            }
            'e' => remove = true,
            'n' => {
                condition.push(w.woptarg.unwrap().to_owned());
            }
            'w' => {
                wrap_targets.push(w.woptarg.unwrap().to_owned());
            }
            'C' => {
                do_complete = true;
                if let Some(s) = w.woptarg {
                    do_complete_param = Some(s.to_owned());
                }
            }
            OPT_ESCAPE => {
                unescape_output = false;
            }
            'h' => {
                builtin_print_help(parser, streams, cmd);
                return Ok(SUCCESS);
            }
            ':' => {
                builtin_missing_argument(parser, streams, cmd, argv[w.wopt_index - 1], true);
                return Err(STATUS_INVALID_ARGS);
            }
            '?' => {
                builtin_unknown_option(parser, streams, cmd, argv[w.wopt_index - 1], true);
                return Err(STATUS_INVALID_ARGS);
            }
            _ => panic!("unexpected retval from WGetopter"),
        }
    }

    if result_mode.no_files && result_mode.force_files {
        if !have_x {
            streams.err.append(wgettext_fmt!(
                BUILTIN_ERR_COMBO2,
                "complete",
                "'--no-files' and '--force-files'"
            ));
        } else {
            // The reason for us not wanting files is `-x`,
            // which is short for `-rf`.
            streams.err.append(wgettext_fmt!(
                BUILTIN_ERR_COMBO2,
                "complete",
                "'--exclusive' and '--force-files'"
            ));
        }
        return Err(STATUS_INVALID_ARGS);
    }

    if w.wopt_index != argc {
        // Use one left-over arg as the do-complete argument
        // to enable `complete -C "git check"`.
        if do_complete && do_complete_param.is_none() && argc == w.wopt_index + 1 {
            do_complete_param = Some(argv[argc - 1].to_owned());
        } else if !do_complete && cmd_to_complete.is_empty() && argc == w.wopt_index + 1 {
            // Or use one left-over arg as the command to complete
            cmd_to_complete.push(argv[argc - 1].to_owned());
        } else {
            streams
                .err
                .append(wgettext_fmt!(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd));
            builtin_print_error_trailer(parser, streams.err, cmd);
            return Err(STATUS_INVALID_ARGS);
        }
    }

    for condition_string in &condition {
        let mut errors = ParseErrorList::new();
        if parse_util_detect_errors(condition_string, Some(&mut errors), false).is_err() {
            for error in errors {
                let prefix = cmd.to_owned() + L!(": -n '") + &condition_string[..] + L!("': ");
                streams.err.append(error.describe_with_prefix(
                    condition_string,
                    &prefix,
                    parser.is_interactive(),
                    false,
                ));
                streams.err.push('\n');
            }
            return Err(STATUS_CMD_ERROR);
        }
    }

    if !comp.is_empty() {
        let mut prefix = WString::new();
        prefix.push_utfstr(cmd);
        prefix.push_str(": ");

        if let Err(err_text) = parse_util_detect_errors_in_argument_list(&comp, &prefix) {
            streams.err.append(wgettext_fmt!(
                "%ls: %ls: contains a syntax error\n",
                cmd,
                comp
            ));
            streams.err.append(err_text);
            streams.err.push('\n');
            return Err(STATUS_CMD_ERROR);
        }
    }

    if do_complete {
        let have_do_complete_param = do_complete_param.is_some();
        let do_complete_param = match do_complete_param {
            None => {
                // No argument given, try to use the current commandline.
                let commandline_state = commandline_get_state(true);
                if !commandline_state.initialized {
                    // This corresponds to using 'complete -C' in non-interactive mode.
                    // See #2361    .
                    builtin_missing_argument(parser, streams, cmd, L!("-C"), true);
                    return Err(STATUS_INVALID_ARGS);
                }
                commandline_state.text
            }
            Some(param) => param,
        };

        let (token, _) = parse_util_token_extent(&do_complete_param, do_complete_param.len());

        // Create a scoped transient command line, so that builtin_commandline will see our
        // argument, not the reader buffer.
        let saved_transient = parser
            .libdata_mut()
            .transient_commandline
            .replace(do_complete_param.clone());
        let _remove_transient = ScopeGuard::new((), |()| {
            parser.libdata_mut().transient_commandline = saved_transient;
        });

        // Prevent accidental recursion (see #6171).
        if !parser.libdata().builtin_complete_current_commandline {
            if !have_do_complete_param {
                parser.libdata_mut().builtin_complete_current_commandline = true;
            }

            let (mut comp, _needs_load) = crate::complete::complete(
                &do_complete_param,
                CompletionRequestOptions::normal(),
                &parser.context(),
            );

            // Apply the same sort and deduplication treatment as pager completions
            crate::complete::sort_and_prioritize(&mut comp, CompletionRequestOptions::default());

            for next in comp {
                // Make a fake commandline, and then apply the completion to it.
                let faux_cmdline = &do_complete_param[token.clone()];
                let mut tmp_cursor = faux_cmdline.len();
                let mut faux_cmdline_with_completion = completion_apply_to_command_line(
                    &OperationContext::background_interruptible(parser.vars()),
                    &next.completion,
                    next.flags,
                    faux_cmdline,
                    &mut tmp_cursor,
                    /*append_only=*/ false,
                    /*is_unique=*/ false,
                );

                // completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE
                // is set. We don't want to set COMPLETE_NO_SPACE because that won't close
                // quotes. What we want is to close the quote, but not append the space. So we
                // just look for the space and clear it.
                if !next.flags.contains(CompleteFlags::NO_SPACE)
                    && string_suffixes_string(L!(" "), &faux_cmdline_with_completion)
                {
                    faux_cmdline_with_completion.truncate(faux_cmdline_with_completion.len() - 1);
                }

                if unescape_output {
                    // The input data is meant to be something like you would have on the command
                    // line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So
                    // we need to unescape the command line. See #1127.
                    faux_cmdline_with_completion = unescape_string(
                        &faux_cmdline_with_completion,
                        UnescapeStringStyle::Script(UnescapeFlags::default()),
                    )
                    .expect("Unescaping commandline to complete failed");
                }

                // Append any description.
                if !next.description.is_empty() {
                    faux_cmdline_with_completion
                        .reserve(faux_cmdline_with_completion.len() + 2 + next.description.len());
                    faux_cmdline_with_completion.push('\t');
                    faux_cmdline_with_completion.push_utfstr(&next.description);
                }
                faux_cmdline_with_completion.push('\n');
                streams.out.append(faux_cmdline_with_completion);
            }

            parser.libdata_mut().builtin_complete_current_commandline = false;
        }
    } else if path.is_empty()
        && gnu_opt.is_empty()
        && short_opt.is_empty()
        && old_opt.is_empty()
        && !remove
        && comp.is_empty()
        && desc.is_empty()
        && condition.is_empty()
        && wrap_targets.is_empty()
        && !result_mode.no_files
        && !result_mode.force_files
        && !result_mode.requires_param
    {
        // No arguments that would add or remove anything specified, so we print the definitions of
        // all matching completions.
        if cmd_to_complete.is_empty() {
            builtin_complete_print(L!(""), streams, parser);
        } else {
            for cmd in cmd_to_complete {
                builtin_complete_print(&cmd, streams, parser);
            }
        }
    } else {
        let mut flags = CompleteFlags::AUTO_SPACE;
        // HACK: Don't escape tildes because at the beginning of a token they probably mean
        // $HOME, for example as produced by a recursive call to "complete -C".
        flags |= CompleteFlags::DONT_ESCAPE_TILDES;
        if preserve_order {
            flags |= CompleteFlags::DONT_SORT;
        }

        if remove {
            builtin_complete_remove(&cmd_to_complete, &path, &short_opt, &gnu_opt, &old_opt);
        } else {
            builtin_complete_add(
                &cmd_to_complete,
                &path,
                &short_opt,
                &gnu_opt,
                &old_opt,
                result_mode,
                &condition,
                &comp,
                &desc,
                flags,
            );
        }

        // Handle wrap targets (probably empty). We only wrap commands, not paths.
        for wrap_target in wrap_targets {
            for i in &cmd_to_complete {
                if remove {
                    complete_remove_wrapper(i.clone(), &wrap_target);
                } else {
                    complete_add_wrapper(i.clone(), wrap_target.clone());
                }
            }
        }
    }

    Ok(SUCCESS)
}
