/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2021-2025 Alfonso Sabato Siciliano
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <curses.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>

#include "bsddialog.h"
#include "bsddialog_theme.h"
#include "lib_util.h"

enum field_action {
	MOVE_CURSOR_BEGIN,
	MOVE_CURSOR_END,
	MOVE_CURSOR_RIGHT,
	MOVE_CURSOR_LEFT,
	DEL_LETTER
};

struct privateitem {
	const char *label;      /* formitem.label */
	unsigned int ylabel;    /* formitem.ylabel */
	unsigned int xlabel;    /* formitem.xlabel */
	unsigned int yfield;    /* formitem.yfield */
	unsigned int xfield;    /* formitem.xfield */
	bool secure;            /* formitem.flags & BSDDIALOG_FIELDHIDDEN */
	bool readonly;          /* formitem.flags & BSDDIALOG_FIELDREADONLY */
	bool fieldnocolor;      /* formitem.flags & BSDDIALOG_FIELDNOCOLOR */
	bool extendfield;       /* formitem.flags & BSDDIALOG_FIELDEXTEND */
	bool fieldonebyte;      /* formitem.flags & BSDDIALOG_FIELDSINGLEBYTE */
	bool cursorend;         /* formitem.flags & BSDDIALOG_FIELDCURSOREND */
	bool cursor;            /* field cursor visibility */
	const char *bottomdesc; /* formitem.bottomdesc */

	wchar_t *privwbuf;       /* formitem.value */
	wchar_t *pubwbuf;        /* string for drawitem() */
	unsigned int maxletters; /* formitem.maxvaluelen, [priv|pub]wbuf size */
	unsigned int nletters;   /* letters in privwbuf and pubwbuf */
	unsigned int pos;        /* pos in privwbuf and pubwbuf */
	unsigned int fieldcols;  /* formitem.fieldlen */
	unsigned int xcursor;    /* position in fieldcols [0 - fieldcols-1] */
	unsigned int xposdraw;   /* first pubwbuf index to draw */
};

struct privateform {
	WINDOW *box;         /* window to draw borders */
	WINDOW *pad;
	unsigned int h;      /* only to create pad */
	unsigned int w;      /* only to create pad */
	unsigned int wmin;   /* to refresh, w can change for FIELDEXTEND */
	unsigned int ys;     /* to refresh */
	unsigned int ye;     /* to refresh */
	unsigned int xs;     /* to refresh */
	unsigned int xe;     /* to refresh */
	unsigned int y;      /* changes moving focus around items */
	unsigned int formheight;  /* API formheight */
	unsigned int viewrows;    /* visible rows, real formheight */
	unsigned int minviewrows; /* min viewrows, ylabel != yfield */
	wchar_t securewch;   /* wide char of conf.form.secure[mb]ch */
	unsigned int nitems; /* like API nitems */
	struct privateitem *pritems;
	int sel;             /* selected item in pritem, can be -1 */
	bool hasbottomdesc;  /* some item has bottomdesc */
};

static int
build_privateform(struct bsddialog_conf*conf, unsigned int nitems,
    struct bsddialog_formitem *items, struct privateform *f)
{
	bool insecurecursor;
	int mbchsize;
	unsigned int i, j, itemybeg, itemxbeg, tmp;
	wchar_t *winit;
	struct privateitem *item;

	/* checks */
	CHECK_ARRAY(nitems, items);
	for (i = 0; i < nitems; i++) {
		if (items[i].fieldlen == 0)
			RETURN_FMTERROR("item %u [0-%u] fieldlen = 0",
			    i, nitems);
		if (items[i].maxvaluelen == 0)
			RETURN_FMTERROR("item %u [0-%u] maxvaluelen = 0",
			    i, nitems);
	}
	f->nitems = nitems;

