// Functions used for implementing the set_color builtin.
#include "config.h"

#include "set_color.h"

#include <cstddef>
#include <cstdlib>

#if HAVE_CURSES_H
#include <curses.h>
#elif HAVE_NCURSES_H
#include <ncurses.h>
#elif HAVE_NCURSES_CURSES_H
#include <ncurses/curses.h>
#endif
#if HAVE_TERM_H
#include <term.h>
#elif HAVE_NCURSES_TERM_H
#include <ncurses/term.h>
#endif

#include <memory>
#include <string>
#include <vector>

#include "../builtin.h"
#include "../color.h"
#include "../common.h"
#include "../env.h"
#include "../output.h"
#include "../parser.h"
#include "../wgetopt.h"
#include "../wutil.h"  // IWYU pragma: keep

class parser_t;

static void print_modifiers(outputter_t &outp, bool bold, bool underline, bool italics, bool dim,
                            bool reverse, rgb_color_t bg) {
    if (bold && enter_bold_mode) {
        // These casts are needed to work with different curses implementations.
        writembs_nofail(outp, tparm(const_cast<char *>(enter_bold_mode)));
    }

    if (underline && enter_underline_mode) {
        writembs_nofail(outp, enter_underline_mode);
    }

    if (italics && enter_italics_mode) {
        writembs_nofail(outp, enter_italics_mode);
    }

    if (dim && enter_dim_mode) {
        writembs_nofail(outp, enter_dim_mode);
    }

    if (reverse && enter_reverse_mode) {
        writembs_nofail(outp, enter_reverse_mode);
    } else if (reverse && enter_standout_mode) {
        writembs_nofail(outp, enter_standout_mode);
    }
    if (!bg.is_none() && bg.is_normal()) {
        writembs_nofail(outp, tparm(const_cast<char *>(exit_attribute_mode)));
    }
}

static void print_colors(io_streams_t &streams, bool bold, bool underline, bool italics, bool dim,
                         bool reverse, rgb_color_t bg) {
    outputter_t outp;
    for (const auto &color_name : rgb_color_t::named_color_names()) {
        if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) {
            print_modifiers(outp, bold, underline, italics, dim, reverse, bg);
            rgb_color_t color = rgb_color_t(color_name);
            outp.set_color(color, rgb_color_t::none());
            if (!bg.is_none()) {
                outp.write_color(bg, false /* not is_fg */);
            }
        }
        outp.writestr(color_name);
        if (!bg.is_none()) {
            // If we have a background, stop it after the color
            // or it goes to the end of the line and looks ugly.
            writembs_nofail(outp, tparm(const_cast<char *>(exit_attribute_mode)));
        }
        outp.writech(L'\n');
    }  // conveniently, 'normal' is always the last color so we don't need to reset here

    streams.out.append(str2wcstring(outp.contents()));
}

static const wchar_t *const short_options = L":b:hvoidrcu";
static const struct woption long_options[] = {{L"background", required_argument, nullptr, 'b'},
                                              {L"help", no_argument, nullptr, 'h'},
                                              {L"bold", no_argument, nullptr, 'o'},
                                              {L"underline", no_argument, nullptr, 'u'},
                                              {L"italics", no_argument, nullptr, 'i'},
                                              {L"dim", no_argument, nullptr, 'd'},
                                              {L"reverse", no_argument, nullptr, 'r'},
                                              {L"version", no_argument, nullptr, 'v'},
                                              {L"print-colors", no_argument, nullptr, 'c'},
                                              {nullptr, 0, nullptr, 0}};

#ifdef __APPLE__
static char sitm_esc[] = "\x1B[3m";
static char ritm_esc[] = "\x1B[23m";
static char dim_esc[] = "\x1B[2m";
#endif

