/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2013 Red Hat, Inc.
 */

/**
 * SECTION:nmt-newt-utils
 * @short_description: Utility functions
 */

#include "libnm-client-aux-extern/nm-default-client.h"

#include "nmt-newt-utils.h"

#include <stdarg.h>
#include <unistd.h>
#include <sys/wait.h>

#include "libnm-glib-aux/nm-io-utils.h"
#include "libnm-glib-aux/nm-str-buf.h"

static void
nmt_newt_dialog_g_log_handler(const char    *log_domain,
                              GLogLevelFlags log_level,
                              const char    *message,
                              gpointer       user_data)
{
    const char   *level_name;
    char         *full_message;
    int           screen_width, screen_height;
    newtComponent text, ok, form;
    newtGrid      grid;

    g_assert(!(log_level & G_LOG_FLAG_RECURSION));

    if (log_level & G_LOG_LEVEL_DEBUG)
        return;

    switch (log_level & G_LOG_LEVEL_MASK) {
    case G_LOG_LEVEL_ERROR:
        level_name = "ERROR";
        break;
    case G_LOG_LEVEL_CRITICAL:
        level_name = "CRITICAL";
        break;
    case G_LOG_LEVEL_WARNING:
        level_name = "WARNING";
        break;
    case G_LOG_LEVEL_MESSAGE:
        level_name = "Message";
        break;
    default:
        level_name = NULL;
    }

    full_message = g_strdup_printf("%s%s%s%s%s",
                                   log_domain ?: "",
                                   log_domain && level_name ? " " : "",
                                   level_name ?: "",
                                   log_domain || level_name ? ": " : "",
                                   message);

    /* newtWinMessage() wraps the window too narrowly by default, so
     * we don't want to use that. But we intentionally avoid using any
     * NmtNewt classes, to avoid possible error recursion.
     */

    newtGetScreenSize(&screen_width, &screen_height);
    text = newtTextboxReflowed(-1, -1, full_message, NM_MAX(70, screen_width - 10), 0, 0, 0);
    g_free(full_message);

    ok = newtButton(-1, -1, "OK");

    grid = newtCreateGrid(1, 2);
    newtGridSetField(grid, 0, 0, NEWT_GRID_COMPONENT, text, 0, 0, 0, 0, 0, 0);
    newtGridSetField(grid, 0, 1, NEWT_GRID_COMPONENT, ok, 0, 1, 0, 0, NEWT_ANCHOR_RIGHT, 0);

    newtGridWrappedWindow(grid, (char *) (level_name ?: ""));
    newtGridFree(grid, TRUE);

    form = newtForm(NULL, NULL, 0);
    newtFormAddComponents(form, text, ok, NULL);
    newtRunForm(form);
    newtFormDestroy(form);
    newtPopWindow();
}

static void
nmt_newt_basic_g_log_handler(const char    *log_domain,
                             GLogLevelFlags log_level,
                             const char    *message,
                             gpointer       user_data)
{
    newtSuspend();
    g_log_default_handler(log_domain, log_level, message, NULL);
    newtResume();
}

static void
nmt_newt_suspend_callback(gpointer user_data)
{
    newtSuspend();
    kill(getpid(), SIGTSTP);
    newtResume();
}

static void
_newtSetColor(int colorset, const char *fg, const char *bg)
{
    newtSetColor(colorset, (char *) fg, (char *) bg);
}

/**
 * nmt_newt_parse_colors:
 * @s: buffer with color settings
 * @is_newt: boolean indicating if buffer s
 *           contains NEWT (true) or NMT (false) color setting
 *
 * Parses content of buffer s and sets color accordingly
 * with newtSetColor()
 */