	/* insecure ch */
	insecurecursor = false;
	if (conf->form.securembch != NULL) {
		mbchsize = mblen(conf->form.securembch, MB_LEN_MAX);
		if (mbtowc(&f->securewch, conf->form.securembch, mbchsize) < 0)
			RETURN_ERROR("Cannot convert securembch to wchar_t");
		insecurecursor = true;
	} else if (conf->form.securech != '\0') {
		f->securewch = btowc(conf->form.securech);
		insecurecursor = true;
	} else {
		f->securewch = L' ';
	}

	/* alloc and set private items */
	f->pritems = malloc(f->nitems * sizeof(struct privateitem));
	if (f->pritems == NULL)
		RETURN_ERROR("Cannot allocate internal form.pritems");
	f->hasbottomdesc = false;
	f->h = f->w = f->minviewrows = 0;
	for (i = 0; i < f->nitems; i++) {
		item = &f->pritems[i];
		item->label = CHECK_STR(items[i].label);
		item->ylabel = items[i].ylabel;
		item->xlabel = items[i].xlabel;
		item->yfield = items[i].yfield;
		item->xfield = items[i].xfield;
		item->secure = items[i].flags & BSDDIALOG_FIELDHIDDEN;
		item->readonly = items[i].flags & BSDDIALOG_FIELDREADONLY;
		item->fieldnocolor = items[i].flags & BSDDIALOG_FIELDNOCOLOR;
		item->extendfield = items[i].flags & BSDDIALOG_FIELDEXTEND;
		item->fieldonebyte = items[i].flags &
		    BSDDIALOG_FIELDSINGLEBYTE;
		item->cursorend = items[i].flags & BSDDIALOG_FIELDCURSOREND;
		item->bottomdesc = CHECK_STR(items[i].bottomdesc);
		if (items[i].bottomdesc != NULL)
			f->hasbottomdesc = true;
		if (item->readonly || (item->secure && !insecurecursor))
			item->cursor = false;
		else
			item->cursor = true;

		item->maxletters = items[i].maxvaluelen;
		item->privwbuf = calloc(item->maxletters + 1, sizeof(wchar_t));
		if (item->privwbuf == NULL)
			RETURN_ERROR("Cannot allocate item private buffer");
		memset(item->privwbuf, 0, item->maxletters + 1);
		item->pubwbuf = calloc(item->maxletters + 1, sizeof(wchar_t));
		if (item->pubwbuf == NULL)
			RETURN_ERROR("Cannot allocate item private buffer");
		memset(item->pubwbuf, 0, item->maxletters + 1);

		if ((winit = alloc_mbstows(CHECK_STR(items[i].init))) == NULL)
			RETURN_ERROR("Cannot allocate item.init in wchar_t*");
		wcsncpy(item->privwbuf, winit, item->maxletters);
		wcsncpy(item->pubwbuf, winit, item->maxletters);
		free(winit);
		item->nletters = wcslen(item->pubwbuf);
		if (item->secure) {
			for (j = 0; j < item->nletters; j++)
				item->pubwbuf[j] = f->securewch;
		}

		item->fieldcols = items[i].fieldlen;
		item->xposdraw = 0;
		item->xcursor = 0;
		item->pos = 0;

		/* size and position */
		f->h = MAX(f->h, item->ylabel);
		f->h = MAX(f->h, item->yfield);
		f->w = MAX(f->w, item->xlabel + strcols(item->label));
		f->w = MAX(f->w, item->xfield + item->fieldcols);
		if (i == 0) {
			itemybeg = MIN(item->ylabel, item->yfield);
			itemxbeg = MIN(item->xlabel, item->xfield);
		} else {
			tmp = MIN(item->ylabel, item->yfield);
			itemybeg = MIN(itemybeg, tmp);
			tmp = MIN(item->xlabel, item->xfield);
			itemxbeg = MIN(itemxbeg, tmp);
		}
		tmp = abs((int)item->ylabel - (int)item->yfield);
		f->minviewrows = MAX(f->minviewrows, tmp);
	}
	if (f->nitems > 0) {
		f->h = f->h + 1 - itemybeg;
		f->w -= itemxbeg;
		f->minviewrows += 1;
	}
	f->wmin = f->w;
	for (i = 0; i < f->nitems; i++) {
		f->pritems[i].ylabel -= itemybeg;
		f->pritems[i].yfield -= itemybeg;
		f->pritems[i].xlabel -= itemxbeg;
		f->pritems[i].xfield -= itemxbeg;
	}

