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

/**
 * SECTION:nmt-newt-form
 * @short_description: The top-level NmtNewt widget
 *
 * #NmtNewtForm is the top-level widget that contains and presents a
 * "form" (aka dialog) to the user.
 */

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

#include <fcntl.h>
#include <unistd.h>

#include "nmt-newt-form.h"
#include "nmt-newt-button.h"
#include "nmt-newt-grid.h"
#include "nmt-newt-utils.h"

G_DEFINE_TYPE(NmtNewtForm, nmt_newt_form, NMT_TYPE_NEWT_CONTAINER)

#define NMT_NEWT_FORM_GET_PRIVATE(o) \
    (G_TYPE_INSTANCE_GET_PRIVATE((o), NMT_TYPE_NEWT_FORM, NmtNewtFormPrivate))

typedef struct {
    newtComponent  form;
    NmtNewtWidget *content;

    guint    x, y, width, height;
    guint    padding;
    gboolean fixed_x, fixed_y;
    gboolean fixed_width, fixed_height;
    char *   title_lc;

    gboolean       dirty, escape_exits;
    NmtNewtWidget *focus;
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
    int scroll_position = 0;
#endif
} NmtNewtFormPrivate;

enum {
    PROP_0,
    PROP_TITLE,
    PROP_FULLSCREEN,
    PROP_FULLSCREEN_VERTICAL,
    PROP_FULLSCREEN_HORIZONTAL,
    PROP_X,
    PROP_Y,
    PROP_WIDTH,
    PROP_HEIGHT,
    PROP_PADDING,
    PROP_ESCAPE_EXITS,

    LAST_PROP
};

enum {
    QUIT,

    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = {0};

static void nmt_newt_form_redraw(NmtNewtForm *form);

/**
 * nmt_newt_form_new:
 * @title: (allow-none): the form title
 *
 * Creates a new form, which will be shown centered on the screen.
 * Compare nmt_newt_form_new_fullscreen(). You can also position a
 * form manually by setting its #NmtNewtForm:x and #NmtNewtForm:y
 * properties at construct time, and/or by setting
 * #NmtNewtForm:fullscreen, #NmtNewtform:fullscreen-horizontal, or
 * #NmtNewtForm:fullscreen-vertical.
 *
 * If @title is NULL, the form will have no title.
 *
 * Returns: a new #NmtNewtForm
 */
NmtNewtForm *
nmt_newt_form_new(const char *title)
{
    return g_object_new(NMT_TYPE_NEWT_FORM, "title", title, NULL);
}

/**
 * nmt_newt_form_new_fullscreen:
 * @title: (allow-none): the form title
 *
 * Creates a new fullscreen form. Compare nmt_newt_form_new().
 *
 * If @title is NULL, the form will have no title.
 *
 * Returns: a new #NmtNewtForm
 */
NmtNewtForm *
nmt_newt_form_new_fullscreen(const char *title)
{
    return g_object_new(NMT_TYPE_NEWT_FORM, "title", title, "fullscreen", TRUE, NULL);
}

static void
nmt_newt_form_init(NmtNewtForm *form)
{
    g_object_ref_sink(form);
}

static void
nmt_newt_form_finalize(GObject *object)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);

    g_free(priv->title_lc);
    g_clear_object(&priv->focus);

    G_OBJECT_CLASS(nmt_newt_form_parent_class)->finalize(object);
}

static void
nmt_newt_form_needs_rebuild(NmtNewtWidget *widget)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(widget);

    if (!priv->dirty) {
        priv->dirty = TRUE;
        nmt_newt_form_redraw(NMT_NEWT_FORM(widget));
    }
}

static void
nmt_newt_form_remove(NmtNewtContainer *container, NmtNewtWidget *widget)
{
    NmtNewtFormPrivate *   priv         = NMT_NEWT_FORM_GET_PRIVATE(container);
    NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_newt_form_parent_class);

    g_return_if_fail(widget == priv->content);

    parent_class->remove(container, widget);
    priv->content = NULL;
}

/**
 * nmt_newt_form_set_content:
 * @form: the #NmtNewtForm
 * @content: the form's content
 *
 * Sets @form's content to be @content.
 */