/// set_color builtin.
maybe_t<int> builtin_set_color(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
    // By the time this is called we should have initialized the curses subsystem.
    assert(curses_initialized);

// Hack in missing italics and dim capabilities omitted from MacOS xterm-256color terminfo
// Helps Terminal.app/iTerm
#ifdef __APPLE__
    const auto term_prog = parser.vars().get(L"TERM_PROGRAM");
    if (!term_prog.missing_or_empty() &&
        (term_prog->as_string() == L"Apple_Terminal" || term_prog->as_string() == L"iTerm.app")) {
        const auto term = parser.vars().get(L"TERM");
        if (!term.missing_or_empty() && (term->as_string() == L"xterm-256color")) {
            enter_italics_mode = sitm_esc;
            exit_italics_mode = ritm_esc;
            enter_dim_mode = dim_esc;
        }
    }
#endif

    // Variables used for parsing the argument list.
    int argc = builtin_count_args(argv);

    // Some code passes variables to set_color that don't exist, like $fish_user_whatever. As a
    // hack, quietly return failure.
    if (argc <= 1) {
        return EXIT_FAILURE;
    }

    const wchar_t *bgcolor = nullptr;
    bool bold = false, underline = false, italics = false, dim = false, reverse = false,
         print = false;

    // Parse options to obtain the requested operation and the modifiers.
    int opt;
    wgetopter_t w;
    while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
        switch (opt) {
            case 'b': {
                bgcolor = w.woptarg;
                break;
            }
            case 'h': {
                builtin_print_help(parser, streams, argv[0]);
                return STATUS_CMD_OK;
            }
            case 'o': {
                bold = true;
                break;
            }
            case 'i': {
                italics = true;
                break;
            }
            case 'd': {
                dim = true;
                break;
            }
            case 'r': {
                reverse = true;
                break;
            }
            case 'u': {
                underline = true;
                break;
            }
            case 'c': {
                print = true;
                break;
            }
            case ':': {
                // We don't error here because "-b" is the only option that requires an argument,
                // and we don't error for missing colors.
                return STATUS_INVALID_ARGS;
            }
            case '?': {
                builtin_unknown_option(parser, streams, L"set_color", argv[w.woptind - 1]);
                return STATUS_INVALID_ARGS;
            }
            default: {
                DIE("unexpected retval from wgetopt_long");
            }
        }
    }

    rgb_color_t bg = rgb_color_t(bgcolor ? bgcolor : L"");
    if (bgcolor && bg.is_none()) {
        streams.err.append_format(_(L"%ls: Unknown color '%ls'\n"), argv[0], bgcolor);
        return STATUS_INVALID_ARGS;
    }

    if (print) {
        // Hack: Explicitly setting a background of "normal" crashes
        // for --print-colors. Because it's not interesting in terms of display,
        // just skip it.
        if (bgcolor && bg.is_special()) {
            bg = rgb_color_t(L"");
        }
        print_colors(streams, bold, underline, italics, dim, reverse, bg);
        return STATUS_CMD_OK;
    }

    // Remaining arguments are foreground color.
    std::vector<rgb_color_t> fgcolors;
    for (; w.woptind < argc; w.woptind++) {
        rgb_color_t fg = rgb_color_t(argv[w.woptind]);
        if (fg.is_none()) {
            streams.err.append_format(_(L"%ls: Unknown color '%ls'\n"), argv[0], argv[w.woptind]);
            return STATUS_INVALID_ARGS;
        }
        fgcolors.push_back(fg);
    }

    // #1323: We may have multiple foreground colors. Choose the best one. If we had no foreground
    // color, we'll get none(); if we have at least one we expect not-none.
    const rgb_color_t fg = best_color(fgcolors, output_get_color_support());
    assert(fgcolors.empty() || !fg.is_none());

    // Test if we have at least basic support for setting fonts, colors and related bits - otherwise
    // just give up...
    if (cur_term == nullptr || !exit_attribute_mode) {
        return STATUS_CMD_ERROR;
    }
    outputter_t outp;

    print_modifiers(outp, bold, underline, italics, dim, reverse, bg);

    if (bgcolor != nullptr && bg.is_normal()) {
        writembs_nofail(outp, tparm(const_cast<char *>(exit_attribute_mode)));
    }

    if (!fg.is_none()) {
        if (fg.is_normal() || fg.is_reset()) {
            writembs_nofail(outp, tparm(const_cast<char *>(exit_attribute_mode)));
        } else {
            if (!outp.write_color(fg, true /* is_fg */)) {
                // We need to do *something* or the lack of any output messes up
                // when the cartesian product here would make "foo" disappear:
                //  $ echo (set_color foo)bar
                outp.set_color(rgb_color_t::reset(), rgb_color_t::none());
            }
        }
    }

    if (bgcolor != nullptr && !bg.is_normal() && !bg.is_reset()) {
        outp.write_color(bg, false /* not is_fg */);
    }

    // Output the collected string.
    streams.out.append(str2wcstring(outp.contents()));

    return STATUS_CMD_OK;
}