	return (0);
}

static bool fieldctl(struct privateitem *item, enum field_action act)
{
	bool change;
	int width, oldwidth, nextwidth, cols;
	unsigned int i;

	change = false;
	switch (act){
	case MOVE_CURSOR_BEGIN:
		if (item->pos == 0 && item->xcursor == 0)
			break;
		/* here the cursor is changed */
		change = true;
		item->pos = 0;
		item->xcursor = 0;
		item->xposdraw = 0;
		break;
	case MOVE_CURSOR_END:
		while (fieldctl(item, MOVE_CURSOR_RIGHT))
			change = true;
		break;
	case MOVE_CURSOR_LEFT:
		if (item->pos == 0)
			break;
		/* check redundant by item->pos == 0 because of 'while' below */
		if (item->xcursor == 0 && item->xposdraw == 0)
			break;
		/* here some letter to left */
		change = true;
		item->pos -= 1;
		width = wcwidth(item->pubwbuf[item->pos]);
		if (((int)item->xcursor) - width < 0) {
			item->xcursor = 0;
			item->xposdraw -= 1;
		} else
			item->xcursor -= width;

		while (true) {
			if (item->xposdraw == 0)
				break;
			if (item->xcursor >= item->fieldcols / 2)
				break;
			if (wcwidth(item->pubwbuf[item->xposdraw - 1]) +
			    item->xcursor + width > item->fieldcols)
				break;

			item->xposdraw -= 1;
			item->xcursor +=
			    wcwidth(item->pubwbuf[item->xposdraw]);
		}
		break;
	case DEL_LETTER:
		if (item->nletters == 0)
			break;
		if (item->pos == item->nletters)
			break;
		/* here a letter under the cursor */
		change = true;
		for (i = item->pos; i < item->nletters; i++) {
			item->privwbuf[i] = item->privwbuf[i+1];
			item->pubwbuf[i] = item->pubwbuf[i+1];
		}
		item->nletters -= 1;
		item->privwbuf[i] = L'\0';
		item->pubwbuf[i] = L'\0';
		break;
	case MOVE_CURSOR_RIGHT: /* used also by "insert", see handler loop */
		if (item->pos + 1 == item->maxletters)
			break;
		if (item->pos == item->nletters)
			break;
		/* here a change to right */
		change = true;
		oldwidth = wcwidth(item->pubwbuf[item->pos]);
		item->pos += 1;
		if (item->pos == item->nletters) { /* empty column */
			nextwidth = 1;
		} else { /* a letter to right */
			nextwidth = wcwidth(item->pubwbuf[item->pos]);
		}
		if (item->xcursor + oldwidth + nextwidth - 1 >= item->fieldcols) {
			cols = nextwidth;
			item->xposdraw = item->pos;
			while (item->xposdraw != 0) {
				cols += wcwidth(item->pubwbuf[item->xposdraw - 1]);
				if (cols > (int)item->fieldcols)
					break;
				item->xposdraw -= 1;
			}
			item->xcursor = 0;
			for (i = item->xposdraw; i < item->pos ; i++)
				item->xcursor += wcwidth(item->pubwbuf[i]);
		}
		else {
			item->xcursor += oldwidth;
		}

		break;
	}

	return (change);
}

static bool insertch(struct privateitem *item, wchar_t wch, wchar_t securewch)
{
	int i;

	if (item->nletters >= item->maxletters)
		return (false);

	for (i = (int)item->nletters - 1; i >= (int)item->pos; i--) {
		item->privwbuf[i+1] = item->privwbuf[i];
		item->pubwbuf[i+1] = item->pubwbuf[i];
	}

	item->privwbuf[item->pos] = wch;
	item->pubwbuf[item->pos] = item->secure ? securewch : wch;
	item->nletters += 1;
	item->privwbuf[item->nletters] = L'\0';
	item->pubwbuf[item->nletters] = L'\0';

	return (true);
}