void
nmt_newt_form_set_content(NmtNewtForm *form, NmtNewtWidget *content)
{
    NmtNewtFormPrivate *   priv         = NMT_NEWT_FORM_GET_PRIVATE(form);
    NmtNewtContainerClass *parent_class = NMT_NEWT_CONTAINER_CLASS(nmt_newt_form_parent_class);

    if (priv->content)
        nmt_newt_form_remove(NMT_NEWT_CONTAINER(form), priv->content);

    priv->content = content;

    if (priv->content)
        parent_class->add(NMT_NEWT_CONTAINER(form), content);
}

static void
nmt_newt_form_build(NmtNewtForm *form)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);
    int                 screen_height, screen_width, form_height, form_width;
    newtComponent *     cos;
    int                 i;

    priv->dirty = FALSE;
    nmt_newt_widget_realize(NMT_NEWT_WIDGET(form));

    nmt_newt_widget_size_request(priv->content, &form_width, &form_height);
    newtGetScreenSize(&screen_width, &screen_height);

    if (!priv->fixed_width)
        priv->width = MIN(form_width + 2 * priv->padding, screen_width - 2);
    if (!priv->fixed_height)
        priv->height = MIN(form_height + 2 * priv->padding, screen_height - 2);

    if (!priv->fixed_x)
        priv->x = (screen_width - form_width) / 2;
    if (!priv->fixed_y)
        priv->y = (screen_height - form_height) / 2;

    nmt_newt_widget_size_allocate(priv->content,
                                  priv->padding,
                                  priv->padding,
                                  priv->width - 2 * priv->padding,
                                  priv->height - 2 * priv->padding);

    if (priv->height - 2 * priv->padding < form_height) {
        newtComponent scroll_bar = newtVerticalScrollbar(priv->width - 1,
                                                         0,
                                                         priv->height,
                                                         NEWT_COLORSET_WINDOW,
                                                         NEWT_COLORSET_ACTCHECKBOX);

        priv->form = newtForm(scroll_bar, NULL, NEWT_FLAG_NOF12);
        newtFormAddComponent(priv->form, scroll_bar);
        newtFormSetHeight(priv->form, priv->height - 2);
    } else
        priv->form = newtForm(NULL, NULL, NEWT_FLAG_NOF12);

    if (priv->escape_exits)
        newtFormAddHotKey(priv->form, NEWT_KEY_ESCAPE);

    cos = nmt_newt_widget_get_components(priv->content);
    for (i = 0; cos[i]; i++)
        newtFormAddComponent(priv->form, cos[i]);
    g_free(cos);

    if (priv->focus) {
        newtComponent fco;

        fco = nmt_newt_widget_get_focus_component(priv->focus);
        if (fco)
            newtFormSetCurrent(priv->form, fco);
    }
#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
    if (priv->scroll_position)
        newtFormSetScrollPosition(priv->form, priv->scroll_position);
#endif

    newtOpenWindow(priv->x, priv->y, priv->width, priv->height, priv->title_lc);
}

static void
nmt_newt_form_destroy(NmtNewtForm *form)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);

#ifdef HAVE_NEWTFORMGETSCROLLPOSITION
    priv->scroll_position = newtFormGetScrollPosition(priv->form);
#endif

    newtFormDestroy(priv->form);
    priv->form = NULL;
    newtPopWindowNoRefresh();

    nmt_newt_widget_unrealize(NMT_NEWT_WIDGET(form));
}

/* A "normal" newt program would call newtFormRun() to run newt's main loop
 * and process events. But we want to let GLib's main loop control the program
 * (eg, so libnm can process D-Bus notifications). So we call this function
 * to run a single iteration of newt's main loop (or rather, to run newt's
 * main loop for 1ms) whenever there are events for newt to process (redrawing
 * or keypresses).
 */
