use super::prelude::*;
use crate::builtins;
use crate::common::get_program_name;
use crate::env::config_paths::get_fish_path;
use crate::err_fmt;
#[cfg(not(feature = "localize-messages"))]
use crate::err_raw;
use crate::proc::{
    JobControl, get_job_control_mode, get_login, is_interactive_session, set_job_control_mode,
};
use crate::reader::reader_in_interactive_read;
use crate::tty_handoff::{TERMINAL_OS_NAME, get_scroll_content_up_capability, xtversion};
use crate::wutil::{self, waccess, wbasename, wdirname, wrealpath};
use cfg_if::cfg_if;
use fish_feature_flags::{self as features, feature_test};
use fish_util::wcsfilecmp_glob;
use fish_widestring::{bytes2wcstring, osstr2wcstring, str2wcstring, wcs2bytes};
use nix::unistd::AccessFlags;
use rust_embed::RustEmbed;

/// Create an enum with name `$name`.
/// Its variants are given as comma-separated tuples, with each tuple containing the variant name,
/// as well as a string representations of the variant.
/// Optionally, additional strings can be provided, which are considered aliases for the enum
/// variant.
/// The generated enum will implement a `from_str` function, mapping from `&str` to the associated
/// enum variant if any, and `to_wstr`, which maps from a variant to the associated
/// `$default_name`.
/// It is the user's responsibility to ensure than none of the string arguments appears more than
/// once.
macro_rules! str_enum {
    ($name:ident, $(($val:ident, $default_name:expr $(, $alias:expr)* $(,)?)),* $(,)?) => {
        #[derive(Clone, Copy)]
        enum $name {
            $($val),*,
        }

        impl $name {
            fn from_str(s: &str) -> Option<Self> {
                // matching on str's lets us avoid having to do binary search and friends ourselves,
                // this is ASCII only anyways
                match s {
                    $($default_name => Some(Self::$val)),*,
                    $($($alias => Some(Self::$val),)*)*
                    _ => None,
                }
            }

            fn to_wstr(self) -> &'static wstr {
                match self {
                    $(Self::$val => L!($default_name)),*,
                }
            }
        }
    }
}

use StatusCmd::*;
str_enum!(
    StatusCmd,
    (Basename, "basename", "current-basename"),
    (BuildInfo, "build-info", "buildinfo"),
    (CurrentCommand, "current-command"),
    (CurrentCommandline, "current-commandline"),
    (Dirname, "dirname", "current-dirname"),
    (Features, "features"),
    (Filename, "filename", "current-filename"),
    (FishPath, "fish-path"),
    (Function, "function", "current-function"),
    (GetFile, "get-file"),
    (IsBlock, "is-block"),
    (IsBreakpoint, "is-breakpoint"),
    (IsCommandSubstitution, "is-command-substitution"),
    (IsFullJobControl, "is-full-job-control"),
    (IsInteractive, "is-interactive"),
    (IsInteractiveJobControl, "is-interactive-job-control"),
    (IsInteractiveRead, "is-interactive-read"),
    (IsLogin, "is-login"),
    (IsNoJobControl, "is-no-job-control"),
    (LineNumber, "line-number", "current-line-number"),
    (ListFiles, "list-files"),
    (Language, "language"),
    (JobControl, "job-control"),
    (StackTrace, "stack-trace", "print-stack-trace"),
    (Terminal, "terminal"),
    (TerminalOs, "terminal-os"),
    (TestFeature, "test-feature"),
    (TestTerminalFeature, "test-terminal-feature"),
);

/// Values that may be returned from the test-feature option to status.
#[repr(i32)]
enum TestFeatureRetVal {
    On,
    Off,
    NotRecognized,
}

struct Options {
    level: i32,
    new_job_control_mode: Option<JobControl>,
    status_cmd: Option<StatusCmd>,
    print_help: bool,
}