static char* alloc_wstomb(wchar_t *wstr)
{
	int len, nbytes, i;
	char mbch[MB_LEN_MAX], *mbstr;

	nbytes = MB_LEN_MAX; /* to ensure a null terminated string */
	len = wcslen(wstr);
	for (i = 0; i < len; i++) {
		wctomb(mbch, wstr[i]);
		nbytes += mblen(mbch, MB_LEN_MAX);
	}
	if ((mbstr = malloc(nbytes)) == NULL)
		return (NULL);

	wcstombs(mbstr,	wstr, nbytes);

	return (mbstr);
}

static int
return_values(struct bsddialog_conf *conf, struct privateform *f,
    struct bsddialog_formitem *items)
{
	unsigned int i;

	for (i = 0; i < f->nitems; i++) {
		if (conf->form.value_wchar)
			items[i].value = (char*)wcsdup(f->pritems[i].privwbuf);
		else
			items[i].value = alloc_wstomb(f->pritems[i].privwbuf);

		if (items[i].value == NULL)
			RETURN_FMTERROR(
			    "Cannot allocate memory for item[%d].value", i);
	}

	return (0);
}

static void set_first_with_default(struct privateform *f, int *focusitem)
{
	unsigned int i;

	f->sel = -1;
	if (focusitem != NULL && *focusitem >=0 && *focusitem < (int)f->nitems)
		if (f->pritems[*focusitem].readonly == false) {
			f->sel = *focusitem;
			return;
		}
	for (i = 0 ; i < f->nitems; i++)
		if (f->pritems[i].readonly == false) {
			f->sel = i;
			break;
		}
}

static unsigned int firstitem(unsigned int nitems, struct privateitem *items)
{
	int i;

	for (i = 0; i < (int)nitems; i++)
		if (items[i].readonly == false)
			break;

	return (i);
}

static unsigned int lastitem(unsigned int nitems, struct privateitem *items)
{
	int i;

	for (i = nitems - 1; i >= 0 ; i--)
		if (items[i].readonly == false)
			break;

	return (i);
}

static unsigned int
previtem(unsigned int nitems, struct privateitem *items, int curritem)
{
	int i;

	for (i = curritem - 1; i >= 0; i--)
		if (items[i].readonly == false)
			return(i);

	for (i = nitems - 1; i > curritem - 1; i--)
		if (items[i].readonly == false)
			return(i);

	return (curritem);
}

static unsigned int
nextitem(unsigned int nitems, struct privateitem *items, int curritem)
{
	int i;

	for (i = curritem + 1; i < (int)nitems; i++)
		if (items[i].readonly == false)
			return(i);

	for (i = 0; i < curritem; i++)
		if (items[i].readonly == false)
			return(i);

	return (curritem);
}

static void redrawbuttons(struct dialog *d, bool focus, bool shortcut)
{
	int selected;

	selected = d->bs.curr;
	if (focus == false)
		d->bs.curr = -1;
	d->bs.shortcut = shortcut;
	draw_buttons(d);
	d->bs.curr = selected;
}

