/*
 * Copyright (c)2004 Cat's Eye Technologies.  All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *   Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 
 *   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.
 * 
 *   Neither the name of Cat's Eye Technologies nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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
 * COPYRIGHT HOLDERS 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. 
 */

/*
 * curses_xlat.c
 * Translate DFUI forms to curses forms.
 * $Id: curses_xlat.c,v 1.3 2004/07/11 17:21:11 cpressey Exp $
 */

#include <sys/time.h>

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#include <dfui/dfui.h>
#include <dfui/mem.h>
#include <dfui/dump.h>

#include "curses_form.h"
#include "curses_widget.h"
#include "curses_util.h"
#include "curses_xlat.h"

#define MAX(a, b) (a > b ? a : b)
#define MIN(a, b) (a < b ? a : b)

/*** CALLBACKS ***/

struct timeval last_update;

/*
 * Callback to give to curses_button_set_click_cb, for buttons
 * that remove the same row of widgets that they are on.
 */
int
cb_click_remove_row(struct curses_widget *w)
{
	struct curses_form *cf = w->form;
	struct curses_widget *few;
	int id = w->user_id;

	/*
	 * Since we're going to be deleting the widget with
	 * the focus, first move the focus onto a widget
	 * that we won't be deleting.
	 */
	do {
		if (cf->widget_focus == NULL)
			cf->widget_focus = cf->widget_head;
		while (cf->widget_focus->user_id == id)
			cf->widget_focus = cf->widget_focus->prev;
	} while (cf->widget_focus == NULL);

	/*
	 * Delete all widgets with the same id as the focused one.
	 */
	for (few = cf->widget_head; few != NULL; few = few->next) {
		if (few->user_id == id) {
			curses_form_widget_remove(few);
			/*
			 * Reset the iterator, as the previous command
			 * may have obliterated the current widget.
			 */
			few = cf->widget_head;
		}
	}

	/*
	 * Slide the remaining widgets up a row.
	 */
	for (few = cf->widget_head; few != NULL; few = few->next) {
		if (few->user_id > id) {
			/*
			 * Slide the rows below the deleted row up one row.
			 */
			few->user_id--;
			few->y--;
		} else if (few->user_id == -1) {
			/*
			 * Slide the buttons, too.
			 */
			few->y--;
		}
	}

	cf->int_height--;

	/*
	 * Now that the widgets are deleted, make sure the focus is
	 * on a usable widget (not a label.)
	 */
	curses_form_focus_skip_forward(cf);
	cf->want_y = cf->widget_focus->y;

	/*
	 * Repaint the form.  XXX Might not be necessary anymore?
	 */
	curses_form_draw(cf);
	curses_form_refresh(cf);
	return(0);
}

/*
 * XXX this should maybe be in libdfui.
 */
struct dfui_dataset *
create_default_dataset(struct dfui_form *f)
{
	struct dfui_dataset *ds;
	struct dfui_field *fi;

	ds = dfui_dataset_new();
	for (fi = dfui_form_field_get_first(f); fi != NULL;
	     fi = dfui_field_get_next(fi)) {
		dfui_dataset_celldata_add(ds,
		     dfui_id_dup(dfui_field_get_id(fi)), "");
	}
		
	return(ds);
}

/*
 * Callback to give to curses_button_set_click_cb, for buttons
 * that insert a row of widgets before the row that they are on.
 */