static void
nmt_newt_form_iterate(NmtNewtForm *form)
{
    NmtNewtFormPrivate *  priv = NMT_NEWT_FORM_GET_PRIVATE(form);
    NmtNewtWidget *       focus;
    struct newtExitStruct es;

    if (priv->dirty) {
        nmt_newt_form_destroy(form);
        nmt_newt_form_build(form);
    }

    newtFormSetTimer(priv->form, 1);
    newtFormRun(priv->form, &es);

    if (es.reason == NEWT_EXIT_HOTKEY || es.reason == NEWT_EXIT_ERROR) {
        /* The user hit Esc or there was an error. */
        g_clear_object(&priv->focus);
        nmt_newt_form_quit(form);
        return;
    }

    if (es.reason == NEWT_EXIT_COMPONENT) {
        /* The user hit Return/Space on a component; update the form focus
         * to point that component, and activate it.
         */
        focus = nmt_newt_widget_find_component(priv->content, es.u.co);
        if (focus) {
            nmt_newt_form_set_focus(form, focus);
            nmt_newt_widget_activated(focus);
        }
    } else {
        /* The 1ms timer ran out. Update focus but don't do anything else. */
        focus = nmt_newt_widget_find_component(priv->content, newtFormGetCurrent(priv->form));
        if (focus)
            nmt_newt_form_set_focus(form, focus);
    }
}

/* @form_stack keeps track of all currently-displayed forms, from top to bottom.
 * @keypress_source is the global stdin-monitoring GSource. When it triggers,
 * nmt_newt_form_keypress_callback() iterates the top-most form, so it can
 * process the keypress.
 */
static GSList * form_stack;
static GSource *keypress_source;

static gboolean
nmt_newt_form_keypress_callback(int fd, GIOCondition condition, gpointer user_data)
{
    g_return_val_if_fail(form_stack != NULL, FALSE);

    nmt_newt_form_iterate(form_stack->data);
    return TRUE;
}

static gboolean
nmt_newt_form_timeout_callback(gpointer user_data)
{
    if (form_stack)
        nmt_newt_form_iterate(form_stack->data);
    return FALSE;
}

static void
nmt_newt_form_redraw(NmtNewtForm *form)
{
    g_timeout_add(0, nmt_newt_form_timeout_callback, NULL);
}

static void
nmt_newt_form_real_show(NmtNewtForm *form)
{
    if (!keypress_source) {
        keypress_source = nm_g_unix_fd_source_new(STDIN_FILENO,
                                                  G_IO_IN,
                                                  G_PRIORITY_DEFAULT,
                                                  nmt_newt_form_keypress_callback,
                                                  NULL,
                                                  NULL);
        g_source_set_can_recurse(keypress_source, TRUE);
        g_source_attach(keypress_source, NULL);
    }

    nmt_newt_form_build(form);
    form_stack = g_slist_prepend(form_stack, g_object_ref(form));
    nmt_newt_form_redraw(form);
}

/**
 * nmt_newt_form_show:
 * @form: an #NmtNewtForm
 *
 * Displays @form and begins running it asynchronously in the default
 * #GMainContext. If another form is currently running, it will remain
 * visible in the background, but will not be able to receive keyboard
 * input until @form exits.
 *
 * Call nmt_newt_form_quit() to quit the form.
 */
void
nmt_newt_form_show(NmtNewtForm *form)
{
    NMT_NEWT_FORM_GET_CLASS(form)->show(form);
}

/**
 * nmt_newt_form_run_sync:
 * @form: an #NmtNewtForm
 *
 * Displays @form as with nmt_newt_form_show(), but then iterates the
 * #GMainContext internally until @form exits.
 *
 * Returns: the widget whose activation caused @form to exit, or
 *   %NULL if it was not caused by a widget. FIXME: this exit value is
 *   sort of weird and may not be 100% accurate anyway.
 */
NmtNewtWidget *
nmt_newt_form_run_sync(NmtNewtForm *form)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);

    nmt_newt_form_show(form);
    while (priv->form)
        g_main_context_iteration(NULL, TRUE);

    return priv->focus;
}

/**
 * nmt_newt_form_quit:
 * @form: an #NmtNewtForm
 *
 * Causes @form to exit.
 */