static void
drawitem(struct privateform *f, int idx, bool focus)
{
	int color;
	unsigned int n, cols;
	struct privateitem *item;

	item = &f->pritems[idx];

	/* Label */
	wattron(f->pad, t.dialog.color);
	mvwaddstr(f->pad, item->ylabel, item->xlabel, item->label);
	wattroff(f->pad, t.dialog.color);

	/* Field */
	if (item->readonly)
		color = t.form.readonlycolor;
	else if (item->fieldnocolor)
		color = t.dialog.color;
	else
		color = focus ? t.form.f_fieldcolor : t.form.fieldcolor;
	wattron(f->pad, color);
	mvwhline(f->pad, item->yfield, item->xfield, ' ', item->fieldcols);
	n = 0;
	cols = wcwidth(item->pubwbuf[item->xposdraw]);
	while (cols <= item->fieldcols &&
	    item->xposdraw + n < wcslen(item->pubwbuf)) {
		n++;
		cols += wcwidth(item->pubwbuf[item->xposdraw + n]);

	}
	mvwaddnwstr(f->pad, item->yfield, item->xfield,
	    &item->pubwbuf[item->xposdraw], n);
	wattroff(f->pad, color);

	/* Bottom Desc */
	if (f->hasbottomdesc) {
		move(SCREENLINES - 1, 2);
		clrtoeol();
		if (item->bottomdesc != NULL && focus) {
			attron(t.form.bottomdesccolor);
			addstr(item->bottomdesc);
			attroff(t.form.bottomdesccolor);
			refresh();
		}
	}

	/* Cursor */
	curs_set((focus && item->cursor) ? 1 : 0);
	wmove(f->pad, item->yfield, item->xfield + item->xcursor);
}

/*
 * Trick: draw 2 times an item switching focus.
 * Problem: curses tries to optimize the rendering but sometimes it misses some
 * updates or draws old stuff. libformw has a similar problem fixed by the
 * same trick.
 * Case 1: KEY_DC and KEY_BACKSPACE, deleted multicolumn letters are drawn
 * again. It seems fixed by new items pad and prefresh(), previously WINDOW.
 * Case2: some terminal, tmux and ssh does not show the cursor.
 */
#define DRAWITEM_TRICK(f, idx, focus) do {                                     \
	drawitem(f, idx, !focus);                                              \
	prefresh((f)->pad, (f)->y, 0, (f)->ys, (f)->xs, (f)->ye, (f)->xe);     \
	drawitem(f, idx, focus);                                               \
	prefresh((f)->pad, (f)->y, 0, (f)->ys, (f)->xs, (f)->ye, (f)->xe);     \
} while (0)

static void update_formbox(struct bsddialog_conf *conf, struct privateform *f)
{
	int h, w;

	getmaxyx(f->box, h, w);
	draw_borders(conf, f->box, LOWERED);

	if (f->viewrows < f->h) {
		wattron(f->box, t.dialog.arrowcolor);
		if (f->y > 0)
			mvwhline(f->box, 0, (w / 2) - 2, UARROW(conf), 5);

		if (f->y + f->viewrows < f->h)
			mvwhline(f->box, h-1, (w / 2) - 2, DARROW(conf), 5);
		wattroff(f->box, t.dialog.arrowcolor);
	}
}

static void curriteminview(struct privateform *f, struct privateitem *item)
{
	unsigned int yup, ydown;

	yup = MIN(item->ylabel, item->yfield);
	ydown = MAX(item->ylabel, item->yfield);

	/* selected item in view */
	if (f->y > yup && f->y > 0)
		f->y = yup;
	if ((int)(f->y + f->viewrows) - 1 < (int)ydown)
		f->y = ydown - f->viewrows + 1;
	/* lower pad after a terminal expansion */
	if (f->y > 0 && (f->h - f->y) < f->viewrows)
		f->y = f->h - f->viewrows;
}

static int form_size_position(struct dialog *d, struct privateform *f)
{
	int htext, hform;

	if (set_widget_size(d->conf, d->rows, d->cols, &d->h, &d->w) != 0)
		return (BSDDIALOG_ERROR);

	/* autosize */
	hform = (int) f->viewrows;
	if (f->viewrows == BSDDIALOG_AUTOSIZE)
		hform = MAX(f->h, f->minviewrows);
	hform += 2; /* formborders */

	if (set_widget_autosize(d->conf, d->rows, d->cols, &d->h, &d->w,
	    d->text, &htext, &d->bs, hform, f->w + 4) != 0)
		return (BSDDIALOG_ERROR);
	/* formheight: avoid overflow, "at most" and at least minviewrows */
	if (d->h - BORDERS - htext - HBUTTONS < 2 + (int)f->minviewrows) {
		f->viewrows = f->minviewrows; /* for widget_checksize() */
	} else if (f->viewrows == BSDDIALOG_AUTOSIZE) {
		f->viewrows = MIN(d->h - BORDERS - htext - HBUTTONS, hform) - 2;
		f->viewrows = MAX(f->viewrows, f->minviewrows);
	} else {
		f->viewrows = MIN(d->h - BORDERS - htext - HBUTTONS, hform) - 2;
	}

	/* checksize */
	if (f->viewrows < f->minviewrows)
		RETURN_FMTERROR("formheight, current: %u needed at least %u",
		    f->viewrows, f->minviewrows);
	if (widget_checksize(d->h, d->w, &d->bs,
	    2 /* borders */ + f->minviewrows, f->w + 4) != 0)
		return (BSDDIALOG_ERROR);

	if (set_widget_position(d->conf, &d->y, &d->x, d->h, d->w) != 0)
		return (BSDDIALOG_ERROR);

	return (0);
}