int
cb_click_insert_row(struct curses_widget *w)
{
	struct curses_form *cf = w->form;
	struct curses_widget *few, *lw;
	int id = w->user_id;
	int top = w->y;
	struct dfui_dataset *ds;
	struct curses_form_userdata *cfu = cf->userdata;

	/*
	 * Find the last widget in the tab order that is of the prev row.
	 */

	for (lw = w; lw != NULL; lw = lw->prev) {
		if (lw->user_id == id - 1)
			break;
	}

	/*
	 * Slide widgets below the row we're going to insert, down.
	 */
	for (few = cf->widget_head; few != NULL; few = few->next) {
		if (few->user_id >= id) {
			/*
			 * Slide the rows below the deleted row up one row.
			 */
			few->user_id++;
			few->y++;
		} else if (few->user_id == -1) {
			/*
			 * Slide the buttons, too.
			 */
			few->y++;
		}
	}
	cf->int_height++;
	
	/*
	 * Insert a new row of widgets.
	 */
	ds = create_default_dataset(cfu->f);
	curses_form_create_widget_row(cf, lw, ds, 1, top, id);
	dfui_dataset_free(ds);

	/*
	 * Repaint the form.
	 */
	curses_form_widget_ensure_visible(cf->widget_focus);
	cf->want_y = cf->widget_focus->y;
	curses_form_draw(cf);
	curses_form_refresh(cf);
	return(0);
}

/*
 * Callback to dfui_form_fields_foreach which determines both
 * the width of the widest label (cbd->max_label_width) and
 * the total width of all labels (cbd->total_label_width).
 */
int
cb_field_measure_name(struct dfui_field *fi, void *userdata)
{
	struct callback_data *cbd = userdata;
	int len;
	
	len = MIN(60, strlen(dfui_info_get_name(dfui_field_get_info(fi))));
	if (len > cbd->max_label_width)
		cbd->max_label_width = len;
	cbd->total_label_width += (len + 2);
	return(1);
}

/*
 * Callback to dfui_form_fields_foreach which adds one label and
 * one textbox (or other control) to a curses_form for each field
 * in the dfui_form.  Each set of labels and controls is added
 * one row below the previous set.
 */
int
cb_field_create_input_single(struct dfui_field *fi, void *userdata)
{
	struct callback_data *cbd = userdata;
	struct curses_widget *label, *box;
	struct dfui_celldata *cd;
	char *value;

	label = curses_form_widget_add(cbd->cf, cbd->left_acc,
	    cbd->cf->height, cbd->max_label_width, CURSES_LABEL,
	    dfui_info_get_name(dfui_field_get_info(fi)), -1);

	cd = dfui_dataset_celldata_find(dfui_form_dataset_get_first(cbd->f),
	    dfui_field_get_id(fi));

	value = dfui_celldata_get_value(cd);

	if (dfui_field_hint_is(fi, "control", "checkbox")) {
		box = curses_form_widget_add(cbd->cf,
		    cbd->max_label_width + 3,
		    cbd->cf->height, 4, CURSES_CHECKBOX, "", -1);
		box->amount = (value[0] == 'Y' ? 1 : 0);
	} else {
		box = curses_form_widget_add(cbd->cf,
		    cbd->max_label_width + 3,
		    cbd->cf->height, 20, CURSES_TEXTBOX, value, 256);
	}
	curses_widget_tooltip_set(box,
	    dfui_info_get_short_desc(dfui_field_get_info(fi)));
	box->user_id = 1;
	box->userdata = fi;

	if (dfui_field_hint_is(fi, "editable", "false"))
		box->editable = 0;
	if (dfui_field_hint_is(fi, "obscured", "true"))
		box->obscured = 1;

	cbd->cf->height++;
	return(1);
}

/*
 * Create a row of widgets in a multiple=true form.
 * Returns the x position of the "Ins" button, if any.
 */
int
curses_form_create_widget_row(struct curses_form *cf, struct curses_widget *cw,
	struct dfui_dataset *ds, int left, int top, int row)
{
	struct curses_widget *box, *button;
	struct dfui_field *fi;
	struct dfui_celldata *cd;
	char *value;
	int col = 0, ins_x = left;
	struct curses_form_userdata *cfu = cf->userdata;
	struct dfui_form *f = cfu->f;