static void
nmt_newt_parse_colors(const char *s, bool is_newt)
{
    gs_free const char **lines = NULL;
    size_t               i;

    lines = nm_strsplit_set(s, ";:\n\r\t ");

    if (!lines)
        return;

    for (i = 0; lines[i]; i++) {
        const char *name;
        const char *fg;
        const char *bg;
        char       *parsed_s;

        parsed_s = (char *) lines[i];
        name     = parsed_s;

        if (!(parsed_s = strchr(parsed_s, '=')) || !*parsed_s)
            continue;

        *parsed_s = '\0';
        fg        = ++parsed_s;

        if (!(parsed_s = strchr(parsed_s, ',')) || !*parsed_s)
            continue;

        *parsed_s = '\0';

        bg = ++parsed_s;

        if (is_newt) {
            if (nm_streq(name, "checkbox"))
                _newtSetColor(NEWT_COLORSET_CHECKBOX, fg, bg);
        } else {
            if (nm_streq(name, "badLabel")) {
                _newtSetColor(NMT_NEWT_COLORSET_BAD_LABEL, fg, bg);
            } else if (nm_streq(name, "plainLabel")) {
                _newtSetColor(NMT_NEWT_COLORSET_PLAIN_LABEL, fg, bg);
            } else if (nm_streq(name, "disabledButton")) {
                _newtSetColor(NMT_NEWT_COLORSET_DISABLED_BUTTON, fg, bg);
            } else if (nm_streq(name, "textboxWithBackground")) {
                _newtSetColor(NMT_NEWT_COLORSET_TEXTBOX_WITH_BACKGROUND, fg, bg);
            }
        }
    }
}

/**
 * nmt_newt_nit_colors:
 * @is_newt: boolean indicating if the function is looking for NEWT or NMT env var
 *
 * Looks for enviroment variables for aditional
 * color set up in nmtui
 */
static void
nmt_newt_init_colors(gboolean is_newt)
{
    const char   *colors;
    gs_free char *file_content = NULL;

    colors = getenv(is_newt ? "NEWT_COLORS" : "NMT_NEWT_COLORS");

    if (!colors) {
        const char *file_name;

        file_name = getenv(is_newt ? "NEWT_COLORS_FILE" : "NMT_NEWT_COLORS_FILE");

        if (file_name && file_name[0] != '\0'
            && (nm_utils_file_get_contents(-1,
                                           file_name,
                                           16384,
                                           NM_UTILS_FILE_GET_CONTENTS_FLAG_NONE,
                                           &file_content,
                                           NULL,
                                           NULL,
                                           NULL))) {
            colors = file_content;
        }
    }

    nmt_newt_parse_colors(colors, is_newt);
}

/**
libnm-client-aux-extern * nmt_newt_init:
 *
 * Wrapper for newtInit() that also does some nmt-newt-internal setup.
 * This should be called once, before any other nmt-newt functions.
 */
void
nmt_newt_init(void)
{
    newtInit();
    newtCls();

    newtSetColor(NEWT_COLORSET_CHECKBOX, "black", "lightgray");
    newtSetColor(NMT_NEWT_COLORSET_BAD_LABEL, "red", "lightgray");
    newtSetColor(NMT_NEWT_COLORSET_PLAIN_LABEL, "black", "lightgray");
    newtSetColor(NMT_NEWT_COLORSET_DISABLED_BUTTON, "blue", "lightgray");
    newtSetColor(NMT_NEWT_COLORSET_TEXTBOX_WITH_BACKGROUND, "black", "white");

    nmt_newt_init_colors(TRUE);
    nmt_newt_init_colors(FALSE);

    if (g_getenv("NMTUI_DEBUG"))
        g_log_set_default_handler(nmt_newt_dialog_g_log_handler, NULL);
    else
        g_log_set_default_handler(nmt_newt_basic_g_log_handler, NULL);

    newtSetSuspendCallback(nmt_newt_suspend_callback, NULL);
}

/**
 * nmt_newt_finished:
 *
 * Wrapper for newtFinished(). Should be called at the end of the program.
 */
void
nmt_newt_finished(void)
{
    newtFinished();
    g_log_set_default_handler(g_log_default_handler, NULL);
}