static int
form_draw(struct dialog *d, bool redraw, struct privateform *f, bool focusinform)
{
	unsigned int i;

	if (redraw) {
		hide_dialog(d);
		refresh(); /* Important for decreasing screen */
	}
	f->viewrows = f->formheight;
	f->w = f->wmin;
	if (form_size_position(d, f) != 0)
		return (BSDDIALOG_ERROR);
	if (draw_dialog(d) != 0) /* doupdate() in main loop */
		return (BSDDIALOG_ERROR);
	if (redraw)
		refresh(); /* Important to fix grey lines expanding screen */
	TEXTPAD(d, 2 /* box borders */ + f->viewrows + HBUTTONS);

	update_box(d->conf, f->box, d->y + d->h - 5 - f->viewrows, d->x + 2,
	    f->viewrows + 2, d->w - 4, LOWERED);

	for (i = 0; i < f->nitems; i++) {
		fieldctl(&f->pritems[i], MOVE_CURSOR_BEGIN);
		if (f->pritems[i].extendfield) {
			f->w = d->w - 6;
			f->pritems[i].fieldcols = f->w - f->pritems[i].xfield;
		}
		if (f->pritems[i].cursorend)
			fieldctl(&f->pritems[i], MOVE_CURSOR_END);
	}

	wresize(f->pad, f->h, f->w);
	for (i = 0; i < f->nitems; i++)
		drawitem(f, i, false);

	f->ys = d->y + d->h - 5 - f->viewrows + 1;
	f->ye = d->y + d->h - 5 ;
	if ((int)f->w >= d->w - 6) { /* left */
		f->xs = d->x + 3;
		f->xe = f->xs + d->w - 7;
	} else { /* center */
		f->xs = d->x + 3 + (d->w - 6)/2 - f->w/2;
		f->xe = f->xs + d->w - 5;
	}

	if (f->sel != -1) { /* at least 1 writable item */
		redrawbuttons(d,
		    d->conf->button.always_active || !focusinform,
		    !focusinform);
		wnoutrefresh(d->widget);
		curriteminview(f, &f->pritems[f->sel]);
		update_formbox(d->conf, f);
		wnoutrefresh(f->box);
		DRAWITEM_TRICK(f, f->sel, focusinform);
	} else if (f->sel == -1 && f->nitems > 0) { /* all read only */
		redrawbuttons(d, true, true);
		wnoutrefresh(d->widget);
		update_formbox(d->conf, f);
		wnoutrefresh(f->box);
		DRAWITEM_TRICK(f, 0, false); /* to refresh pad*/
	} else { /* no item */
		wnoutrefresh(f->box);
	}

	return (0);
}

/* API */
int
bsddialog_form(struct bsddialog_conf *conf, const char *text, int rows,
    int cols, unsigned int formheight, unsigned int nitems,
    struct bsddialog_formitem *items, int *focusitem)
{
	bool switchfocus, changeitem, focusinform, loop;
	int next, retval, wchtype;
	unsigned int i;
	wint_t input;
	struct privateitem *item;
	struct privateform form;
	struct dialog d;