	/*
	 * Create one input underneath each field heading.
	 */
	for (fi = dfui_form_field_get_first(f); fi != NULL;
	     fi = dfui_field_get_next(fi)) {
		cd = dfui_dataset_celldata_find(ds, dfui_field_get_id(fi));
		value = dfui_celldata_get_value(cd);
		if (cw == NULL) {
			if (dfui_field_hint_is(fi, "control", "checkbox")) {
				box = curses_form_widget_add(cf,
				    left, top, 4, CURSES_CHECKBOX, "", -1);
				box->amount = (value[0] == 'Y' ? 1 : 0);
			} else {
				box = curses_form_widget_add(cf, left, top,
				    cfu->widths[col] - 1, CURSES_TEXTBOX, value, 256);
			}
		} else {
			if (dfui_field_hint_is(fi, "control", "checkbox")) {
				box = curses_form_widget_insert_after(cw,
				    left, top, 4, CURSES_CHECKBOX, "", -1);
				box->amount = (value[0] == 'Y' ? 1 : 0);
			} else {
				box = curses_form_widget_insert_after(cw, left, top,
				    cfu->widths[col] - 1, CURSES_TEXTBOX, value, 256);
			}
			cw = box;
		}
		curses_widget_tooltip_set(box,
		    dfui_info_get_short_desc(dfui_field_get_info(fi)));
		box->user_id = row;
		box->userdata = fi;

		if (dfui_field_hint_is(fi, "editable", "false"))
			box->editable = 0;
		if (dfui_field_hint_is(fi, "obscured", "true"))
			box->obscured = 1;

		left += cfu->widths[col++];
	}

	/*
	 * If this is an extensible form,
	 * create buttons for each dataset.
	 */
	if (dfui_form_is_extensible(f)) {
		if (cw == NULL) {
			button = curses_form_widget_add(cf, left,
			    top, -1, CURSES_BUTTON, "Ins", -1);
		} else {
			button = curses_form_widget_insert_after(cw, left,
			    top, -1, CURSES_BUTTON, "Ins", -1);
			cw = button;
		}
		ins_x = left;
		button->user_id = row;
		curses_button_set_click_cb(button, cb_click_insert_row);

		left += button->width + 1;

		if (cw == NULL) {
			button = curses_form_widget_add(cf, left,
			    top, -1, CURSES_BUTTON, "Del", -1);
		} else {
			button = curses_form_widget_insert_after(cw, left,
			    top, -1, CURSES_BUTTON, "Del", -1);
			cw = button;
		}
		button->user_id = row;
		curses_button_set_click_cb(button, cb_click_remove_row);
	}

	return(ins_x);
}

/*
 * Create the widgets for a multiple=true form.  For each field
 * in the form, a label containing the field's name, which serves
 * as a heading, is created.  Underneath these labels, for each
 * dataset in the form, a row of input widgets (typically textboxes)
 * is added.  Non-action, manipulation buttons are also added to
 * the right of each row.
 */
int
curses_form_create_widgets_multiple(struct callback_data *cbd)
{
	struct curses_widget *label, *button;
	struct dfui_field *fi;
	struct dfui_dataset *ds;
	char *name;
	int left = cbd->left_acc;
	int top = cbd->top_acc;
	int row = 1, col = 0, ins_x = left;
	struct curses_form_userdata *cfu = cbd->cf->userdata;

	for (fi = dfui_form_field_get_first(cbd->f); fi != NULL;
	     fi = dfui_field_get_next(fi)) {
		/*
		 * Create a label to serve as a heading for the column.
		 */
		name = dfui_info_get_name(dfui_field_get_info(fi));	
		label = curses_form_widget_add(cbd->cf, left,
		    top, -1, CURSES_LABEL, name, -1);
		cfu->widths[col++] = label->width + 2;
		left += (label->width + 2);
	}

	/*
	 * Create a row of widgets for each dataset.
	 */
	top++;
	for (ds = dfui_form_dataset_get_first(cbd->f); ds != NULL;
	     ds = dfui_dataset_get_next(ds)) {
		ins_x = curses_form_create_widget_row(cbd->cf, NULL, ds,
		     cbd->left_acc, top++, row++);
	}

	/*
	 * Finally, create an 'Add' button to add a new row
	 * if this is an extensible form.
	 */
	if (dfui_form_is_extensible(cbd->f)) {
		button = curses_form_widget_add(cbd->cf,
		    ins_x, top, -1,
		    CURSES_BUTTON, "Add", -1);
		button->user_id = row;
		curses_button_set_click_cb(button, cb_click_insert_row);
		cbd->cf->height++;
	}

	return(1);
}