/**
 * nmt_newt_message_dialog:
 * @message: a printf()-style message format
 * @...: arguments
 *
 * Displays the given message in a dialog box with a single "OK"
 * button, and returns after the user clicks "OK".
 */
void
nmt_newt_message_dialog(const char *message, ...)
{
    va_list ap;
    char   *msg, *msg_lc, *ok_lc;

    va_start(ap, message);
    msg = g_strdup_vprintf(message, ap);
    va_end(ap);

    msg_lc = nmt_newt_locale_from_utf8(msg);
    ok_lc  = nmt_newt_locale_from_utf8(_("OK"));
    newtWinMessage(NULL, ok_lc, "%s", msg_lc);

    g_free(ok_lc);
    g_free(msg_lc);
    g_free(msg);
}

/**
 * nmt_newt_choice_dialog:
 * @button1: the label for the first button
 * @button2: the label for the second button
 * @message: a printf()-style message format
 * @...: arguments
 *
 * Displays the given message in a dialog box with two buttons with
 * the indicated labels, and waits for the user to click one.
 *
 * Returns: which button was clicked: 1 for @button1, 2 for @button2 or 0 otherwise
 */
int
nmt_newt_choice_dialog(const char *button1, const char *button2, const char *message, ...)
{
    va_list ap;
    char   *msg, *msg_lc, *button1_lc, *button2_lc;
    int     choice;

    va_start(ap, message);
    msg = g_strdup_vprintf(message, ap);
    va_end(ap);

    msg_lc     = nmt_newt_locale_from_utf8(msg);
    button1_lc = nmt_newt_locale_from_utf8(button1);
    button2_lc = nmt_newt_locale_from_utf8(button2);
    choice     = newtWinChoice(NULL, button1_lc, button2_lc, "%s", msg_lc);

    g_free(button1_lc);
    g_free(button2_lc);
    g_free(msg_lc);
    g_free(msg);

    return choice;
}

/**
 * nmt_newt_locale_to_utf8:
 * @str_lc: a string in the user's locale encoding
 *
 * Convenience wrapper around g_locale_to_utf8().
 *
 * Note that libnewt works in terms of the user's locale character
 * set, NOT UTF-8, so all strings received from libnewt must be
 * converted back to UTF-8 before being returned to the caller or used
 * in other APIs.
 *
 * Returns: @str_lc, converted to UTF-8.
 */
char *
nmt_newt_locale_to_utf8(const char *str_lc)
{
    char *str_utf8;

    str_utf8 = g_locale_to_utf8(str_lc, -1, NULL, NULL, NULL);
    if (!str_utf8)
        str_utf8 = g_strdup("");
    return str_utf8;
}

/**
 * nmt_newt_locale_from_utf8:
 * @str_utf8: a UTF-8 string
 *
 * Convenience wrapper around g_locale_from_utf8().
 *
 * Note that libnewt works in terms of the user's locale character
 * set, NOT UTF-8, so all strings from nmt-newt must be converted to
 * locale encoding before being passed to libnewt.
 *
 * Returns: @str_utf8, converted to the user's locale encoding.
 */
char *
nmt_newt_locale_from_utf8(const char *str_utf8)
{
    char *str_lc;

    str_lc = g_locale_from_utf8(str_utf8, -1, NULL, NULL, NULL);
    if (!str_lc)
        str_lc = g_strdup("");
    return str_lc;
}

/**
 * nmt_newt_text_width
 * @str: a UTF-8 string
 *
 * Computes the width (in terminal columns) of @str.
 *
 * Returns: the width of @str
 */
int
nmt_newt_text_width(const char *str)
{
    int      width;
    gunichar ch;

    for (width = 0; *str; str = g_utf8_next_char(str)) {
        ch = g_utf8_get_char(str);

        /* Based on _vte_iso2022_unichar_width */
        if (G_LIKELY(ch < 0x80))
            width += 1;
        else if (G_UNLIKELY(g_unichar_iszerowidth(ch)))
            width += 0;
        else if (G_UNLIKELY(g_unichar_iswide(ch)))
            width += 2;
        else
            width += 1;
    }

    return width;
}