	if (prepare_dialog(conf, text, rows, cols, &d) != 0)
		return (BSDDIALOG_ERROR);
	set_buttons(&d, true, OK_LABEL, CANCEL_LABEL);

	if (build_privateform(conf, nitems, items, &form) != 0)
		return (BSDDIALOG_ERROR);

	if ((form.box = newwin(1, 1, 1, 1)) == NULL)
		RETURN_ERROR("Cannot build WINDOW form box");
	wbkgd(form.box, t.dialog.color);
	if ((form.pad = newpad(1, 1)) == NULL)
		RETURN_ERROR("Cannot build WINDOW form pad");
	wbkgd(form.pad, t.dialog.color);

	set_first_with_default(&form, focusitem);
	if (form.sel != -1) {
		focusinform = true;
		form.y = 0;
		item = &form.pritems[form.sel];
	} else {
		item = NULL;
		focusinform = false;
	}

	form.formheight = formheight;
	if (form_draw(&d, false, &form, focusinform) != 0)
		return (BSDDIALOG_ERROR);

	changeitem = switchfocus = false;
	loop = true;
	while (loop) {
		doupdate();
		if ((wchtype = get_wch(&input)) == ERR)
			continue;
		switch(input) {
		case KEY_ENTER:
		case 10: /* Enter */
			if (focusinform && conf->button.always_active == false) {
				next = nextitem(form.nitems, form.pritems, form.sel);
				if (next > form.sel)
					changeitem = true; /* needs next */
				else
					switchfocus = true;
			} else {
				retval = BUTTONVALUE(d.bs);
				loop = false;
			}
			break;
		case 27: /* Esc */
			if (conf->key.enable_esc) {
				retval = BSDDIALOG_ESC;
				loop = false;
			}
			break;
		case '\t': /* TAB */
			if (focusinform) {
				next = nextitem(form.nitems, form.pritems,
				    form.sel);
				if (next > form.sel)
					changeitem = true;  /* needs next */
				else
					switchfocus = true;
			} else {
				if (d.bs.curr + 1 < (int)d.bs.nbuttons) {
					d.bs.curr++;
				} else {
					d.bs.curr = 0;
					if (form.sel != -1) {
						switchfocus = true;
					}
				}
				redrawbuttons(&d, true, true);
				wnoutrefresh(d.widget);
			}
			break;
		case KEY_LEFT:
			if (focusinform) {
				if (fieldctl(item, MOVE_CURSOR_LEFT))
					DRAWITEM_TRICK(&form, form.sel, true);
			} else if (d.bs.curr > 0) {
				d.bs.curr--;
				redrawbuttons(&d, true, true);
				wnoutrefresh(d.widget);
			} else if (form.sel != -1) {
				switchfocus = true;
			}
			break;
		case KEY_RIGHT:
			if (focusinform) {
				if (fieldctl(item, MOVE_CURSOR_RIGHT))
					DRAWITEM_TRICK(&form, form.sel, true);
			} else if (d.bs.curr < (int) d.bs.nbuttons - 1) {
				d.bs.curr++;
				redrawbuttons(&d, true, true);
				wnoutrefresh(d.widget);
			} else if (form.sel != -1) {
				switchfocus = true;
			}
			break;
		case KEY_CTRL('p'):
		case KEY_UP:
			if (focusinform) {
				next = previtem(form.nitems, form.pritems,
				    form.sel);
				changeitem = form.sel != next;
			} else if (form.sel != -1) {
				switchfocus = true;
			}
			break;
		case KEY_CTRL('n'):
		case KEY_DOWN:
			if (focusinform == false)
				break;
			if (form.nitems == 1) {
				switchfocus = true;
			} else {
				next = nextitem(form.nitems, form.pritems,
				    form.sel);
				changeitem = form.sel != next;
			}
			break;
		case KEY_PPAGE:
			if (focusinform) {
				next = firstitem(form.nitems, form.pritems);
				changeitem = form.sel != next;
			}
			break;
		case KEY_NPAGE:
			if (focusinform) {
				next = lastitem(form.nitems, form.pritems);
				changeitem = form.sel != next;
			}
			break;
		case KEY_BACKSPACE:
		case 127: /* Backspace */
			if (focusinform == false)
				break;
			if (fieldctl(item, MOVE_CURSOR_LEFT))
				if (fieldctl(item, DEL_LETTER))
					DRAWITEM_TRICK(&form, form.sel, true);
			break;
		case KEY_DC:
			if (focusinform == false)
				break;
			if (fieldctl(item, DEL_LETTER))
				DRAWITEM_TRICK(&form, form.sel, true);
			break;
		case KEY_HOME:
			if (focusinform == false)
				break;
			if (fieldctl(item, MOVE_CURSOR_BEGIN))
				DRAWITEM_TRICK(&form, form.sel, true);
			break;
		case KEY_END:
			if (focusinform == false)
				break;
			if (fieldctl(item, MOVE_CURSOR_END))
				DRAWITEM_TRICK(&form, form.sel, true);
			break;
		case KEY_F(1):
			if (conf->key.f1_file == NULL &&
			    conf->key.f1_message == NULL)
				break;
			curs_set(0);
			if (f1help_dialog(conf) != 0) {
				retval = BSDDIALOG_ERROR;
				loop = false;
			}
			if (form_draw(&d, true, &form, focusinform) != 0)
				return (BSDDIALOG_ERROR);
			break;
		case KEY_CTRL('l'):
		case KEY_RESIZE:
			if (form_draw(&d, true, &form, focusinform) != 0)
				return (BSDDIALOG_ERROR);
			break;
		default:
			if (wchtype == KEY_CODE_YES)
				break;
			if (focusinform) {
				if (item->fieldonebyte && wctob(input) == EOF)
					break;
				/*
				 * MOVE_CURSOR_RIGHT manages new positions
				 * because the cursor remains on the new letter,
				 * "if" and "while" update the positions.
				 */
				if (insertch(item, input, form.securewch)) {
					fieldctl(item, MOVE_CURSOR_RIGHT);
					/*
					 * no if (fieldctl), update always
					 * because it fails with maxletters.
					 */
					DRAWITEM_TRICK(&form, form.sel, true);
				}
			} else {
				if (shortcut_buttons(input, &d.bs)) {
					DRAW_BUTTONS(d);
					doupdate();
					retval = BUTTONVALUE(d.bs);
					loop = false;
				}
			}
			break;
		} /* end switch get_wch() */

		if (switchfocus) {
			focusinform = !focusinform;
			d.bs.curr = 0;
			redrawbuttons(&d,
			    conf->button.always_active || !focusinform,
			    !focusinform);
			wnoutrefresh(d.widget);
			if (focusinform == false)
				DRAWITEM_TRICK(&form, form.sel, false);
			else {
				next = firstitem(form.nitems, form.pritems);
				if (next == form.sel)
					DRAWITEM_TRICK(&form, form.sel, true);
				else
					changeitem = true;
			}
			switchfocus = false;
		}

		if (changeitem) {
			/* useless after if(switchfocus) */
			DRAWITEM_TRICK(&form, form.sel, false);
			form.sel = next;
			item = &form.pritems[form.sel];
			curriteminview(&form, item);
			update_formbox(conf, &form);
			wnoutrefresh(form.box);
			DRAWITEM_TRICK(&form, form.sel, true);
			changeitem = false;
		}
	} /* end while (loop) */

	curs_set(0);

	if (return_values(conf, &form, items) == BSDDIALOG_ERROR)
		return (BSDDIALOG_ERROR);

	if (focusitem != NULL)
		*focusitem = form.sel;

	if (form.hasbottomdesc && conf->clear) {
		move(SCREENLINES - 1, 2);
		clrtoeol();
	}
	for (i = 0; i < form.nitems; i++) {
		free(form.pritems[i].privwbuf);
		free(form.pritems[i].pubwbuf);
	}
	delwin(form.pad);
	delwin(form.box);
	end_dialog(&d);

	return (retval);
}