/*
 * Callback to dfui_form_actions_foreach which determines both
 * the width of the widest button (cbd->max_button_width) and
 * the total width of all buttons (cbd->total_button_width).
 */
int
cb_action_measure_name(struct dfui_action *a, void *userdata)
{
	struct callback_data *cbd = userdata;
	int len;

	len = strlen(dfui_info_get_name(dfui_action_get_info(a)));
	if (len > cbd->max_button_width)
		cbd->max_button_width = len;
	cbd->total_button_width += (len + 6);
	return(1);
}

void
center_buttons(struct callback_data *cbd)
{
	struct curses_widget *w;
	int row_width, row_offset;

	/*
	 * Center the previous row of buttons on the form
	 * if this is not a menu.
	 */
	if (!cbd->is_menu) {
		/* Find the width of all buttons on the previous row. */
		row_width = 0;
		for (w = cbd->row_start; w != NULL; w = w->next) {
			row_width += w->width + 2;
		}
			
		/*
		 * Adjust the x position of each of button by
		 * a calculated offset.
		 */
		row_offset = (cbd->cf->width - row_width) / 2;
		for (w = cbd->row_start; w != NULL; w = w->next) {
			w->x += row_offset;
		}

		/*
		 * Mark the next button we will create
		 * as the first button of a row.
		 */
		cbd->row_start = NULL;
	}
}

/*
 * Callback to dfui_form_actions_foreach which creates a row of
 * buttons, one for each action, at the bottom of a curses_form.
 */
int
cb_action_create_button(struct dfui_action *a, void *userdata)
{
	struct callback_data *cbd = userdata;
	struct curses_widget *w;
	char name[80];

	strlcpy(name, dfui_info_get_name(dfui_action_get_info(a)), 70);

	dfui_debug("creating button `%s' (%d) @ %d / %d\n",
		name, strlen(name), cbd->left_acc, cbd->cf->width);

	if (!cbd->is_menu && cbd->button_rows == 0)
		cbd->button_rows = 1;

	/*
	 * Check for overflow.  If the next button would appear
	 * off the right side of the form, start putting buttons
	 * on the next row.  Or, if this is a menu, always put the
	 * next button on the next line.
	 */
	if (cbd->is_menu ||
	    ((cbd->left_acc + strlen(name) + 6) > cbd->cf->width &&
	    cbd->left_acc > 1)) {
		center_buttons(cbd);
		cbd->cf->height++;
		cbd->left_acc = 1;
		cbd->button_rows++;
	}

	w = curses_form_widget_add(cbd->cf, cbd->left_acc,
	    cbd->cf->height, -1, CURSES_BUTTON, name, -1);
	curses_widget_tooltip_set(w,
	    dfui_info_get_short_desc(dfui_action_get_info(a)));
	cbd->left_acc += (w->width + 2);
	w->user_id = -1;
	w->userdata = a;
	curses_button_set_click_cb(w, cb_click_close_form);
	if (cbd->row_start == NULL)
		cbd->row_start = w;

	return(1);
}

void
set_help(struct dfui_form *f, struct curses_form *cf)
{
	char *help_text;

	cf->has_help = 1;
	help_text = dfui_info_get_long_desc(dfui_form_get_info(f));
	if (strlen(help_text) > 0) {
		cf->help_text = dfui_strdup(help_text);
	} else {
		cf->help_text = dfui_strdup("No help available.");
	}
}

/*** FORM TRANSLATORS ***/