/**
 * nmt_newt_text_truncate
 * @str: a UTF-8 string
 * @max_width: the maximum width in terminal columns
 *
 * Truncates @str to at most @max_width columns, appending an ellipsis if
 * anything was dropped (the ellipsis is counted in @max_width).
 *
 * Returns: (transfer full): a newly-allocated truncated copy of @str.
 */
char *
nmt_newt_text_truncate(const char *str, int max_width)
{
    nm_auto_str_buf NMStrBuf buf = NM_STR_BUF_INIT(0, FALSE);
    const char              *p;
    int                      width = 0;

    if (max_width <= 0)
        return g_strdup("");
    if (nmt_newt_text_width(str) <= max_width)
        return g_strdup(str);

    max_width--; /* reserve one column for the ellipsis */

    for (p = str; *p; p = g_utf8_next_char(p)) {
        gunichar ch = g_utf8_get_char(p);
        int      w  = g_unichar_iszerowidth(ch) ? 0 : (g_unichar_iswide(ch) ? 2 : 1);

        if (width + w > max_width)
            break;
        width += w;
        nm_str_buf_append_len(&buf, p, g_utf8_next_char(p) - p);
    }
    nm_str_buf_append(&buf, "\xe2\x80\xa6"); /* U+2026 HORIZONTAL ELLIPSIS */

    return nm_str_buf_dup_str(&buf);
}

/**
 * nmt_newt_edit_string:
 * @data: data to edit
 *
 * libnewt does not have a multi-line editable text component, so
 * nmt-newt provides this function instead, which will open the user's
 * editor to edit a file containing the given @data (ensuring that the
 * current screen state is saved before starting the editor and
 * restored after it returns).
 *
 * Returns: the edited data, or %NULL if an error occurred.
 */
char *
nmt_newt_edit_string(const char *data)
{
    gssize  len, nwrote;
    char   *filename, *argv[3];
    GError *error = NULL;
    int     fd, status;
    char   *new_data = NULL;

    fd = g_file_open_tmp("XXXXXX.json", &filename, &error);
    if (fd == -1) {
        nmt_newt_message_dialog(_("Could not create temporary file: %s"), error->message);
        g_error_free(error);
        return NULL;
    }

    len = data ? strlen(data) : 0;
    while (len) {
        nwrote = write(fd, data, len);

        if (nwrote == -1) {
            if (errno == EINTR) {
                continue;
            }

            nmt_newt_message_dialog(_("Could not write to temporary file: %s"),
                                    nm_strerror_native(errno));
            nm_close(fd);
            goto done;
        }

        len -= nwrote;
        data += nwrote;
    }
    nm_close(fd);

    argv[0] = (char *) g_getenv("VISUAL");
    if (!argv[0])
        argv[0] = (char *) g_getenv("EDITOR");
    if (!argv[0])
        argv[0] = (char *) "vi";
    argv[1] = filename;
    argv[2] = NULL;

    newtSuspend();
    g_spawn_sync(NULL,
                 argv,
                 NULL,
                 G_SPAWN_SEARCH_PATH | G_SPAWN_CHILD_INHERITS_STDIN,
                 NULL,
                 NULL,
                 NULL,
                 NULL,
                 &status,
                 &error);
    newtResume();

    if (error) {
        nmt_newt_message_dialog(_("Could not create temporary file: %s"), error->message);
        g_error_free(error);
        goto done;
    }

    if (!g_spawn_check_exit_status(status, &error)) {
        nmt_newt_message_dialog(_("Editor failed: %s"), error->message);
        g_error_free(error);
        goto done;
    }

    if (!g_file_get_contents(filename, &new_data, NULL, &error)) {
        nmt_newt_message_dialog(_("Could not re-read file: %s"), error->message);
        g_error_free(error);
        goto done;
    }

done:
    unlink(filename);
    g_free(filename);

    return new_data;
}