impl Options {
    // Set the status command to the given command.
    // Returns true on success, false if there's already a status command, in which case an error is printed.
    fn try_set_status_cmd(&mut self, subcmd: StatusCmd, streams: &mut IoStreams) -> bool {
        match self.status_cmd.replace(subcmd) {
            Some(existing) => {
                err_fmt!(
                    builtins::Error::COMBO_EXCLUSIVE,
                    existing.to_wstr(),
                    subcmd.to_wstr(),
                )
                .cmd(L!("status"))
                .finish(streams);
                false
            }
            None => true,
        }
    }
}

impl Default for Options {
    fn default() -> Self {
        Self {
            level: 1,
            new_job_control_mode: None,
            status_cmd: None,
            print_help: false,
        }
    }
}

/// Short options for long options that have no corresponding short options.
const FISH_PATH_SHORT: char = '\x01';
const IS_FULL_JOB_CTRL_SHORT: char = '\x02';
const IS_INTERACTIVE_JOB_CTRL_SHORT: char = '\x03';
const IS_NO_JOB_CTRL_SHORT: char = '\x04';
const IS_INTERACTIVE_READ_SHORT: char = '\x05';

const SHORT_OPTIONS: &wstr = L!("L:cbilfnhj:t");
const LONG_OPTIONS: &[WOption] = &[
    wopt(L!("help"), NoArgument, 'h'),
    wopt(L!("current-filename"), NoArgument, 'f'),
    wopt(L!("current-line-number"), NoArgument, 'n'),
    wopt(L!("filename"), NoArgument, 'f'),
    wopt(L!("fish-path"), NoArgument, FISH_PATH_SHORT),
    wopt(L!("is-block"), NoArgument, 'b'),
    wopt(L!("is-command-substitution"), NoArgument, 'c'),
    wopt(
        L!("is-full-job-control"),
        NoArgument,
        IS_FULL_JOB_CTRL_SHORT,
    ),
    wopt(L!("is-interactive"), NoArgument, 'i'),
    wopt(
        L!("is-interactive-job-control"),
        NoArgument,
        IS_INTERACTIVE_JOB_CTRL_SHORT,
    ),
    wopt(
        L!("is-interactive-read"),
        NoArgument,
        IS_INTERACTIVE_READ_SHORT,
    ),
    wopt(L!("is-login"), NoArgument, 'l'),
    wopt(L!("is-no-job-control"), NoArgument, IS_NO_JOB_CTRL_SHORT),
    wopt(L!("job-control"), RequiredArgument, 'j'),
    wopt(L!("level"), RequiredArgument, 'L'),
    wopt(L!("line"), NoArgument, 'n'),
    wopt(L!("line-number"), NoArgument, 'n'),
    wopt(L!("print-stack-trace"), NoArgument, 't'),
];

localizable_consts! {
    BUILTIN_INVALID_JOB_CONTROL_MODE "Invalid job control mode '%s'"
}

/// Print the features and their values.
fn print_features(streams: &mut IoStreams) {
    // TODO: move this to features.rs
    let mut max_len = i32::MIN;
    for md in features::METADATA {
        max_len = max_len.max(md.name.len() as i32);
    }
    for md in features::METADATA {
        let set = if feature_test(md.flag) {
            L!("on")
        } else {
            L!("off")
        };
        streams.out.append(&sprintf!(
            "%-*s%-3s %s %s\n",
            max_len + 1,
            md.name,
            set,
            md.groups,
            md.description,
        ));
    }
}