struct curses_form *
curses_form_construct_from_dfui_form_single(struct dfui_form *f)
{
	struct callback_data cbd;
	int desc_width;
	struct curses_form_userdata *cfu;
	int min_width = 0;
	char *min_width_str;

	dfui_debug("-----\nconstructing single form: %s\n",
	    dfui_info_get_name(dfui_form_get_info(f)));

	cbd.is_menu = dfui_form_hint_is(f, "role", "menu");
	cbd.cf = curses_form_new(dfui_info_get_name(dfui_form_get_info(f)));
	DFUI_MALLOC(cfu, curses_form_userdata);
	cfu->f = f;
	cbd.cf->userdata = cfu;
	cbd.cf->cleanup = 1;

	set_help(f, cbd.cf);

	/* Calculate offsets for nice positioning of labels and buttons. */

	cbd.max_label_width = 0;
	cbd.total_label_width = 0;
	cbd.max_button_width = 0;
	cbd.total_button_width = 0;
	cbd.left_acc = 1;
	cbd.f = f;

	dfui_form_fields_foreach(cb_field_measure_name, f, &cbd);
	dfui_form_actions_foreach(cb_action_measure_name, f, &cbd);

	if (cbd.total_label_width > (xmax - 2))
		cbd.total_label_width = (xmax - 2);	/* XXX scroll/wrap? */

	/* Take the short description and turn it into a set of labels. */

	if ((min_width_str = dfui_form_hint_find(f, "minimum_width")) != NULL)
		min_width = atoi(min_width_str);

	desc_width = 40;
	desc_width = MAX(desc_width, min_width);
	if (cbd.is_menu) {
		desc_width = MAX(desc_width, cbd.max_button_width);
	} else {
		desc_width = MAX(desc_width, cbd.total_button_width);
	}
	desc_width = MAX(desc_width, cbd.max_label_width);  /* XXX + max_field_width */
	desc_width = MIN(desc_width, xmax - 4); /* -2 for borders, -2 for spaces */

	dfui_debug("min width: %d\n", min_width);
	dfui_debug("button width: %d\n", cbd.total_button_width);
	dfui_debug("label width: %d\n", cbd.total_label_width);
	dfui_debug("resulting width: %d\n", desc_width);
	dfui_debug("form width: %d\n", cbd.cf->width);

	cbd.cf->height = curses_form_descriptive_labels_add(cbd.cf,
	    dfui_info_get_short_desc(dfui_form_get_info(f)),
	    1, cbd.cf->height + 1, desc_width);

	dfui_debug("form width now: %d\n", cbd.cf->width);

	if (!cbd.is_menu)
		cbd.cf->height++;

	/* Add the fields and buttons. */

	dfui_form_fields_foreach(cb_field_create_input_single, f, &cbd);

	if (dfui_form_field_get_first(f) != NULL)
		cbd.cf->height++;

	cbd.left_acc = 1;
	cbd.row_start = NULL;
	cbd.button_rows = 0;
	dfui_form_actions_foreach(cb_action_create_button, f, &cbd);
	center_buttons(&cbd);

	cbd.cf->height++;
	
	curses_form_finalize(cbd.cf);

	return(cbd.cf);
}

