// SPDX-License-Identifier: GPL-2.0+
/*
 * 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 "nm-default.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 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));
}