fn parse_cmd_opts(
    opts: &mut Options,
    optind: &mut usize,
    args: &mut [&wstr],
    parser: &Parser,
    streams: &mut IoStreams,
) -> BuiltinResult {
    let cmd = args[0];
    let mut w = WGetopter::new(SHORT_OPTIONS, LONG_OPTIONS, args);
    while let Some(c) = w.next_opt() {
        match c {
            'L' => {
                opts.level = {
                    let arg = w.woptarg.expect("Option -L requires an argument");
                    match fish_wcstoi(arg) {
                        Ok(level) if level >= 0 => level,
                        Err(wutil::Error::Overflow) | Ok(_) => {
                            err_fmt!("Invalid level value '%s'", arg)
                                .cmd(cmd)
                                .finish(streams);
                            return Err(STATUS_INVALID_ARGS);
                        }
                        _ => {
                            err_fmt!(builtins::Error::NOT_NUMBER, arg)
                                .cmd(cmd)
                                .finish(streams);
                            return Err(STATUS_INVALID_ARGS);
                        }
                    }
                };
            }
            flag @ ('c' | 'b' | 'i' | 'l' | 'f' | 'n' | 't') => {
                let subcmd = match flag {
                    'c' => IsCommandSubstitution,
                    'b' => IsBlock,
                    'i' => IsInteractive,
                    'l' => IsLogin,
                    'f' => Filename,
                    'n' => LineNumber,
                    't' => StackTrace,
                    _ => unreachable!(),
                };
                if !opts.try_set_status_cmd(subcmd, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            'j' => {
                if !opts.try_set_status_cmd(JobControl, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
                let Ok(job_mode) = w.woptarg.unwrap().try_into() else {
                    err_fmt!(BUILTIN_INVALID_JOB_CONTROL_MODE, w.woptarg.unwrap())
                        .cmd(cmd)
                        .finish(streams);
                    return Err(STATUS_CMD_ERROR);
                };
                opts.new_job_control_mode = Some(job_mode);
            }
            IS_FULL_JOB_CTRL_SHORT => {
                if !opts.try_set_status_cmd(IsFullJobControl, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            IS_INTERACTIVE_JOB_CTRL_SHORT => {
                if !opts.try_set_status_cmd(IsInteractiveJobControl, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            IS_INTERACTIVE_READ_SHORT => {
                if !opts.try_set_status_cmd(IsInteractiveRead, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            IS_NO_JOB_CTRL_SHORT => {
                if !opts.try_set_status_cmd(IsNoJobControl, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            FISH_PATH_SHORT => {
                if !opts.try_set_status_cmd(FishPath, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
            }
            'h' => opts.print_help = true,
            ':' => {
                builtin_missing_argument(parser, streams, cmd, None, args[w.wopt_index - 1], false);
                return Err(STATUS_INVALID_ARGS);
            }
            ';' => {
                builtin_unexpected_argument(parser, streams, cmd, args[w.wopt_index - 1], false);
                return Err(STATUS_INVALID_ARGS);
            }
            '?' => {
                builtin_unknown_option(parser, streams, cmd, args[w.wopt_index - 1], false);
                return Err(STATUS_INVALID_ARGS);
            }
            _ => panic!("unexpected retval from WGetopter"),
        }
    }

    *optind = w.wopt_index;

    Ok(SUCCESS)
}

struct EmptyEmbed;
impl RustEmbed for EmptyEmbed {
    fn get(_file_path: &str) -> Option<rust_embed::EmbeddedFile> {
        None
    }
    fn iter() -> impl Iterator<Item = std::borrow::Cow<'static, str>> {
        std::iter::empty()
    }
}

cfg_if!(
    if #[cfg(feature = "embed-manpages")] {
        #[derive(RustEmbed)]
        #[folder = "$FISH_RESOLVED_BUILD_DIR/fish-docs/man/man1"]
        #[prefix = "man/man1/"]
        struct Docs;
    } else {
        type Docs = EmptyEmbed;
    }
);

cfg_if!(
    if #[cfg(using_cmake)] {
        #[derive(RustEmbed)]
        #[folder = "$FISH_CMAKE_BINARY_DIR/share"]
        #[include = "__fish_build_paths.fish"]
        struct CMakeBinaryDir;
    } else {
        type CMakeBinaryDir = EmptyEmbed;
    }
);

pub fn status(parser: &mut Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult {
    let cmd = args[0];
    let argc = args.len();

    let mut opts = Options::default();
    let mut optind = 0usize;
    parse_cmd_opts(&mut opts, &mut optind, args, parser, streams)?;

    if opts.print_help {
        builtin_print_help(parser, streams, cmd);
        return Ok(SUCCESS);
    }

    // If a status command hasn't already been specified via a flag check the first word.
    // Note that this can be simplified after we eliminate allowing subcommands as flags.
    if optind < argc {
        match StatusCmd::from_str(args[optind].to_string().as_str()) {
            Some(s) => {
                if !opts.try_set_status_cmd(s, streams) {
                    return Err(STATUS_CMD_ERROR);
                }
                optind += 1;
            }
            None => {
                err_fmt!(builtins::Error::INVALID_SUBCMD)
                    .subcmd(cmd, args[1])
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
        }
    }
    // Every argument that we haven't consumed already is an argument for a subcommand.
    let args = &args[optind..];

    let Some(subcmd) = opts.status_cmd else {
        debug_assert!(args.is_empty(), "passed arguments to nothing");

        if get_login() {
            streams.out.appendln(wgettext!("This is a login shell"));
        } else {
            streams.out.appendln(wgettext!("This is not a login shell"));
        }
        let job_control_mode = match get_job_control_mode() {
            JobControl::Interactive => wgettext!("Only on interactive jobs"),
            JobControl::None => wgettext!("Never"),
            JobControl::All => wgettext!("Always"),
        };
        streams
            .out
            .appendln(&wgettext_fmt!("Job control: %s", job_control_mode));
        streams.out.append(&parser.stack_trace());

        return Ok(SUCCESS);
    };

    match subcmd {
        c @ JobControl => {
            let job_control_mode = match opts.new_job_control_mode {
                Some(j) => {
                    // Flag form used
                    // Any extra args would have already failed, either as an
                    // unrecognized subcmd, or as a "subcmd combo"
                    assert!(args.is_empty(), "unexpected job-control args");
                    j
                }
                None => {
                    if args.len() != 1 {
                        err_fmt!(builtins::Error::UNEXP_ARG_COUNT, 1, args.len())
                            .subcmd(cmd, c.to_wstr())
                            .finish(streams);
                        return Err(STATUS_INVALID_ARGS);
                    }
                    let Ok(new_mode) = args[0].try_into() else {
                        err_fmt!(BUILTIN_INVALID_JOB_CONTROL_MODE, args[0])
                            .subcmd(cmd, c.to_wstr())
                            .finish(streams);
                        return Err(STATUS_CMD_ERROR);
                    };
                    new_mode
                }
            };
            set_job_control_mode(job_control_mode);
        }
        Features => print_features(streams),
        c @ TestFeature => {
            if args.len() != 1 {
                err_fmt!(builtins::Error::UNEXP_ARG_COUNT, 1, args.len())
                    .subcmd(cmd, c.to_wstr())
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            let mut retval = TestFeatureRetVal::NotRecognized;
            for md in features::METADATA {
                if md.name == args[0] {
                    retval = match feature_test(md.flag) {
                        true => TestFeatureRetVal::On,
                        false => TestFeatureRetVal::Off,
                    };
                }
            }
            return Err(retval as i32);
        }
        c @ GetFile => {
            if args.len() != 1 {
                err_fmt!(builtins::Error::UNEXP_ARG_COUNT, 1, args.len())
                    .subcmd(cmd, c.to_wstr())
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            let arg = wcs2bytes(args[0]);
            let arg = std::str::from_utf8(&arg).unwrap();
            let Some(emfile) = crate::autoload::Asset::get(arg)
                .or_else(|| Docs::get(arg))
                .or_else(|| CMakeBinaryDir::get(arg))
            else {
                return Err(STATUS_CMD_ERROR);
            };
            let src = bytes2wcstring(&emfile.data);
            streams.out.append(&src);
            return Ok(SUCCESS);
        }
        Language => {
            cfg_if! {
                if #[cfg(not(feature = "localize-messages"))] {
                    err_raw!(L!("fish was built with the `localize-messages` feature disabled. The `status language` command is unavailable.").to_owned())
                                .finish(streams);
                    return Err(STATUS_CMD_ERROR);
                } else {
                    if args.is_empty() {
                        streams.out.append(&crate::localization::status_language());
                        return Ok(SUCCESS);
                    }
                    match args[0].to_string().as_str() {
                        "list-available" => {
                            streams.out.append(&crate::localization::list_available_languages());
                            return Ok(SUCCESS);
                        },
                        "set" => {
                            let langs = args[1..]
                                .iter()
                                .map(|lang| lang.to_string())
                                .collect::<Vec<_>>();
                            let lints = crate::localization::update_from_status_language_builtin(&langs);
                            let formatted_lints = lints.display_all();
                            if !formatted_lints.is_empty() {
                                streams.err.append(&formatted_lints);
                            }
                            return Ok(SUCCESS);
                        }
                        "unset" => {
                            crate::localization::unset_from_status_language_builtin(parser.vars());
                            return Ok(SUCCESS);
                        }
                        invalid => {
                            err_fmt!(builtins::Error::INVALID_SUBSUBCMD,  invalid)
                                .subcmd(cmd, subcmd.to_wstr())
                                .finish(streams);
                            return Err(STATUS_INVALID_ARGS);
                        }
                    }
                }
            }
        }
        ListFiles => {
            let mut paths = vec![];
            let mut add = |arg| {
                let arg = wcs2bytes(arg);
                let arg = std::str::from_utf8(&arg).unwrap();
                for path in crate::autoload::Asset::iter()
                    .chain(Docs::iter())
                    .chain(CMakeBinaryDir::iter())
                {
                    if arg.is_empty() || path.starts_with(arg) {
                        paths.push(str2wcstring(&path));
                    }
                }
            };
            if args.is_empty() {
                add(L!(""));
            } else {
                for arg in args {
                    add(arg);
                }
            }

            paths.sort_by(|a, b| wcsfilecmp_glob(a, b));
            for path in &paths {
                streams.out.appendln(path);
            }

            if !paths.is_empty() {
                return Ok(SUCCESS);
            } else {
                return Err(STATUS_CMD_ERROR);
            }
        }
        c @ TestTerminalFeature => {
            if args.len() != 1 {
                err_fmt!(builtins::Error::UNEXP_ARG_COUNT, 1, args.len())
                    .subcmd(cmd, c.to_wstr())
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            if args[0] != "scroll-content-up" {
                err_fmt!("unrecognized feature '%s'", args[0])
                    .subcmd(cmd, c.to_wstr())
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            return if get_scroll_content_up_capability() == Some(true) {
                Ok(SUCCESS)
            } else {
                Err(STATUS_CMD_ERROR)
            };
        }

        ref s => {
            if !args.is_empty() {
                err_fmt!(builtins::Error::UNEXP_ARG_COUNT, 0, args.len())
                    .subcmd(cmd, s.to_wstr())
                    .finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            match s {
                BuildInfo => {
                    let version = str2wcstring(crate::BUILD_VERSION);
                    let target = str2wcstring(env!("BUILD_TARGET_TRIPLE"));
                    let host = str2wcstring(env!("BUILD_HOST_TRIPLE"));
                    let profile = str2wcstring(env!("BUILD_PROFILE"));
                    streams.out.append(L!("Build system: "));
                    let buildsystem = if cfg!(using_cmake) {
                        L!("CMake")
                    } else {
                        L!("Cargo")
                    };
                    streams.out.appendln(buildsystem);
                    streams.out.append(L!("Version: "));
                    streams.out.appendln(&version);
                    if target == host {
                        streams.out.append(L!("Target (and host): "));
                        streams.out.appendln(&target);
                    } else {
                        streams.out.append(L!("Target: "));
                        streams.out.appendln(&target);
                        streams.out.append(L!("Host: "));
                        streams.out.appendln(&host);
                    }
                    streams.out.append(L!("Profile: "));
                    streams.out.appendln(&profile);
                    streams.out.append(L!("Features: "));
                    let features: &[&str] = &[
                        #[cfg(feature = "embed-manpages")]
                        "embed-manpages",
                        #[cfg(feature = "localize-messages")]
                        "localize-messages",
                        #[cfg(target_feature = "crt-static")]
                        "crt-static",
                    ];
                    streams.out.appendln(&str2wcstring(features.join(" ")));
                    streams.out.appendln("");
                    return Ok(SUCCESS);
                }
                Basename | Dirname | Filename => {
                    let res = parser.current_filename();
                    let function = res.unwrap_or_default();
                    let f = match (function.is_empty(), s) {
                        (false, Dirname) => wdirname(&function),
                        (false, Basename) => wbasename(&function),
                        (true, _) => wgettext!("Standard input"),
                        (false, _) => &function,
                    };
                    streams.out.appendln(f);
                }
                Function => {
                    let f = match parser.get_function_name(opts.level) {
                        Some(f) => f,
                        None => wgettext!("Not a function").to_owned(),
                    };
                    streams.out.appendln(&f);
                }
                LineNumber => {
                    // TBD is how to interpret the level argument when fetching the line number.
                    // See issue #4161.
                    // streams.out.append_format(L"%d\n", parser.get_lineno(opts.level));
                    streams
                        .out
                        .appendln(&parser.lineno_for_display().to_wstring());
                }
                IsInteractive => {
                    if is_interactive_session() {
                        return Ok(SUCCESS);
                    }
                    return Err(STATUS_CMD_ERROR);
                }
                IsCommandSubstitution => {
                    if parser.scope().is_subshell {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsBlock => {
                    if parser.is_block() {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsBreakpoint => {
                    if parser.is_breakpoint() {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsLogin => {
                    if get_login() {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsFullJobControl => {
                    if get_job_control_mode() == JobControl::All {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsInteractiveJobControl => {
                    if get_job_control_mode() == JobControl::Interactive {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsInteractiveRead => {
                    if reader_in_interactive_read() {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                IsNoJobControl => {
                    if get_job_control_mode() == JobControl::None {
                        return Ok(SUCCESS);
                    } else {
                        return Err(STATUS_CMD_ERROR);
                    }
                }
                StackTrace => {
                    streams.out.append(&parser.stack_trace());
                }
                CurrentCommand => {
                    let command = &parser.libdata().status_vars.command;
                    if !command.is_empty() {
                        streams.out.appendln(command);
                    } else {
                        streams.out.appendln(get_program_name());
                    }
                }
                CurrentCommandline => {
                    let commandline = &parser.libdata().status_vars.commandline;
                    streams.out.appendln(commandline);
                }
                FishPath => {
                    use crate::env::config_paths::FishPath::*;
                    let result = match get_fish_path() {
                        Absolute(path) => {
                            let path = osstr2wcstring(path);
                            Cow::Owned(match wrealpath(&path) {
                                Some(p) if waccess(&p, AccessFlags::F_OK).is_ok() => p,
                                // realpath did not work, just append the path
                                // - maybe this was obtained via $PATH?
                                _ => path,
                            })
                        }
                        LookUpInPath => Cow::Borrowed(get_program_name()),
                    };
                    streams.out.appendln(&result);
                }
                Terminal => {
                    let xtversion = xtversion().unwrap_or_default();
                    streams.out.appendln(xtversion);
                }
                TerminalOs => {
                    let Some(Some(terminal_os_name)) = TERMINAL_OS_NAME.get() else {
                        return Err(STATUS_CMD_ERROR);
                    };
                    streams.out.appendln(first_line(terminal_os_name));
                }
                JobControl | Features | TestFeature | GetFile | ListFiles | Language
                | TestTerminalFeature => {
                    unreachable!("")
                }
            }
        }
    }

    Ok(SUCCESS)
}

fn first_line(s: &wstr) -> &wstr {
    &s[..s.chars().position(|c| c == '\n').unwrap_or(s.len())]
}