struct curses_form *
curses_form_construct_from_dfui_form_multiple(struct dfui_form *f)
{
	struct callback_data cbd;
	int desc_width;
	struct curses_form_userdata *cfu;
	int min_width = 0;
	char *min_width_str;

	dfui_debug("-----\nconstructing multiple form: %s\n",
	    dfui_info_get_name(dfui_form_get_info(f)));

	cbd.is_menu = 0;
	cbd.cf = curses_form_new(dfui_info_get_name(dfui_form_get_info(f)));
	DFUI_MALLOC(cfu, curses_form_userdata);
	cfu->f = f;
	cbd.cf->userdata = cfu;
	cbd.cf->cleanup = 1;

	set_help(f, cbd.cf);

	/* Calculate offsets for nice positioning of labels and buttons. */
	
	cbd.max_label_width = 0;
	cbd.total_label_width = 0;
	cbd.max_button_width = 0;
	cbd.total_button_width = 0;
	cbd.left_acc = 1;
	cbd.f = f;

	dfui_form_fields_foreach(cb_field_measure_name, f, &cbd);
	dfui_form_actions_foreach(cb_action_measure_name, f, &cbd);

	/* Take the short description and turn it into a set of labels. */

	if ((min_width_str = dfui_form_hint_find(f, "minimum_width")) != NULL)
		min_width = atoi(min_width_str);

	desc_width = 40;
	desc_width = MAX(desc_width, min_width);
	desc_width = MAX(desc_width, cbd.total_button_width);
	desc_width = MAX(desc_width, cbd.total_label_width);
	desc_width = MIN(desc_width, xmax - 3);

	dfui_debug("min width: %d\n", min_width);
	dfui_debug("button width: %d\n", cbd.total_button_width);
	dfui_debug("label width: %d\n", cbd.total_label_width);
	dfui_debug("resulting width: %d\n", desc_width);
	dfui_debug("form width: %d\n", cbd.cf->width);

	cbd.cf->height = curses_form_descriptive_labels_add(cbd.cf,
	    dfui_info_get_short_desc(dfui_form_get_info(f)),
	    1, cbd.cf->height + 1, desc_width);

	dfui_debug("form width now: %d\n", cbd.cf->width);

	/* Add the fields. */

	cbd.top_acc = cbd.cf->height + 1;
	cbd.cf->height += dfui_form_dataset_count(f) + 2;

	curses_form_create_widgets_multiple(&cbd);

	cbd.cf->height++;

	/* Add the buttons. */

	cbd.left_acc = 1;
	cbd.row_start = NULL;
	cbd.button_rows = 0;
	dfui_form_actions_foreach(cb_action_create_button, f, &cbd);
	center_buttons(&cbd);

	cbd.cf->height++;

	curses_form_finalize(cbd.cf);

	return(cbd.cf);
}

struct curses_form *
curses_form_construct_from_dfui_form(struct dfui_form *f)
{
	if (dfui_form_is_multiple(f))
		return(curses_form_construct_from_dfui_form_multiple(f));
	else
		return(curses_form_construct_from_dfui_form_single(f));
}

#define FIFTY_EIGHT_SPACES "                                                          "

void
strcpy_max(char *dest, char *src, int max)
{
	strncpy(dest, src, max);
	if (strlen(src) > max) {
		strcpy(dest + (max - 3), "...");
	} else {
		strncpy(dest + strlen(src),
		    FIFTY_EIGHT_SPACES, max - strlen(src));
	}
}

struct curses_form *
curses_form_construct_from_dfui_progress(struct dfui_progress *pr,
					 struct curses_widget **pbar,
					 struct curses_widget **plab,
					 struct curses_widget **pcan)
{
	struct curses_form *cf;
	char *desc;

	desc = dfui_info_get_short_desc(dfui_progress_get_info(pr));

	cf = curses_form_new(dfui_info_get_name(dfui_progress_get_info(pr)));

	cf->width = 60;
	cf->height = 6;

	*plab = curses_form_widget_add(cf, -1, 1, 58,
	    CURSES_LABEL, FIFTY_EIGHT_SPACES, -1);
	strcpy_max((*plab)->text, desc, 58);
	*pbar = curses_form_widget_add(cf, -1, 3, 40,
	    CURSES_PROGRESS, "", -1);
	*pcan = curses_form_widget_add(cf, -1, 5, -1,
	    CURSES_BUTTON, "Cancel", -1);
	(*pbar)->amount = dfui_progress_get_amount(pr);

	curses_form_finalize(cf);

	gettimeofday(&last_update, NULL);

	return(cf);
}

void
curses_widgets_update_from_dfui_progress(struct dfui_progress *pr,
					 struct curses_widget *pbar,
					 struct curses_widget *plab,
					 struct curses_widget *pcan)
{
	char *desc;
	struct timeval now;
	long msec_diff;