void
nmt_newt_form_quit(NmtNewtForm *form)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);

    g_return_if_fail(priv->form != NULL);

    nmt_newt_form_destroy(form);

    form_stack = g_slist_remove(form_stack, form);

    if (form_stack)
        nmt_newt_form_iterate(form_stack->data);
    else
        nm_clear_g_source_inst(&keypress_source);

    g_signal_emit(form, signals[QUIT], 0);
    g_object_unref(form);
}

/**
 * nmt_newt_form_set_focus:
 * @form: an #NmtNewtForm
 * @widget: the widget to focus
 *
 * Focuses @widget in @form.
 */
void
nmt_newt_form_set_focus(NmtNewtForm *form, NmtNewtWidget *widget)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(form);

    g_return_if_fail(priv->form != NULL);

    if (priv->focus == widget)
        return;

    if (priv->focus)
        g_object_unref(priv->focus);
    priv->focus = widget;
    if (priv->focus)
        g_object_ref(priv->focus);
}

static void
nmt_newt_form_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);
    int                 screen_width, screen_height;

    switch (prop_id) {
    case PROP_TITLE:
        if (g_value_get_string(value)) {
            priv->title_lc = nmt_newt_locale_from_utf8(g_value_get_string(value));
        } else
            priv->title_lc = NULL;
        break;
    case PROP_FULLSCREEN:
        if (g_value_get_boolean(value)) {
            newtGetScreenSize(&screen_width, &screen_height);
            priv->x = priv->y = 2;
            priv->fixed_x = priv->fixed_y = TRUE;
            priv->width                   = screen_width - 4;
            priv->height                  = screen_height - 4;
            priv->fixed_width = priv->fixed_height = TRUE;
        }
        break;
    case PROP_FULLSCREEN_VERTICAL:
        if (g_value_get_boolean(value)) {
            newtGetScreenSize(&screen_width, &screen_height);
            priv->y            = 2;
            priv->fixed_y      = TRUE;
            priv->height       = screen_height - 4;
            priv->fixed_height = TRUE;
        }
        break;
    case PROP_FULLSCREEN_HORIZONTAL:
        if (g_value_get_boolean(value)) {
            newtGetScreenSize(&screen_width, &screen_height);
            priv->x           = 2;
            priv->fixed_x     = TRUE;
            priv->width       = screen_width - 4;
            priv->fixed_width = TRUE;
        }
        break;
    case PROP_X:
        if (g_value_get_uint(value)) {
            priv->x       = g_value_get_uint(value);
            priv->fixed_x = TRUE;
        }
        break;
    case PROP_Y:
        if (g_value_get_uint(value)) {
            priv->y       = g_value_get_uint(value);
            priv->fixed_y = TRUE;
        }
        break;
    case PROP_WIDTH:
        if (g_value_get_uint(value)) {
            priv->width       = g_value_get_uint(value);
            priv->fixed_width = TRUE;
        }
        break;
    case PROP_HEIGHT:
        if (g_value_get_uint(value)) {
            priv->height       = g_value_get_uint(value);
            priv->fixed_height = TRUE;
        }
        break;
    case PROP_PADDING:
        priv->padding = g_value_get_uint(value);
        break;
    case PROP_ESCAPE_EXITS:
        priv->escape_exits = g_value_get_boolean(value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
nmt_newt_form_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NmtNewtFormPrivate *priv = NMT_NEWT_FORM_GET_PRIVATE(object);

    switch (prop_id) {
    case PROP_TITLE:
        if (priv->title_lc) {
            g_value_take_string(value, nmt_newt_locale_to_utf8(priv->title_lc));
        } else
            g_value_set_string(value, NULL);
        break;
    case PROP_X:
        g_value_set_uint(value, priv->x);
        break;
    case PROP_Y:
        g_value_set_uint(value, priv->y);
        break;
    case PROP_WIDTH:
        g_value_set_uint(value, priv->width);
        break;
    case PROP_HEIGHT:
        g_value_set_uint(value, priv->height);
        break;
    case PROP_PADDING:
        g_value_set_uint(value, priv->padding);
        break;
    case PROP_ESCAPE_EXITS:
        g_value_set_boolean(value, priv->escape_exits);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
nmt_newt_form_class_init(NmtNewtFormClass *form_class)
{
    GObjectClass *         object_class    = G_OBJECT_CLASS(form_class);
    NmtNewtContainerClass *container_class = NMT_NEWT_CONTAINER_CLASS(form_class);
    NmtNewtWidgetClass *   widget_class    = NMT_NEWT_WIDGET_CLASS(form_class);

    g_type_class_add_private(form_class, sizeof(NmtNewtFormPrivate));

    /* virtual methods */
    object_class->set_property = nmt_newt_form_set_property;
    object_class->get_property = nmt_newt_form_get_property;
    object_class->finalize     = nmt_newt_form_finalize;

    widget_class->needs_rebuild = nmt_newt_form_needs_rebuild;

    container_class->remove = nmt_newt_form_remove;

    form_class->show = nmt_newt_form_real_show;

    /* signals */

    /**
     * NmtNewtForm::quit:
     * @form: the #NmtNewtForm
     *
     * Emitted when the form quits.
     */
    signals[QUIT] = g_signal_new("quit",
                                 G_OBJECT_CLASS_TYPE(object_class),
                                 G_SIGNAL_RUN_FIRST,
                                 G_STRUCT_OFFSET(NmtNewtFormClass, quit),
                                 NULL,
                                 NULL,
                                 NULL,
                                 G_TYPE_NONE,
                                 0);

    /**
     * NmtNewtForm:title:
     *
     * The form's title. If non-%NULL, this will be displayed above
     * the form in its border.
     */
    g_object_class_install_property(
        object_class,
        PROP_TITLE,
        g_param_spec_string("title",
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:fullscreen:
     *
     * If %TRUE, the form will fill the entire "screen" (ie, terminal
     * window).
     */
    g_object_class_install_property(
        object_class,
        PROP_FULLSCREEN,
        g_param_spec_boolean("fullscreen",
                             "",
                             "",
                             FALSE,
                             G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:fullscreen-vertical:
     *
     * If %TRUE, the form will fill the entire "screen" (ie, terminal
     * window) vertically, but not necessarily horizontally.
     */
    g_object_class_install_property(
        object_class,
        PROP_FULLSCREEN_VERTICAL,
        g_param_spec_boolean("fullscreen-vertical",
                             "",
                             "",
                             FALSE,
                             G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:fullscreen-horizontal:
     *
     * If %TRUE, the form will fill the entire "screen" (ie, terminal
     * window) horizontally, but not necessarily vertically.
     */
    g_object_class_install_property(
        object_class,
        PROP_FULLSCREEN_HORIZONTAL,
        g_param_spec_boolean("fullscreen-horizontal",
                             "",
                             "",
                             FALSE,
                             G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:x:
     *
     * The form's x coordinate. By default, the form will be centered
     * on the screen.
     */
    g_object_class_install_property(
        object_class,
        PROP_X,
        g_param_spec_uint("x",
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          0,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:y:
     *
     * The form's y coordinate. By default, the form will be centered
     * on the screen.
     */
    g_object_class_install_property(
        object_class,
        PROP_Y,
        g_param_spec_uint("y",
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          0,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:width:
     *
     * The form's width. By default, this will be determined by the
     * width of the form's content.
     */
    g_object_class_install_property(
        object_class,
        PROP_WIDTH,
        g_param_spec_uint("width",
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          0,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:height:
     *
     * The form's height. By default, this will be determined by the
     * height of the form's content.
     */
    g_object_class_install_property(
        object_class,
        PROP_HEIGHT,
        g_param_spec_uint("height",
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          0,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:padding:
     *
     * The padding between the form's content and its border.
     */
    g_object_class_install_property(
        object_class,
        PROP_PADDING,
        g_param_spec_uint("padding",
                          "",
                          "",
                          0,
                          G_MAXUINT,
                          1,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
    /**
     * NmtNewtForm:escape-exits:
     *
     * If %TRUE, then hitting the Escape key will cause the form to
     * exit.
     */
    g_object_class_install_property(
        object_class,
        PROP_ESCAPE_EXITS,
        g_param_spec_boolean("escape-exits",
                             "",
                             "",
                             FALSE,
                             G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY));
}