	gettimeofday(&now, NULL);
	msec_diff = (now.tv_sec - last_update.tv_sec) * 1000 +
		    (now.tv_usec - last_update.tv_usec) / 1000;
	if (msec_diff < 250)
		return;

	desc = dfui_info_get_short_desc(dfui_progress_get_info(pr));

	if (dfui_progress_get_amount(pr) != pbar->amount ||
	    strncmp(plab->text, desc, MIN(55, strlen(desc))) != 0) {
		strcpy_max(plab->text, desc, 58);
		curses_widget_draw(plab);
		pbar->amount = dfui_progress_get_amount(pr);
		curses_widget_draw(pbar);
	} else {
		curses_progress_spin(pbar);
	}
	wmove(pcan->form->win, pcan->y + 1, pcan->x + pcan->width + 1);
	curses_form_refresh(NULL);
	last_update = now;
}

char *
curses_widget_xlat_value(struct curses_widget *cw)
{
	if (cw->type == CURSES_TEXTBOX)
		return(cw->text);
	else if (cw->type == CURSES_CHECKBOX)
		return(cw->amount ? "Y" : "N");
	else
		return("");
}

struct dfui_response *
response_construct_from_curses_form_single(struct dfui_form *f,
					   struct curses_form *cf,
					   struct curses_widget *cw)
{
	struct dfui_response *r = NULL;
	struct dfui_action *selected = NULL;
	struct dfui_dataset *ds = NULL;
	dfui_id_t id;
	char *value;

	selected = cw->userdata;
	r = dfui_response_new(dfui_id_dup(dfui_form_get_id(f)),
			      dfui_id_dup(dfui_action_get_id(selected)));
	ds = dfui_dataset_new();
	for (cw = cf->widget_head; cw != NULL; cw = cw->next) {
		if (cw->user_id > 0) {
			id = dfui_field_get_id((struct dfui_field *)cw->userdata);
			value = curses_widget_xlat_value(cw);
			dfui_dataset_celldata_add(ds, dfui_id_dup(id), value);
		}
	}
	dfui_response_dataset_add(r, ds);

	return(r);
}

struct dfui_response *
response_construct_from_curses_form_multiple(struct dfui_form *f,
					     struct curses_form *cf,
					     struct curses_widget *cw)
{
	struct dfui_response *r = NULL;
	struct dfui_action *selected = NULL;
	struct dfui_dataset *ds = NULL;
	dfui_id_t id;
	char *value;
	int row = 0;
	int rows = 100; /* XXX obviously we'd prefer something more efficient here! */
	int cds_added = 0;

	selected = cw->userdata;
	r = dfui_response_new(dfui_id_dup(dfui_form_get_id(f)),
			      dfui_id_dup(dfui_action_get_id(selected)));

	/* Create one dataset per row. */
	for (row = 1; row < rows; row++) {
		ds = dfui_dataset_new();
		cds_added = 0;
		for (cw = cf->widget_head; cw != NULL; cw = cw->next) {
			if (cw->user_id == row &&
			    (cw->type == CURSES_TEXTBOX || cw->type == CURSES_CHECKBOX)) {
				id = dfui_field_get_id((struct dfui_field *)cw->userdata);
				value = curses_widget_xlat_value(cw);
				dfui_dataset_celldata_add(ds, dfui_id_dup(id), value);
				cds_added += 1;
			}
		}
		if (cds_added > 0) {
			dfui_response_dataset_add(r, ds);
		} else {
			dfui_dataset_free(ds);
		}
	}

	return(r);
}

struct dfui_response *
response_construct_from_curses_form(struct dfui_form *f,
				    struct curses_form *cf,
				    struct curses_widget *cw)
{
	if (dfui_form_is_multiple(f))
		return(response_construct_from_curses_form_multiple(f, cf, cw));
	else
		return(response_construct_from_curses_form_single(f, cf, cw));
}
