/*
 * Copyright (c) 2005-2023 Julien Nadeau Carriere <vedge@csoft.net>
 * All rights reserved.
 *
 * 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 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 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.
 */

/*
 * Table widget. Displays a set of cells organized in one or more columns.
 * Cells can contain text, numerical values, dynamically-generated content
 * or embedded Agar widgets. It implements a polled mode with asset-recycling
 * for tables which must be cleared and reconstructed frequently. Individual
 * cells, entire rows, or columns can be selected.
 */

#include <agar/core/core.h>
#ifdef AG_WIDGETS

#include <agar/gui/table.h>
#include <agar/gui/primitive.h>
#include <agar/gui/cursors.h>
#include <agar/gui/keyboard.h>

#include <string.h>
#include <stdarg.h>

static int     SortCellsAscending(const void *_Nonnull, const void *_Nonnull);
static int     SortCellsDescending(const void *_Nonnull, const void *_Nonnull);
static AG_Size PrintCellGeneral(const AG_TableCell *_Nonnull, char *_Nonnull, AG_Size);
static void    DrawCell(AG_Table *_Nonnull, AG_TableCell *_Nonnull, const AG_Rect *);
static void    UpdateEmbeddedWidgets(AG_Table *_Nonnull);
static int     CompareCellsGeneral(const AG_TableCell *, const AG_TableCell *);
static int     CompareCellsFnTxt(const AG_TableCell *_Nonnull, const AG_TableCell *_Nonnull);
static void    RestoreRowSelections(AG_Table *_Nonnull);
static void    RestoreCellSelections(AG_Table *_Nonnull);
static void    RestoreColSelections(AG_Table *_Nonnull);

#undef  COLUMN_RESIZE_RANGE
#define COLUMN_RESIZE_RANGE 10  /* TODO css */

AG_Table *
AG_TableNew(void *parent, Uint flags)
{
	AG_Table *t;

	t = Malloc(sizeof(AG_Table));
	AG_ObjectInit(t, &agTableClass);

	if (flags & AG_TABLE_HFILL) { WIDGET(t)->flags |= AG_WIDGET_HFILL; }
	if (flags & AG_TABLE_VFILL) { WIDGET(t)->flags |= AG_WIDGET_VFILL; }
	t->flags |= flags;

	AG_ObjectAttach(parent, t);
	return (t);
}

/* Timer callback for polling updates. */
static Uint32
PollTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();
	AG_Event *fnPoll = t->fn[AG_TABLE_FN_POLL];

	fnPoll->fn(fnPoll);

	if (t->mOffs+t->mVis >= t->m) {
		t->mOffs = MAX(0, t->m - t->mVis);
	}
	AG_Redraw(t);
	return (to->ival);
}

AG_Table *
AG_TableNewPolled(void *parent, Uint flags, void (*fn)(AG_Event *),
    const char *fmt, ...)
{
	AG_Table *t;
	AG_Event *ev;
	va_list ap;

	t = AG_TableNew(parent, flags);

	AG_ObjectLock(t);

	t->flags |= AG_TABLE_POLL;
	ev = t->fn[AG_TABLE_FN_POLL] = AG_SetEvent(t, "table-poll", fn, NULL);
	if (fmt) {
		va_start(ap, fmt);
		AG_EventGetArgs(ev, fmt, ap);
		va_end(ap);
	}

	AG_AddTimer(t, &t->pollTo, 250, PollTimeout, NULL);

	AG_ObjectUnlock(t);
	return (t);
}

/*
 * Return the cell at unchecked location m,n.
 * The table must be locked.
 */
AG_TableCell *
AG_TableGetCell(AG_Table *t, int m, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
#ifdef AG_DEBUG
	if (m < 0 || m >= t->m ||
	    n < 0 || n >= t->n)
		AG_FatalError("Illegal cell access");
#endif
	return (&t->cells[m][n]);
}

/*
 * Cell selection control
 * The table must be locked.
 */
int
AG_TableCellSelected(AG_Table *t, int m, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	return (t->cells[m][n].selected);
}
void
AG_TableSelectCell(AG_Table *t, int m, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->cells[m][n].selected = 1;
}
void
AG_TableDeselectCell(AG_Table *t, int m, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->cells[m][n].selected = 0;
}

/*
 * Column selection control
 * The table must be locked.
 */
int
AG_TableColSelected(AG_Table *t, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	return (t->cols[n].flags & AG_TABLE_COL_SELECTED);
}
void
AG_TableSelectCol(AG_Table *t, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->cols[n].flags |= AG_TABLE_COL_SELECTED;
}
void
AG_TableDeselectCol(AG_Table *t, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->cols[n].flags &= ~(AG_TABLE_COL_SELECTED);
}

void
AG_TableSetPollInterval(AG_Table *t, Uint ival)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if (ival == 0) {
		AG_DelTimer(t, &t->pollTo);
	} else {
		AG_AddTimer(t, &t->pollTo, ival, PollTimeout, NULL);
	}

	AG_ObjectUnlock(t);
}

/* Set an initial size requisition in width (pixels) x n (columns). */
void
AG_TableSizeHint(AG_Table *t, int w, int n)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	if (w != -1) { t->wHint = w; }
	if (n != -1) { t->hHint = n*agTextFontHeight; }
}

/* Set the selection mode (by row, by column or by cell). */
void
AG_TableSetSelectionMode(AG_Table *t, enum ag_table_selmode mode)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->selMode = mode;
}

/* Set the column click action (SELECT, SORT). */
void
AG_TableSetColumnAction(AG_Table *t, Uint flags)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->colAction = flags;
}

/* Set the field delimiter character(s) (defaults to ":") */
void
AG_TableSetSeparator(AG_Table *t, const char *sep)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	t->sep = sep;

	AG_ObjectUnlock(t);
}

/* Set column height in pixels */
void
AG_TableSetColHeight(AG_Table *t, int h)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->hCol = h;

	AG_WidgetUpdate(t);
	AG_Redraw(t);
}

/* Set row height in pixels */
void
AG_TableSetRowHeight(AG_Table *t, int h)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->hRow = h;

	AG_WidgetUpdate(t);
	AG_Redraw(t);
}

/* Specify the minimum allowed column width in pixels. */
void
AG_TableSetColMin(AG_Table *t, int w)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	t->wColMin = w;

	for (n = 0; n < t->n; n++) {
		AG_TableCol *tc = &t->cols[n];

		if (tc->w < t->wColMin)
			tc->w = t->wColMin;
	}

	AG_ObjectUnlock(t);
}

/*
 * Specify a default column width in pixels to use in size requisition,
 * for "%" and FILL columns.
 */
void
AG_TableSetDefaultColWidth(AG_Table *t, int w)
{
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	t->wColDefault = w;
}

/* Register a new popup menu. */
AG_MenuItem *
AG_TableSetPopup(AG_Table *t, int m, int n)
{
	AG_TablePopup *tp;
	AG_MenuItem *rv;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	SLIST_FOREACH(tp, &t->popups, popups) {
		if (tp->m == m && tp->n == n) {
			rv = tp->item;
			goto out;
		}
	}
	tp = Malloc(sizeof(AG_TablePopup));
	tp->m = m;
	tp->n = n;
	tp->panel = NULL;
	tp->menu = AG_MenuNew(NULL, 0);
	tp->item = tp->menu->root;			/* XXX redundant */
	SLIST_INSERT_HEAD(&t->popups, tp, popups);
	rv = tp->item;
out:
	AG_ObjectUnlock(t);
	return (rv);
}

/* Set a callback function. */
void
AG_TableSetFn(AG_Table *t, enum ag_table_fn which, AG_EventFn fn,
    const char *fmt, ...)
{
	if (which >= AG_TABLE_FN_LAST) {
		return;
	}
	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	t->fn[which] = AG_SetEvent(t, NULL, fn, NULL);
	if (fmt) {
		va_list ap;

		va_start(ap, fmt);
		AG_EventGetArgs(t->fn[which], fmt, ap);
		va_end(ap);
	}

	AG_ObjectUnlock(t);
}

/* Compute the effective widths of the columns. */
static void
SizeColumns(AG_Table *_Nonnull t)
{
	AG_TableCol *tc, *tcFill = NULL;
	AG_Rect r;
	int n, x;

	t->wTot = 0;
	for (n = 0; n < t->n; n++) {
		tc = &t->cols[n];

		if (tc->wPct != -1) {
			tc->w = tc->wPct*t->r.w/100;
			t->wTot += tc->w;
			tc->wPct = -1;
			continue;
		}
		if (tc->flags & AG_TABLE_COL_FILL) {
			tcFill = tc;
			tcFill->flags &= ~(AG_TABLE_COL_FILL);
		} else {
			t->wTot += tc->w;
		}
	}
	if (tcFill != NULL) {
		tcFill->w = t->r.w;
		for (n = 0; n < t->n; n++) {
			tc = &t->cols[n];
			if (tc != tcFill)
				tcFill->w -= tc->w;
		}
		if (tcFill->w < t->wColMin) {
			tcFill->w = t->wColMin;
		}
		t->wTot += tcFill->w;
	}

	/* Update the cursor change areas for column resize. */
	r.y = WIDGET(t)->paddingTop;
	r.h = t->hCol;
	for (n = 0, x = WIDGET(t)->paddingLeft; n < t->n; n++) {
		tc = &t->cols[n];
		
		r.x = x - COLUMN_RESIZE_RANGE/2 - t->xOffs + tc->w;
		r.w = COLUMN_RESIZE_RANGE;
		AG_SetStockCursor(t, &tc->ca, &r, AG_HRESIZE_CURSOR);
		x += tc->w;
	}
}

static void
SizeRequest(void *_Nonnull obj, AG_SizeReq *_Nonnull r)
{
	AG_Table *t = obj;
	AG_SizeReq rBar;
	int n;

	r->h = WIDGET(t)->paddingTop + t->hHint + WIDGET(t)->paddingBottom;
	r->w = WIDGET(t)->paddingLeft + WIDGET(t)->paddingRight;

	if (t->wHint != -1) {
		r->w += t->wHint;
	} else {
		for (n = 0; n < t->n; n++) {
			AG_TableCol *tc = &t->cols[n];
		
			if (tc->flags & AG_TABLE_COL_FILL || tc->wPct != -1) {
				r->w += t->wColDefault;
			} else {
				r->w += tc->w;
			}
		}
	}
	
	if (t->vbar != NULL) {
		AG_WidgetSizeReq(t->vbar, &rBar);
		r->w += rBar.w;
	}
	if (t->hbar != NULL) {
		AG_WidgetSizeReq(t->hbar, &rBar);
		r->h += rBar.h;
	}
}

static int
SizeAllocate(void *_Nonnull obj, const AG_SizeAlloc *_Nonnull a)
{
	AG_Table *t = obj;
	AG_SizeReq rBar;
	AG_SizeAlloc aBar;
	int vBarSz=0, hBarSz=0;

	t->r.x = WIDGET(t)->paddingLeft;
	t->r.y = WIDGET(t)->paddingTop + t->hCol;
	t->r.w = a->w - t->r.x - WIDGET(t)->paddingRight;
	t->r.h = a->h - t->r.y - WIDGET(t)->paddingBottom;

	if (t->r.h <= 0)
		return (-1);
	
	if (t->vbar) {
		AG_WidgetSizeReq(t->vbar, &rBar);
		if (rBar.w > (a->w >> 1)) { rBar.w = (a->w >> 1); }
		aBar.x = a->w - rBar.w;
		aBar.y = 0;
		aBar.w = rBar.w;
		aBar.h = a->h;
		AG_WidgetSizeAlloc(t->vbar, &aBar);
		vBarSz = WIDTH(t->vbar);
	}
	if (t->hbar) {
		if (vBarSz) {
			aBar.h -= rBar.w;
			AG_WidgetSizeAlloc(t->vbar, &aBar);
		}
		AG_WidgetSizeReq(t->hbar, &rBar);
		if (rBar.h > (a->h >> 1)) { rBar.h = (a->h >> 1); }
		aBar.x = 0;
		aBar.y = a->h - rBar.h;
		aBar.w = a->w - vBarSz;
		aBar.h = rBar.h;
		AG_WidgetSizeAlloc(t->hbar, &aBar);
		hBarSz = HEIGHT(t->hbar);
	}

	t->r.w -= vBarSz;
	t->r.h -= hBarSz;

	SizeColumns(t);

	if (t->xOffs + t->r.w >= t->wTot)                /* Horiz scrollbar */
		t->xOffs = MAX(0, t->wTot - t->r.w);

	t->mVis = t->r.h/t->hRow;                         /* Vert scrollbar */
	if (t->mOffs+t->mVis >= t->m)
		t->mOffs = MAX(0, t->m - t->mVis);

	return (0);
}

AG_Size
AG_TablePrintCell(const AG_TableCell *c, char *buf, AG_Size bufsz)
{
	switch (c->type) {
	case AG_CELL_STRING:
		return Strlcpy(buf, c->data.s, bufsz);
	case AG_CELL_PSTRING:
		return Strlcpy(buf, (char *)c->data.p, bufsz);
	case AG_CELL_INT:
		if (strcmp(c->fmt, "%d") == 0) {
			return StrlcpyInt(buf, c->data.i, bufsz);
		} else {
			return Snprintf(buf, bufsz, c->fmt, c->data.i);
		}
	case AG_CELL_PINT:
		return Snprintf(buf, bufsz, c->fmt, *(int *)c->data.p);
	case AG_CELL_UINT:
		if (strcmp(c->fmt, "%u") == 0) {
			return StrlcpyUint(buf, (Uint)c->data.i, bufsz);
		} else {
			return Snprintf(buf, bufsz, c->fmt, (Uint)c->data.i);
		}
	case AG_CELL_PUINT:
		return Snprintf(buf, bufsz, c->fmt, *(Uint *)c->data.p);
	case AG_CELL_FLOAT:
		return Snprintf(buf, bufsz, c->fmt, (float)c->data.f);
	case AG_CELL_PFLOAT:
		return Snprintf(buf, bufsz, c->fmt, *(float *)c->data.p);
	case AG_CELL_DOUBLE:
		return Snprintf(buf, bufsz, c->fmt, c->data.f);
	case AG_CELL_PDOUBLE:
		return Snprintf(buf, bufsz, c->fmt, *(double *)c->data.p);
	default:
		return PrintCellGeneral(c, buf, bufsz);
	}
}

static AG_Size
PrintCellGeneral(const AG_TableCell *_Nonnull c, char *_Nonnull buf, AG_Size bufsz)
{
	switch (c->type) {
	case AG_CELL_FN_TXT:
		return c->fnTxt(c->data.p, buf, bufsz);
	case AG_CELL_POINTER:
		return Snprintf(buf, bufsz, c->fmt, c->data.p);
	case AG_CELL_LONG:
	case AG_CELL_ULONG:
		return Snprintf(buf, bufsz, c->fmt, c->data.l);
	case AG_CELL_PLONG:
		return Snprintf(buf, bufsz, c->fmt, *(long *)c->data.p);
	case AG_CELL_PULONG:
		return Snprintf(buf, bufsz, c->fmt, *(Ulong *)c->data.p);
	case AG_CELL_PUINT8:
		return Snprintf(buf, bufsz, c->fmt, *(Uint8 *)c->data.p);
	case AG_CELL_PSINT8:
		return Snprintf(buf, bufsz, c->fmt, *(Sint8 *)c->data.p);
	case AG_CELL_PUINT16:
		return Snprintf(buf, bufsz, c->fmt, *(Uint16 *)c->data.p);
	case AG_CELL_PSINT16:
		return Snprintf(buf, bufsz, c->fmt, *(Sint16 *)c->data.p);
	case AG_CELL_PUINT32:
		return Snprintf(buf, bufsz, c->fmt, *(Uint32 *)c->data.p);
	case AG_CELL_PSINT32:
		return Snprintf(buf, bufsz, c->fmt, *(Sint32 *)c->data.p);
#ifdef HAVE_64BIT
	case AG_CELL_SINT64:
	case AG_CELL_UINT64:
		return Snprintf(buf, bufsz, c->fmt, c->data.u64);
	case AG_CELL_PUINT64:
		return Snprintf(buf, bufsz, c->fmt, *(Uint64 *)c->data.p);
	case AG_CELL_PINT64:
		return Snprintf(buf, bufsz, c->fmt, *(Sint64 *)c->data.p);
#endif
	default:
		if (bufsz > 0)
			buf[0] = '\0';
	}
	return (0);
}

static void
ScrollToSelection(AG_Table *_Nonnull t)
{
	const int mVis = t->mVis;
	int m;

	if (t->n < 1) {
		return;
	}
	for (m = 0; m < t->m; m++) {
		if (!t->cells[m][0].selected) {
			continue;
		}
		t->mOffs = (t->mOffs > m) ? m : MAX(0, m-mVis+2);
		AG_Redraw(t);
		return;
	}
}

static int _Pure_Attribute
SelectionIsVisible(const AG_Table *_Nonnull t)
{
	const int mOffsVis = t->mOffs + t->mVis;
	const int hRow = t->hRow;
	const int hCol = t->hCol;
	int n, m;
	int x, y;
	
	for (n = 0, x = -t->xOffs;
	     n < t->n && x < t->r.w;
	     n++) {
		const AG_TableCol *col = &t->cols[n];
	
		if (col->w <= 0) {
			continue;
		}
		for (m = t->mOffs, y = hCol;
		     m < MIN(t->m, mOffsVis) && (y < hCol + t->r.h);
		     m++) {
			if (t->cells[m][n].selected && m < (mOffsVis - 1)) {
				return (1);
			}
			y += hRow;
		}
		x += col->w;
	}
	return (0);
}

static void
Draw(void *_Nonnull obj)
{
	AG_Table *t = obj;
	const AG_Color *cFg  = &WCOLOR(t, FG_COLOR);
	const AG_Color *cSel = &WCOLOR(t, SELECTION_COLOR);
	AG_Color cLine       =  WCOLOR(t, LINE_COLOR);
	AG_Rect rCol, rCell;
	const int paddingTop  = WIDGET(t)->paddingTop;
	const int paddingLeft = WIDGET(t)->paddingLeft;
	int n,m, hCol;

	AG_WidgetDraw(t->vbar);
	AG_WidgetDraw(t->hbar);
	
	if (t->flags & AG_TABLE_WIDGETS)
		UpdateEmbeddedWidgets(t);
	
	if (!(t->flags & AG_TABLE_NOAUTOSORT) &&
	    t->flags & AG_TABLE_NEEDSORT)
		AG_TableSort(t);

	rCell.h = t->hRow + 1;

	hCol = t->hCol;
	rCol.y = 0;
	rCol.h = hCol + t->r.h - 2;

	AG_PushClipRect(t, &WIDGET(t)->r);
	AG_PushBlendingMode(t, AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC);

	for (n = 0, rCell.x = paddingLeft - t->xOffs;
	     n < t->n && rCell.x < t->r.w;
	     n++) {
		AG_TableCol *col = &t->cols[n];
		const int w = col->w;
		AG_Rect r;
	
		if (w <= 0) {
			continue;
		}
		rCell.w = w;
		rCol.w = ((rCell.x + w) < t->r.w) ? w : (t->r.w - rCell.x);
		rCol.x = rCell.x;

		/* Column header and separator */
		if (rCol.x > 0 && rCol.x < t->r.w) {
			AG_DrawLineV(t, rCol.x, hCol-1, rCol.h,
			    &WCOLOR(t, LINE_COLOR));           /* (not cLine) */
		}
		AG_PushClipRect(t, &rCol);

		r.x = rCol.x;
		r.y = paddingTop;
		r.w = rCol.w+1;
		r.h = hCol;

		if (col->flags & AG_TABLE_COL_SELECTED) {
			AG_DrawBoxSunk(t, &r, cSel);
		} else {
			AG_DrawBoxRaised(t, &r, cFg);
		}

		/* Column header label */
		if (col->name[0] != '\0') {
			if (col->surface == -1) {
				col->surface = AG_WidgetMapSurface(t,
				    AG_TextRender(col->name));
			}
			AG_WidgetBlitSurface(t, col->surface,
			    rCell.x + (w >> 1) - (WSURFACE(t,col->surface)->w >> 1),
			    paddingTop + (hCol >> 1) - (WSURFACE(t,col->surface)->h >> 1));

			if (col->flags & AG_TABLE_COL_ASCENDING) {
				AG_DrawArrowUp(t,
				    rCell.x + w - COLUMN_RESIZE_RANGE,
				    paddingTop + (hCol >> 1), 10,
				    &cLine);
			} else if (col->flags & AG_TABLE_COL_DESCENDING) {
				AG_DrawArrowDown(t,
				    rCell.x + w - COLUMN_RESIZE_RANGE,
				    paddingTop + (hCol >> 1), 10,
				    &cLine);
			}
		}

		/* Rows of this column */
		for (m = t->mOffs, rCell.y = paddingTop + hCol;
		     m < t->m && (rCell.y < rCol.h);
		     m++) {
			AG_TableCell *c = &t->cells[m][n];

			if (c->selected) {
				AG_DrawRect(t, &rCell, cSel);
			}
			AG_DrawLineH(t, 0, t->r.w - 1, rCell.y,
			    c->selected ? cSel : &cLine);
			DrawCell(t, c, &rCell);
			rCell.y += t->hRow;
		}

		/* Indicate column selection. */
		if ((t->flags & AG_TABLE_HIGHLIGHT_COLS) &&
		    (col->flags & AG_TABLE_COL_SELECTED)) {
			AG_Color c;

			AG_ColorRGBA_8(&c, 0,0,250, 32);
			AG_DrawRectBlended(t, &rCol, &c,
			    AG_ALPHA_SRC, AG_ALPHA_ONE_MINUS_SRC);
		}

		AG_PopClipRect(t);
		rCell.x += w;
	}

	AG_PopBlendingMode(t);
	AG_PopClipRect(t);

	if (rCell.x > 0 &&
	    rCell.x < t->r.w) {
		AG_DrawLineV(t, rCell.x, hCol-1, rCol.h, &cLine);
	}
	t->flags &= ~(AG_TABLE_REDRAW_CELLS);
}

static void
DrawCell(AG_Table *_Nonnull t, AG_TableCell *_Nonnull c, const AG_Rect *rd)
{
	char buf[AG_TABLE_BUF_MAX], *pTxt = buf;
	AG_Size cSize;
	const int cellPaddingLeft = 2;		/* TODO css */

	if (c->surface != -1) {
		if (t->flags & AG_TABLE_REDRAW_CELLS) {
			AG_WidgetUnmapSurface(t, c->surface);
			c->surface = -1;
		} else {
			goto blit;
		}
	}

	switch (c->type) {
	case AG_CELL_STRING:					/* Avoid copy */
		c->surface = AG_WidgetMapSurface(t, AG_TextRender(c->data.s));
		goto blit;
	case AG_CELL_PSTRING:					/* Avoid copy */
		c->surface = AG_WidgetMapSurface(t, AG_TextRender((char *)
		                                                  c->data.p));
		goto blit;
	case AG_CELL_FN_SU:
		c->surface = AG_WidgetMapSurface(t,
		    c->fnSu(c->data.p, rd->x, rd->y));
		goto blit;
	case AG_CELL_FN_SU_NODUP:
		c->surface = AG_WidgetMapSurfaceNODUP(t,
		    c->fnSu(c->data.p, rd->x, rd->y));
		goto blit;
	case AG_CELL_WIDGET:
		if (WIDGET_OPS(c->data.p)->draw != NULL) {
			AG_WidgetDraw(c->data.p);
		}
		c->surface = -1;
		return;
	case AG_CELL_NULL:
		if (c->fmt[0] != '\0') {
			c->surface = AG_WidgetMapSurface(t, AG_TextRender(c->fmt));
			goto blit;
		} else {
			return;
		}
		break;
	default:
		if ((cSize = AG_TablePrintCell(c, buf, sizeof(buf)))
		    >= sizeof(buf)) {
			if ((pTxt = TryMalloc(cSize)) == NULL) {
				return;
			}
			AG_TablePrintCell(c, pTxt, cSize);
		}
		break;
	}

	c->surface = AG_WidgetMapSurface(t, AG_TextRender(pTxt));

	if (cSize >= sizeof(buf))
		free(pTxt);
blit:
	AG_WidgetBlitSurface(t, c->surface,
	    rd->x + cellPaddingLeft,
	    rd->y + (t->hRow >> 1) - (WSURFACE(t,c->surface)->h >> 1));
}

/* Set the effective position of all embedded widgets in the table. */
static void
UpdateEmbeddedWidgets(AG_Table *_Nonnull t)
{
	AG_SizeAlloc wa;
	AG_Widget *wt;
	AG_Rect rd;
	AG_TableCol *col;
	AG_TableCell *c;
	const int hRow = t->hRow;
	int m, n;
	int updateCoords = 0;

	rd.h = hRow;

	for (n=0, rd.x=-t->xOffs; n < t->n; n++) {
		col = &t->cols[n];
		rd.w = col->w;
		rd.y = t->hCol - t->mOffs*hRow;
		for (m = 0; m < t->m; m++) {
			c = &t->cells[m][n];
			if (c->type != AG_CELL_WIDGET) {
				continue;
			}
			wt = c->data.p;

			if (wt->x != rd.x || wt->y != rd.y ||
			    wt->w != rd.w || wt->h != rd.h) {
				wa.x = rd.x;
				wa.y = rd.y;
				wa.w = rd.w;
				wa.h = rd.h;
				AG_WidgetSizeAlloc(wt, &wa);
				updateCoords++;
			}

			/*
			 * Adjust sensitivity rectangle if widget is
			 * partially visible.
			 */
			wt->rSens.w = (rd.x+rd.w > t->r.w) ?
			    (t->r.w - rd.x - 4) : rd.w;
			wt->rSens.x2 = wt->rSens.x1 + wt->rSens.w;
	
			wt->rSens.h = (rd.y+rd.h > t->r.h+t->hCol) ?
			    (t->r.h + t->hCol - rd.y - 4) : rd.h;
			wt->rSens.y2 = wt->rSens.y1 + wt->rSens.h;

			rd.y += hRow;
		}
		rd.x += col->w;
	}

	/* Apply any changes to widget size allocations. */
	if (updateCoords) {
		AG_WidgetUpdateCoords(t,
		    WIDGET(t)->rView.x1,
		    WIDGET(t)->rView.y1);
	}
}

/*
 * Free any resources allocated by a table cell.
 * Table must be locked.
 */
void
AG_TableFreeCell(AG_Table *t, AG_TableCell *c)
{
#if 0
	if (c->widget != NULL) {
		AG_ObjectDetach(c->widget);
		AG_ObjectDestroy(c->widget);
	}
#endif
	if (c->surface != -1) {
		AG_WidgetUnmapSurface(t, c->surface);
		c->surface = -1;
	}
}

/* Sort the items in the table. */
void
AG_TableSort(AG_Table *t)
{
	int (*sortFn)(const void *, const void *) = NULL;
	int i;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (i = 0; i < t->n; i++) {
		const AG_TableCol *tc = &t->cols[i];

		if (tc->flags & AG_TABLE_COL_ASCENDING) {
			sortFn = SortCellsAscending;
			break;
		} else if (tc->flags & AG_TABLE_COL_DESCENDING) {
			sortFn = SortCellsDescending;
			break;
		}
	}
	if (i == t->n) {
		goto out;
	}
	t->nSorting = i;
	qsort(t->cells, t->m, sizeof(AG_TableCell *), sortFn);
out:
	t->flags &= ~(AG_TABLE_NEEDSORT);
	AG_ObjectUnlock(t);
}

static int
SortCellsDescending(const void *_Nonnull p1, const void *_Nonnull p2)
{
	const AG_TableCell *row1 = *(const AG_TableCell **)p1;
	const AG_TableCell *row2 = *(const AG_TableCell **)p2;
	const AG_Table *t = row1[0].tbl;
	const AG_TableCol *tc = &t->cols[t->nSorting];

	if (tc->sortFn) {
		return tc->sortFn(&row1[t->nSorting], &row2[t->nSorting]);
	}
	return AG_TableCompareCells(&row1[t->nSorting], &row2[t->nSorting]);
}

static int
SortCellsAscending(const void *_Nonnull p1, const void *_Nonnull p2)
{
	const AG_TableCell *row1 = *(const AG_TableCell **)p1;
	const AG_TableCell *row2 = *(const AG_TableCell **)p2;
	const AG_Table *t = row1[0].tbl;
	const AG_TableCol *tc = &t->cols[t->nSorting];

	if (tc->sortFn) {
		return tc->sortFn(&row2[t->nSorting], &row1[t->nSorting]);
	}
	return AG_TableCompareCells(&row2[t->nSorting], &row1[t->nSorting]);
}

static __inline__ Uint
HashPrevCell(AG_Table *_Nonnull t, const AG_TableCell *_Nonnull c)
{
	char buf[AG_TABLE_HASHBUF_MAX];
	Uchar *p;
	Uint h;

	AG_TablePrintCell(c, buf, sizeof(buf));
	for (h = 0, p = (Uchar *)buf; *p != '\0'; p++) {
		h = 31*h + *p;
	}
	return (h % t->nPrevBuckets);
}

void
AG_TableClear(AG_Table *t)
{
	AG_TableBegin(t);
}

/*
 * Clear the items on the table and save the selection state. The function
 * returns with the table locked.
 */
void
AG_TableBegin(AG_Table *t)
{
	int m, n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);		/* Lock across TableBegin/End */

	/* Copy the existing cells to the backing store and free the table. */
	for (m = 0; m < t->m; m++) {
		for (n = 0; n < t->n; n++) {
			AG_TableCell *c = &t->cells[m][n], *cPrev;
			AG_TableBucket *tbPrev = &t->cPrev[HashPrevCell(t,c)];

			cPrev = Malloc(sizeof(AG_TableCell));
			memcpy(cPrev, c, sizeof(AG_TableCell));
			cPrev->nPrev = n;
			TAILQ_INSERT_HEAD(&tbPrev->cells, cPrev, cells);
			TAILQ_INSERT_HEAD(&t->cPrevList, cPrev, cells_list);
		}
		free(t->cells[m]);
	}
	free(t->cells);
	t->cells = NULL;
	t->m = 0;
	t->flags &= ~(AG_TABLE_WIDGETS);
}

/*
 * Restore the selection state and recover the surfaces of matching
 * items in the backing store. Unlock the table.
 */
void
AG_TableEnd(AG_Table *t)
{
	static void (*pf[])(AG_Table *_Nonnull) = {
		RestoreRowSelections,		/* SEL_ROWS */
		RestoreCellSelections,		/* SEL_CELLS */
		RestoreColSelections,		/* SEL_COLS */
	};
	AG_TableCell *tc, *tcNext;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");

	if (t->n == 0)
		goto out;

	/*
	 * Recover surfaces and selection state from the backing store.
	 */
#ifdef AG_DEBUG
	if (t->selMode >= AG_TABLE_SEL_LAST)
		AG_FatalError("selMode");
#endif
	pf[t->selMode](t);

	/* Clear the backing store. */
	for (tc = TAILQ_FIRST(&t->cPrevList);
	     tc != TAILQ_END(&t->cPrevList);
	     tc = tcNext) {
		tcNext = TAILQ_NEXT(tc, cells_list);
		AG_TableFreeCell(t, tc);
		free(tc);
	}
	TAILQ_INIT(&t->cPrevList);

	/* It is safe to use memset() in place of TAILQ_INIT(). */
	memset(t->cPrev, 0, t->nPrevBuckets*sizeof(AG_TableBucket));
out:
	AG_ObjectUnlock(t);		/* Lock across TableBegin/End */
}


/* Restore selection state on a per-row basis. */
static void
RestoreRowSelections(AG_Table *_Nonnull t)
{
	int m, n;
	int nMatched, nCompared;

	for (m = 0; m < t->m; m++) {
		nMatched = 0;
		nCompared = 0;
		for (n = 0; n < t->n; n++) {
			AG_TableCell *c = &t->cells[m][n];
			AG_TableCell *cPrev;
			AG_TableBucket *tb;

			if (c->type == AG_CELL_NULL) {
				continue;
			}
			tb = &t->cPrev[HashPrevCell(t,c)];
			TAILQ_FOREACH(cPrev, &tb->cells, cells) {
				if (cPrev->nPrev != n ||
				    AG_TableCompareCells(c, cPrev) != 0) {
					continue;
				}
				c->surface = cPrev->surface;
				cPrev->surface = -1;
				if (cPrev->selected)
					nMatched++;
			}
			nCompared++;
		}
		if (nMatched == nCompared)
			AG_TableSelectRow(t, m);
	}
}

/* Compare two table cells. */
int
AG_TableCompareCells(const AG_TableCell *c1, const AG_TableCell *c2)
{
	if (c1->type != c2->type || strcmp(c1->fmt, c2->fmt) != 0) {
		if (c1->type == AG_CELL_NULL || c2->type == AG_CELL_NULL) {
			return (c1->type == AG_CELL_NULL ? 1 : -1);
		}
		return (1);
	}
	if (c1->id != 0 && c2->id != 0) {
		return (c1->id - c2->id);
	}
	switch (c1->type) {
	case AG_CELL_STRING:
		return (strcoll(c1->data.s, c2->data.s));
	case AG_CELL_INT:
		return (c1->data.i - c2->data.i);
	case AG_CELL_UINT:
		return ((Uint)c1->data.i - (Uint)c2->data.i);
	/* TODO */
	case AG_CELL_FLOAT:
		return (int)((c1->data.f - c2->data.f)*1000000.0f);
	case AG_CELL_DOUBLE:
		return (int)((c1->data.f - c2->data.f)*1000000.0);
	case AG_CELL_WIDGET:
		/* TODO hooks */
		return (1);
	default:
		return CompareCellsGeneral(c1, c2);
	}
	return (1);
}

static int
CompareCellsGeneral(const AG_TableCell *c1, const AG_TableCell *c2)
{
	switch (c1->type) {
	case AG_CELL_PSTRING:
		return (strcoll((char *)c1->data.p, (char *)c2->data.p));
	case AG_CELL_PINT:
	case AG_CELL_PUINT:
		return (*(int *)c1->data.p - *(int *)c2->data.p);
	case AG_CELL_FN_SU:
	case AG_CELL_FN_SU_NODUP:
		return ((c1->data.p != c2->data.p) || (c1->fnSu != c2->fnSu));
	case AG_CELL_FN_TXT:
		return CompareCellsFnTxt(c1, c2);
	case AG_CELL_POINTER:
		return (c1->data.p != c2->data.p);
	case AG_CELL_LONG:
		return (c1->data.l - c2->data.l);
	case AG_CELL_ULONG:
		return ((Ulong)c1->data.l - (Ulong)c2->data.l);
	case AG_CELL_PULONG:
	case AG_CELL_PLONG:
		return (*(long *)c1->data.p - *(long *)c2->data.p);
	case AG_CELL_PUINT8:
	case AG_CELL_PSINT8:
		return (*(Uint8 *)c1->data.p - *(Uint8 *)c2->data.p);
	case AG_CELL_PUINT16:
	case AG_CELL_PSINT16:
		return (*(Uint16 *)c1->data.p - *(Uint16 *)c2->data.p);
	case AG_CELL_PUINT32:
	case AG_CELL_PSINT32:
		return (*(Uint32 *)c1->data.p - *(Uint32 *)c2->data.p);
#ifdef HAVE_64BIT
	case AG_CELL_SINT64:
	case AG_CELL_UINT64:
		return (c1->data.u64 - c2->data.u64);
	case AG_CELL_PUINT64:
	case AG_CELL_PINT64:
		return (*(Uint64 *)c1->data.p - *(Uint64 *)c2->data.p);
#endif
	case AG_CELL_NULL:
	default:
		return (0);
	}
}

/* Compare two "%[Ft]" cells. */
static int
CompareCellsFnTxt(const AG_TableCell *_Nonnull c1, const AG_TableCell *_Nonnull c2)
{
	char b1[AG_TABLE_BUF_MAX];
	char b2[AG_TABLE_BUF_MAX];

	/* XXX TODO handle oversize cells */
	c1->fnTxt(c1->data.p, b1, sizeof(b1));
	c2->fnTxt(c2->data.p, b2, sizeof(b2));
	return (strcoll(b1, b2));
}

/*
 * Restore selection state on a per-cell basis. In most applications, the
 * user will almost always need to specify per-cell unique IDs for selections
 * to restore properly.
 */
static void
RestoreCellSelections(AG_Table *_Nonnull t)
{
	int m, n;

	for (n = 0; n < t->n; n++) {
		for (m = 0; m < t->m; m++) {
			AG_TableCell *c = &t->cells[m][n];
			AG_TableCell *cPrev;
			AG_TableBucket *tb;
			
			if (c->type == AG_CELL_NULL) {
				continue;
			}
			tb = &t->cPrev[HashPrevCell(t,c)];
			TAILQ_FOREACH(cPrev, &tb->cells, cells) {
				if (AG_TableCompareCells(c, cPrev) != 0) {
					continue;
				}
				c->selected = cPrev->selected;
				c->surface = cPrev->surface;
				cPrev->surface = -1;
				break;
			}
		}
	}
}

/*
 * Restore selection state on a per-column basis.
 */
static void
RestoreColSelections(AG_Table *_Nonnull t)
{
	int m, n;

	for (n = 0; n < t->n; n++) {
		for (m = 0; m < t->m; m++) {
			AG_TableCell *c = &t->cells[m][n];
			AG_TableCell *cPrev;
			AG_TableBucket *tb;
			
			if (c->type == AG_CELL_NULL) {
				continue;
			}
			tb = &t->cPrev[HashPrevCell(t,c)];

			TAILQ_FOREACH(cPrev, &tb->cells, cells) {
				if (AG_TableCompareCells(c, cPrev) != 0) {
					continue;
				}
				c->surface = cPrev->surface;
				cPrev->surface = -1;
			}
		}
	}
}

/* Return true if multiple item selection is enabled. */
static __inline__ int
SelectingMultiple(AG_Table *_Nonnull t)
{
	return ((t->flags & AG_TABLE_MULTITOGGLE) ||
	        ((t->flags & AG_TABLE_MULTI) &&
		  AG_GetModState(t) & AG_KEYMOD_CTRL));
}

/* Return true if a range of items are being selected. */
static __inline__ int
SelectingRange(AG_Table *_Nonnull t)
{
	return ((t->flags & AG_TABLE_MULTI) &&
	        (AG_GetModState(t) & AG_KEYMOD_SHIFT));
}

/* Display the popup menu. */
static void
ShowPopup(AG_Table *_Nonnull t, AG_TablePopup *_Nonnull tp, int x, int y)
{
	if (tp->panel != NULL) {
		AG_MenuCollapse(tp->item);
		tp->panel = NULL;
	}
	tp->menu->itemSel = tp->item;
	tp->panel = AG_MenuExpand(t, tp->item, x+4, y+4);
}

/* Right click on a column header; display the column's popup menu. */
static void
ColumnRightClick(AG_Table *_Nonnull t, int px, int py)
{
	const int x = px - (COLUMN_RESIZE_RANGE/2);
	int n, cx;

	for (n = 0, cx = -t->xOffs;
	     n < t->n;
	     n++) {
		const int w = t->cols[n].w;
		const int x2 = cx+w;

		if (x > cx && x < x2) {
			AG_TablePopup *tp;

			SLIST_FOREACH(tp, &t->popups, popups) {
				if (tp->m == -1 && tp->n == n) {
					ShowPopup(t, tp, px,py);
					return;
				}
			}
		}
		cx += w;
	}
}

/* Left click on a column header; column action is sort. */
static void
ColumnLeftClickSort(AG_Table *_Nonnull t, AG_TableCol *_Nonnull tc)
{
	int n;

	for (n = 0; n < t->n; n++) {
		AG_TableCol *tcOther = &t->cols[n];

		if (tcOther != tc) {
			tcOther->flags &= ~(AG_TABLE_COL_ASCENDING);
			tcOther->flags &= ~(AG_TABLE_COL_DESCENDING);
		}
	}
	if (tc->flags & AG_TABLE_COL_ASCENDING) {
		Debug(t, "Sorting column \"%s\" descending\n", tc->name);
		tc->flags &= ~(AG_TABLE_COL_ASCENDING);
		tc->flags |= AG_TABLE_COL_DESCENDING;
	} else {
		Debug(t, "Sorting column \"%s\" ascending\n", tc->name);
		tc->flags &= ~(AG_TABLE_COL_DESCENDING);
		tc->flags |= AG_TABLE_COL_ASCENDING;
	}
	t->flags |= AG_TABLE_NEEDSORT;
}

/* Timer callback for double click. */
static Uint32
DoubleClickTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
	int *which = AG_PTR(1);

	*which = -1;
	return (0);
}

/* Left click on a column header; resize or execute column action. */
static void
ColumnLeftClick(AG_Table *_Nonnull t, int px)
{
	AG_Event *ev;
	const int x = px - (COLUMN_RESIZE_RANGE/2);
	int multi = SelectingMultiple(t);
	int x1, n;

	for (n = 0, x1 = -t->xOffs;
	     n < t->n;
	     n++) {
		AG_TableCol *tc = &t->cols[n];
		const int x2 = x1 + tc->w;

		if (x > x1 && x < x2) {
			if ((x2 - x) <= COLUMN_RESIZE_RANGE) {
				if (t->nResizing == -1) {
					Debug(t, "Resizing col#%d (\"%s\")\n",
					    n, tc->name);
					t->nResizing = n;
				}
			} else {
				if (t->colAction & AG_TABLE_COL_SELECT) {
					Debug(t, "Selecting col#%d (\"%s\")\n",
					    n, tc->name);
					if (multi) {
						AG_INVFLAGS(tc->flags,
						    AG_TABLE_COL_SELECTED);
					} else {
						tc->flags |= AG_TABLE_COL_SELECTED;
					}
				}
				if (t->colAction & AG_TABLE_COL_SORT) {
					ColumnLeftClickSort(t, tc);
				}
				if ((ev = t->fn[AG_TABLE_FN_COL_CLICK]))
					AG_PostEventByPtr(t, ev, "%i", n);

				if (t->dblClickedCol != -1 &&
				    t->dblClickedCol == n) {
					AG_DelTimer(t, &t->dblClickTo);
					if ((ev = t->fn[AG_TABLE_FN_COL_DBLCLICK])) {
						AG_PostEventByPtr(t, ev, "%i", n);
					}
					t->dblClickedCol = -1;
				} else {
					t->dblClickedCol = n;
					AG_AddTimer(t, &t->dblClickTo,
					    agMouseDblclickDelay,
					    DoubleClickTimeout, "%p", &t->dblClickedCol);
				}
				goto cont;
			}
			AG_Redraw(t);
		}
		if (!multi) {
			tc->flags &= ~(AG_TABLE_COL_SELECTED);
			AG_Redraw(t);
		}
cont:
		x1 += tc->w;
	}
}

/* Process left click on a cell. */
static void
CellLeftClick(AG_Table *_Nonnull t, int mc, int x)
{
	AG_TableCell *c;
	AG_TableCol *col;
	AG_Event *ev;
	int m, n, i, j, nc;
	int xCol = 0;
	
	if (t->n < 1)
		return;

	for (nc = 0; nc < t->n; nc++) {
		col = &t->cols[nc];

		if (((t->xOffs + x) >= xCol &&                    /* Inside */
		     (t->xOffs + x) <  xCol + col->w) ||
		    (col->flags & AG_TABLE_COL_FILL)) {
			break;
		}
		xCol += col->w;
	}
	if (nc == t->n)
		nc = (t->n - 1);

	switch (t->selMode) {
	case AG_TABLE_SEL_ROWS:
		if (SelectingRange(t)) {
			for (m = 0; m < t->m; m++) {
				if (AG_TableRowSelected(t,m))
					break;
			}
			if (m == t->m) {
				break;
			}
			if (m < mc) {
				Debug(t, "Selecting rows #%d-#%d\n", m, mc);
				for (i = m; i <= mc; i++)
					AG_TableSelectRow(t, i);
			} else if (m > mc) {
				Debug(t, "Selecting rows #%d-#%d\n", mc, m);
				for (i = mc; i <= m; i++)
					AG_TableSelectRow(t, i);
			} else {
				Debug(t, "Selecting row #%d\n", mc);
				AG_TableSelectRow(t, mc);
			}
		} else if (SelectingMultiple(t)) {
			for (n = 0; n < t->n; n++) {
				c = &t->cells[mc][n];
				Debug(t, "Cell[%d][%d]: Selection %d -> %d\n", mc, n,
				    c->selected, !c->selected);
				c->selected = !c->selected;
			}
		} else {
			Debug(t, "Selecting row #%d\n", mc);
			AG_TableDeselectAllRows(t);
			AG_TableSelectRow(t, mc);
			if ((ev = t->fn[AG_TABLE_FN_ROW_CLICK]) != NULL)
				AG_PostEventByPtr(t, ev, "%i", mc);

			if (t->dblClickedRow != -1 &&
			    t->dblClickedRow == mc) {
				AG_DelTimer(t, &t->dblClickTo);
				if ((ev = t->fn[AG_TABLE_FN_ROW_DBLCLICK])) {
					AG_PostEventByPtr(t, ev, "%i", mc);
				}
				t->dblClickedRow = -1;
			} else {
				t->dblClickedRow = mc;
				AG_AddTimer(t, &t->dblClickTo,
				    agMouseDblclickDelay,
				    DoubleClickTimeout, "%p", &t->dblClickedRow);
			}
		}
		break;
	case AG_TABLE_SEL_CELLS:
		if (SelectingRange(t)) {
			for (m = 0, n=0; m < t->m; m++) {
				for (n = 0; n < t->n; n++) {
					if (AG_TableCellSelected(t,m,n))
						break;
				}
				if (n == t->n)
					break;
			}
			if (m == t->m) {
				break;
			}
			if (m < mc && n < nc) {
				Debug(t, "Selecting cells [%d,%d][%d-%d]\n",
				    n, nc, m, mc);
				for (i = n; i <= nc; i++)
					for (j = m; j <= mc; j++)
						AG_TableSelectCell(t, j,i);
			} else if (m > mc && n > nc) {
				Debug(t, "Selecting cells [%d,%d][%d,%d]\n",
				    nc, n, mc, m);
				for (i = nc; i <= n; i++)
					for (j = mc; j <= m; j++)
						AG_TableSelectCell(t, j,i);
			} else {
				Debug(t, "Selecting cell %d,%d\n", mc, nc);
				AG_TableSelectCell(t, mc, nc);
			}
		} else if (SelectingMultiple(t)) {
			c = &t->cells[mc][nc];
			Debug(t, "Cell[%d][%d]: Selection %d -> %d\n", mc, nc,
			    c->selected, !c->selected);
			c->selected = !c->selected;
		} else {
			Debug(t, "Selecting cell %d,%d\n", mc, nc);
			for (m = 0; m < t->m; m++) {
				for (n = 0; n < t->n; n++) {
					c = &t->cells[m][n];
					c->selected = ((int)m == mc) &&
					              ((int)n == nc);
				}
			}
			if ((ev = t->fn[AG_TABLE_FN_CELL_CLICK]))
				AG_PostEventByPtr(t, ev, "%i", mc);

			if (t->dblClickedCell != -1 &&
			    t->dblClickedCell == mc) {
				AG_DelTimer(t, &t->dblClickTo);
				if ((ev = t->fn[AG_TABLE_FN_CELL_DBLCLICK])) {
					AG_PostEventByPtr(t, ev, "%i", mc);
				}
				t->dblClickedCell = -1;
			} else {
				t->dblClickedCell = mc;
				AG_AddTimer(t, &t->dblClickTo,
				    agMouseDblclickDelay,
				    DoubleClickTimeout, "%p", &t->dblClickedCell);
			}
		}
		break;
	case AG_TABLE_SEL_COLS:
		if (SelectingRange(t)) {
			for (n = 0; n < t->n; n++) {
				if (AG_TableColSelected(t,n))
					break;
			}
			if (n == t->n) {
				break;
			}
			if (n < nc) {
				for (i = n; i <= nc; i++)
					AG_TableSelectCol(t, i);
			} else if (n > nc) {
				for (i = nc; i <= n; i++)
					AG_TableSelectCol(t, i);
			} else {
				AG_TableSelectCol(t, nc);
			}
		} else if (SelectingMultiple(t)) {
			for (n = 0; n < t->n; n++) {
				col = &t->cols[n];
				AG_INVFLAGS(col->flags, AG_TABLE_COL_SELECTED);
			}
		} else {
			for (n = 0; n < t->n; n++) {
				col = &t->cols[n];
				if (n == nc) {
					col->flags |= AG_TABLE_COL_SELECTED;
				} else {
					col->flags &= ~(AG_TABLE_COL_SELECTED);
				}
			}
			if ((ev = t->fn[AG_TABLE_FN_COL_CLICK]))
				AG_PostEventByPtr(t, ev, "%i", nc);

			if (t->dblClickedCol != -1 &&
			    t->dblClickedCol == nc) {
				AG_DelTimer(t, &t->dblClickTo);
				if ((ev = t->fn[AG_TABLE_FN_COL_DBLCLICK])) {
					AG_PostEventByPtr(t, ev, "%i", nc);
				}
				t->dblClickedCol = -1;
			} else {
				t->dblClickedCol = nc;
				AG_AddTimer(t, &t->dblClickTo,
				    agMouseDblclickDelay,
				    DoubleClickTimeout, "%p", &t->dblClickedCol);
			}
		}
		break;
	default:
		break;
	}
	AG_Redraw(t);
}

/* Right click on cell; show the cell's popup menu. */
static void
CellRightClick(AG_Table *_Nonnull t, int m, int px, int py)
{
	int x = px - (COLUMN_RESIZE_RANGE/2), cx;
	int n;

	for (n = 0, cx = -t->xOffs;
	     n < t->n;
	     n++) {
		AG_TableCol *tc = &t->cols[n];
		AG_TablePopup *tp;

		if (x < cx ||
		    x > cx+tc->w) {
			continue;
		}
		SLIST_FOREACH(tp, &t->popups, popups) {
			if ((tp->m == m || tp->m == -1) &&
			    (tp->n == n || tp->n == -1)) {
				ShowPopup(t, tp, px,py);
				return;
			}
		}
		cx += tc->w;
	}
}

/* Return the row at the given y-coordinate. */
static __inline__ int
RowAtY(AG_Table *_Nonnull t, int y)
{
	int m = t->mOffs;
	
	if (y > t->hCol) {
		m += (y - t->hCol)/t->hRow;
	}
	if (m < 0) { m = 0; }
	if (m >= (int)t->m) { m = (t->m > 0) ? ((int)t->m - 1) : 0; }
	return (m);
}

static void
DecrementSelection(AG_Table *_Nonnull t, int inc)
{
	int m;

	if (t->m < 1) {
		return;
	}
	for (m = 0; m < t->m; m++) {
		if (AG_TableRowSelected(t,m)) {
			m -= inc;
			if (m < 0) { m = 0; }
			break;
		}
	}
	if (m < t->m) {
		AG_TableDeselectAllRows(t);
		AG_TableSelectRow(t, m);
	} else {
		AG_TableSelectRow(t, 0);
	}
	if (!SelectionIsVisible(t))
		ScrollToSelection(t);
}

static void
IncrementSelection(AG_Table *_Nonnull t, int inc)
{
	int m;

	if (t->m < 1) {
		return;
	}
	for (m = t->m-1; m >= 0; m--) {
		if (AG_TableRowSelected(t,m)) {
			m += inc;
			if (m >= t->m) { m = t->m-1; }
			break;
		}
	}
	if (m >= 0) {
		AG_TableDeselectAllRows(t);
		AG_TableSelectRow(t, m);
	} else {
		AG_TableSelectRow(t, 0);
	}
	if (!SelectionIsVisible(t))
		ScrollToSelection(t);
}

static void
MouseButtonDown(void *obj, AG_MouseButton button, int mx, int my)
{
	AG_Table *t = obj;
	const int x = mx - WIDGET(t)->paddingLeft;
	const int y = my - WIDGET(t)->paddingTop;
	int m;
	
	if (!AG_WidgetIsFocused(t))
		AG_WidgetFocus(t);
	
	switch (button) {
	case AG_MOUSE_WHEELUP:
		t->mOffs -= t->lineScrollAmount;
		if (t->mOffs < 0) {
			t->mOffs = 0;
		}
		AG_Redraw(t);
		break;
	case AG_MOUSE_WHEELDOWN:
		t->mOffs += t->lineScrollAmount;
		if (t->mOffs > (t->m - t->mVis)) {
			t->mOffs = MAX(0, t->m - t->mVis);
		}
		AG_Redraw(t);
		break;
	case AG_MOUSE_LEFT:
		if (y <= t->hCol) {
			ColumnLeftClick(t, x);
			break;
		}
		if (t->m == 0 || t->n == 0) {
			return;
		}
		m = RowAtY(t, y);
		Debug(t, "Cell click row #%d at y=%d\n", m, y);
		CellLeftClick(t, m, x);
		break;
	case AG_MOUSE_RIGHT:
		if (y <= t->hCol) {
			ColumnRightClick(t, x,y);
			break;
		}
		if (t->m == 0 || t->n == 0) {
			return;
		}
		m = RowAtY(t, y);
		CellRightClick(t, m, x,y);
		break;
	default:
		break;
	}
}

static void
MouseButtonUp(void *obj, AG_MouseButton button, int mx, int my)
{
	AG_Table *t = obj;

	switch (button) {
	case AG_MOUSE_LEFT:
		if (t->nResizing >= 0) {
			t->nResizing = -1;
			AG_Redraw(t);
		}
		break;
	default:
		break;
	}
}

/* Timer callback for keyboard selection moving. */
static Uint32
MoveTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();
	const int incr = AG_INT(1);

	if (incr < 0) {
		DecrementSelection(t, -incr);
	} else {
		IncrementSelection(t, incr);
	}
	return (agKbdRepeat);
}

static void
KeyDown(void *obj, AG_KeySym ks, AG_KeyMod kmod, AG_Char ch)
{
	AG_Table *t = obj;
	const int ms = agKbdDelay;

	switch (ks) {
	case AG_KEY_UP:
		DecrementSelection(t, 1);
		AG_AddTimer(t, &t->moveTo, ms, MoveTimeout,"%i",-1);
		break;
	case AG_KEY_DOWN:
		IncrementSelection(t, 1);
		AG_AddTimer(t, &t->moveTo, ms, MoveTimeout,"%i",+1);
		break;
	case AG_KEY_PAGEUP:
		DecrementSelection(t, agPageIncrement);
		AG_AddTimer(t, &t->moveTo, ms, MoveTimeout,"%i",-agPageIncrement);
		break;
	case AG_KEY_PAGEDOWN:
		IncrementSelection(t, agPageIncrement);
		AG_AddTimer(t, &t->moveTo, ms, MoveTimeout,"%i",+agPageIncrement);
		break;
	case AG_KEY_HOME:
		t->mOffs = 0;
		AG_Redraw(t);
		break;
	case AG_KEY_END:
		t->mOffs = MAX(0, t->m - t->mVis);
		AG_Redraw(t);
		break;
	default:
		break;
	}
}

static void
MouseMotion(void *obj, int mx, int my, int dx, int dy)
{
	AG_Table *t = obj;
	const int x = mx - WIDGET(t)->paddingLeft;

	if (x < 0 || x >= WIDTH(t))
		return;

	if (t->nResizing >= 0 && t->nResizing < t->n) {
		AG_TableCol *tc = &t->cols[t->nResizing];

		if ((tc->w += dx) < t->wColMin) {
			tc->w = t->wColMin;
		}
		SizeColumns(t);
		AG_Redraw(t);
	}
}

static void
KeyUp(void *obj, AG_KeySym ks, AG_KeyMod kmod, AG_Char ch)
{
	AG_Table *t = obj;

	switch (ks) {
	case AG_KEY_UP:
	case AG_KEY_PAGEUP:
	case AG_KEY_DOWN:
	case AG_KEY_PAGEDOWN:
		AG_DelTimer(t, &t->moveTo);
		break;
	default:
		break;
	}
}

static void
OnFontChange(AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();
	const int fontHeight = WFONT(t)->height;
	Uint m, n;
	
	t->hRow = fontHeight + 4;		/* TODO css cell-padding */
	t->hCol = fontHeight + 4;		/* TODO css head-padding */
	
	for (n = 0; n < t->n; n++) {
		AG_TableCol *tc = &t->cols[n];

		if (tc->surface != -1) {
			AG_WidgetUnmapSurface(t, tc->surface);
			tc->surface = -1;
		}
		for (m = 0; m < t->m; m++) {
			AG_TableCell *c = &t->cells[m][n];

			if (c->surface != -1) {
				AG_WidgetUnmapSurface(t, c->surface);
				c->surface = -1;
			}
		}
	}
}

static void
LostFocus(AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();

	AG_DelTimer(t, &t->moveTo);
	AG_DelTimer(t, &t->dblClickTo);

	if (t->nResizing >= 0)
		t->nResizing = -1;
}

static void
Hidden(AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();
	int i;

	AG_DelTimer(t, &t->moveTo);
	AG_DelTimer(t, &t->dblClickTo);

	if (t->nResizing >= 0)
		t->nResizing = -1;

	for (i = 0; i < t->m; i++) {
		AG_TableCell *tc = t->cells[i];

		if (tc->widget != NULL) {
			AG_PostEvent(tc->widget, "widget-hidden", NULL);
		}
		if (tc->surface != -1) {
			AG_WidgetUnmapSurface(t, tc->surface);
			tc->surface = -1;
		}
	}
}

static void
Shown(AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_SELF();
	int i;

	for (i = 0; i < t->m; i++) {
		AG_TableCell *tc = t->cells[i];

		if (tc->widget != NULL)
			AG_PostEvent(tc->widget, "widget-shown", NULL);
	}
}

int
AG_TableRowSelected(AG_Table *t, int m)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if (m >= t->m) {
		goto out;
	}
	for (n = 0; n < t->n; n++) {
		if (t->cells[m][n].selected) {
			AG_ObjectUnlock(t);
			return (1);
		}
	}
out:
	AG_ObjectUnlock(t);
	return (0);
}

void
AG_TableSelectRow(AG_Table *t, int m)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if (m < t->m) {
		for (n = 0; n < t->n; n++) {
			t->cells[m][n].selected = 1;
		}
		AG_PostEvent(t, "row-selected", "%i", m);
	}

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

void
AG_TableDeselectRow(AG_Table *t, int m)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if (m < t->m)
		for (n = 0; n < t->n; n++)
			t->cells[m][n].selected = 0;

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

void
AG_TableSelectAllRows(AG_Table *t)
{
	int m, n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++) {
		for (m = 0; m < t->m; m++)
			t->cells[m][n].selected = 1;
	}

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

void
AG_TableDeselectAllRows(AG_Table *t)
{
	int m, n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++) {
		for (m = 0; m < t->m; m++)
			t->cells[m][n].selected = 0;
	}

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

void
AG_TableSelectAllCols(AG_Table *t)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++)
		t->cols[n].flags |= AG_TABLE_COL_SELECTED;

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

void
AG_TableDeselectAllCols(AG_Table *t)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++)
		t->cols[n].flags &= ~(AG_TABLE_COL_SELECTED);

	AG_Redraw(t);
	AG_ObjectUnlock(t);
}

int
AG_TableAddCol(AG_Table *t, const char *name, const char *size_spec,
    int (*sortFn)(const void *, const void *))
{
	AG_TableCol *tc, *colsNew;
	int m, n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if ((colsNew = TryRealloc(t->cols, (t->n+1)*sizeof(AG_TableCol))) == NULL) {
		n = -1;
		goto out;
	}
	t->cols = colsNew;

	tc = &t->cols[t->n];
	if (name != NULL) {
		Strlcpy(tc->name, name, sizeof(tc->name));
	} else {
		tc->name[0] = '\0';
	}
	tc->flags = 0;
	tc->sortFn = sortFn;
	tc->w = 0;
	tc->wPct = -1;
	tc->surface = -1;
	tc->ca = NULL;

	if (size_spec != NULL) {
		switch (AG_WidgetParseSizeSpec(size_spec, &tc->w)) {
		case AG_WIDGET_PERCENT:
			tc->wPct = tc->w;
			tc->w = 0;
			break;
		case AG_WIDGET_FILL:
			tc->flags |= AG_TABLE_COL_FILL;
			break;
		default:
			break;
		}
	} else {
		tc->flags |= AG_TABLE_COL_FILL;
	}

	/* Resize the row arrays. */
	for (m = 0; m < t->m; m++) {
		AG_TableCell *cNew;

		if ((cNew = TryRealloc(t->cells[m],
		    (t->n + 1)*sizeof(AG_TableCell))) == NULL) {
			n = -1;
			goto out;
		}
		t->cells[m] = cNew;
		AG_TableInitCell(t, &t->cells[m][t->n]);
	}
	n = t->n++;
	t->flags |= AG_TABLE_NEEDSORT;
	AG_Redraw(t);
out:
	AG_ObjectUnlock(t);
	return (n);
}

void
AG_TableInitCell(AG_Table *t, AG_TableCell *c)
{
	c->type = AG_CELL_NULL;
	c->fmt[0] = '\0';
	c->fnSu = NULL;
	c->fnTxt = NULL;
	c->selected = 0;
	c->surface = -1;
	c->widget = NULL;
	c->tbl = t;
	c->id = 0;
	c->nPrev = 0;
}

int
AG_TableAddRow(AG_Table *t, const char *fmtp, ...)
{
	char fmt[64], *sp = &fmt[0];
	va_list ap;
	int n, rv;
	AG_TableCell **cNew;

	Strlcpy(fmt, fmtp, sizeof(fmt));

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	if ((cNew = TryRealloc(t->cells, (t->m+1)*sizeof(AG_TableCell))) == NULL) {
		rv = -1;
		goto out;
	}
	if ((cNew[t->m] = TryMalloc(t->n*sizeof(AG_TableCell))) == NULL) {
		Free(cNew);
		rv = -1;
		goto out;
	}
	t->cells = cNew;

	va_start(ap, fmtp);
	for (n = 0; n < t->n; n++) {
		AG_TableCell *c = &t->cells[t->m][n];
		char *s = AG_Strsep(&sp, t->sep), *sc;
		int ptr = 0, lflag = 0, ptr_long = 0;
		int infmt = 0;

		AG_TableInitCell(t, c);
		if (s == NULL || s[0] == '\0') {
			continue;
		}
		Strlcpy(c->fmt, s, sizeof(c->fmt));
		for (sc = &s[0]; *sc != '\0'; sc++) {
			if (*sc == '%') {
				infmt = 1;
				continue;
			}
			if (*sc == '*' && sc[1] != '\0') {
				ptr++;
			} else if (*sc == '[' && sc[1] != '\0') {
				ptr_long++;
				break;
			} else if (*sc == 'l') {
				lflag++;
			} else if (infmt && strchr("sdixufgp]", *sc) != NULL) {
				break;
			} else if (strchr(".0123456789", *sc)) {
				continue;
			} else {
				infmt = 0;
			}
		}
		if (*sc == '\0' || !infmt) {
			c->type = AG_CELL_NULL;
			continue;
		}
		if (ptr) {
			c->data.p = va_arg(ap, void *);
		} else if (ptr_long) {
			sc++;
			c->data.p = va_arg(ap, void *);
			if (sc[0] == 's') {
				if (sc[1] == '3' && sc[2] == '2') {
					c->type = AG_CELL_PSINT32;
				} else if (s[1] == '1' && sc[2] == '6') {
					c->type = AG_CELL_PSINT16;
				} else if (s[1] == '8') {
					c->type = AG_CELL_PSINT8;
				}
			} else if (sc[0] == 'u') {
				if (sc[1] == '3' && sc[2] == '2') {
					c->type = AG_CELL_PUINT32;
				} else if (s[1] == '1' && sc[2] == '6') {
					c->type = AG_CELL_PUINT16;
				} else if (s[1] == '8') {
					c->type = AG_CELL_PUINT8;
				}
			} else if (sc[0] == 'F' && sc[1] == 't') {
				c->type = AG_CELL_FN_TXT;
				c->fnTxt = c->data.p;
				c->data.p = c;
			} else if (sc[0] == 'F' && sc[1] == 's') {
				c->type = AG_CELL_FN_SU;
				c->fnSu = c->data.p;
				c->data.p = c;
			} else if (sc[0] == 'F' && sc[1] == 'S') {
				c->type = AG_CELL_FN_SU_NODUP;
				c->fnSu = c->data.p;
				c->data.p = c;
			} else if (sc[0] == 'W') {
				AG_SizeAlloc a;

				a.x = 0;
				a.y = 0;
				a.w = 0;
				a.h = 0;
				c->type = AG_CELL_WIDGET;
				c->widget = c->data.p;
				AG_ObjectAttach(t, c->widget);
				AG_WidgetSizeAlloc(c->widget, &a);
				t->flags |= AG_TABLE_WIDGETS;
			}
		}
		switch (sc[0]) {
		case 's':
			if (ptr) {
				c->type = AG_CELL_PSTRING;
			} else {
				c->type = AG_CELL_STRING;
				{
					char *sArg = va_arg(ap, char *);

					if (sArg != NULL && sArg[0] != '\0') {
						Strlcpy(c->data.s, sArg,
						    sizeof(c->data.s));
					} else {
						c->data.s[0] = '\0';
					}
				}
			}
			break;
		case 'd':
		case 'i':
			if (lflag == 0) {
				if (ptr) {
					c->type = AG_CELL_PINT;
				} else {
					c->type = AG_CELL_INT;
					c->data.i = va_arg(ap, int);
				}
#ifdef HAVE_64BIT
			} else if (lflag == 2) {
				if (ptr) {
					c->type = AG_CELL_PINT64;
				} else {
					c->type = AG_CELL_SINT64;
					c->data.l = va_arg(ap, Sint64);
				}
#endif
			} else {
				if (ptr) {
					c->type = AG_CELL_PLONG;
				} else {
					c->type = AG_CELL_LONG;
					c->data.l = va_arg(ap, long);
				}
			}
			break;
		case 'u':
		case 'x':
			if (lflag == 0) {
				if (ptr) {
					c->type = AG_CELL_PUINT;
				} else {
					c->type = AG_CELL_UINT;
					c->data.i = va_arg(ap, int);
				}
#ifdef HAVE_64BIT
			} else if (lflag == 2) {
				if (ptr) {
					c->type = AG_CELL_PUINT64;
				} else {
					c->type = AG_CELL_UINT64;
					c->data.l = va_arg(ap, Uint64);
				}
#endif
			} else {
				if (ptr) {
					c->type = AG_CELL_PULONG;
				} else {
					c->type = AG_CELL_ULONG;
					c->data.l = va_arg(ap, Ulong);
				}
			}
			break;
		case 'f':
		case 'g':
			if (lflag) {
				if (ptr) {
					c->type = AG_CELL_PFLOAT;
				} else {
					c->type = AG_CELL_FLOAT;
					c->data.f = va_arg(ap, double);
				}
			} else {
				if (ptr) {
					c->type = AG_CELL_PDOUBLE;
				} else {
					c->type = AG_CELL_DOUBLE;
					c->data.f = va_arg(ap, double);
				}
			}
			break;
		case 'p':
			c->type = AG_CELL_POINTER;
			c->data.p = va_arg(ap, void *);
			break;
		}
	}
	va_end(ap);

	rv = t->m++;

	t->flags |= AG_TABLE_NEEDSORT;

	AG_Redraw(t);
out:
	AG_ObjectUnlock(t);
	return (rv);
}

void
AG_TableDelRow(AG_Table *t, int m)
{
	int n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++) {
		AG_TableCell *c = &t->cells[m][n];

		AG_Debug(t, "cell: %d, sel=%d\n", n, c->selected);

		/* TODO */
	}

	AG_ObjectUnlock(t);
}

int
AG_TableSaveASCII(AG_Table *t, void *pf, char sep)
{
	char buf[AG_TABLE_BUF_MAX];
	FILE *f = (FILE *)pf;
	AG_Size cSize;
	int m, n;

	AG_OBJECT_ISA(t, "AG_Widget:AG_Table:*");
	AG_ObjectLock(t);

	for (n = 0; n < t->n; n++) {
		if (t->cols[n].name[0] == '\0') {
			continue;
		}
		fputs(t->cols[n].name, f);
		fputc(sep, f);
	}
	fputc('\n', f);
	for (m = 0; m < t->m; m++) {
		for (n = 0; n < t->n; n++) {
			if (t->cols[n].name[0] == '\0') {
				continue;
			}
			if ((cSize = AG_TablePrintCell(&t->cells[m][n], buf,
			    sizeof(buf))) >= sizeof(buf)) {
				char *text = TryMalloc(cSize);

				AG_TablePrintCell(&t->cells[m][n], text, cSize);
				fputs(text, f);
				free(text);
			} else {
				fputs(buf, f);
			}
			fputc(sep, f);
		}
		fputc('\n', f);
	}

	AG_ObjectUnlock(t);
	return (0);
}

static void
ScrollbarChanged(AG_Event *_Nonnull event)
{
	AG_Table *t = AG_TABLE_PTR(1);

	SizeColumns(t);
}

static void
Init(void *_Nonnull obj)
{
	AG_Table *t = obj;
	AG_Scrollbar *sb;
	Uint i;

	WIDGET(t)->flags |= AG_WIDGET_FOCUSABLE |
	                    AG_WIDGET_UNFOCUSED_MOTION |
	                    AG_WIDGET_UNFOCUSED_BUTTONUP |
			    AG_WIDGET_USE_TEXT;

	t->sep = ":";
	t->flags = 0;
	t->hRow = agTextFontHeight+4;
	t->hCol = agTextFontHeight+4;
	t->wColMin = 16;
	t->wColDefault = 80;
	t->wHint = -1;				/* Use column size specs */
	t->hHint = t->hCol + (t->hRow << 1);
	t->r.x = 0;
	t->r.y = 0;
	t->r.w = 0;
	t->r.h = 0;
	t->selMode = AG_TABLE_SEL_ROWS;
	t->wTot = 0;
	t->xOffs = 0;
	t->mOffs = 0;
	t->mVis = 0;
	t->colAction = AG_TABLE_COL_SORT;
	t->nSorting = 0;
	t->nResizing = -1;
	t->cols = NULL;
	t->cells = NULL;
	t->n = 0;
	t->m = 0;
	for (i = 0; i < AG_TABLE_FN_LAST; i++) {
		t->fn[i] = NULL;
	}
	t->lineScrollAmount = 5;
	SLIST_INIT(&t->popups);
	t->dblClickedRow = -1;
	t->dblClickedCol = -1;
	t->dblClickedCell = -1;

	AG_InitTimer(&t->moveTo, "move", 0);
	AG_InitTimer(&t->pollTo, "poll", 0);
	AG_InitTimer(&t->dblClickTo, "dblClick", 0);

	/* Horizontal scrollbar */
	sb = t->hbar = AG_ScrollbarNew(t, AG_SCROLLBAR_HORIZ, AG_SCROLLBAR_EXCL);
	AG_SetInt(sb,  "min",     0);
	AG_BindInt(sb, "max",     &t->wTot);
	AG_BindInt(sb, "value",   &t->xOffs);
	AG_BindInt(sb, "visible", &t->r.w);
	AG_WidgetSetFocusable(sb, 0);
	AG_AddEvent(sb, "scrollbar-changed", ScrollbarChanged, "%p", t);

	/* Vertical scrollbar */
	sb = t->vbar = AG_ScrollbarNew(t, AG_SCROLLBAR_VERT, AG_SCROLLBAR_EXCL);
	AG_SetInt(sb,  "min",     0);
	AG_BindInt(sb, "max",     &t->m);
	AG_BindInt(sb, "value",   &t->mOffs);
	AG_BindInt(sb, "visible", &t->mVis);
	AG_WidgetSetFocusable(sb, 0);

	t->nPrevBuckets = 256;
	t->cPrev = Malloc(t->nPrevBuckets*sizeof(AG_TableBucket));
	for (i = 0; i < t->nPrevBuckets; i++) {
		AG_TableBucket *tb = &t->cPrev[i];
		TAILQ_INIT(&tb->cells);
	}
	TAILQ_INIT(&t->cPrevList);
	
	AG_AddEvent(t, "font-changed", OnFontChange, NULL);
	AG_SetEvent(t, "widget-lostfocus", LostFocus, NULL);
	AG_AddEvent(t, "widget-hidden", Hidden, NULL);
	AG_AddEvent(t, "widget-shown", Shown, NULL);
}

static void
Destroy(void *_Nonnull obj)
{
	AG_Table *t = obj;
	AG_TablePopup *pop, *nPop;
	AG_TableCell *c, *cNext;
	int i;

	for (pop = SLIST_FIRST(&t->popups);
	     pop != SLIST_END(&t->popups);
	     pop = nPop) {
		nPop = SLIST_NEXT(pop, popups);
		AG_ObjectDestroy(pop->menu);
		free(pop);
	}

	for (i = 0; i < t->m; i++) {
		free(t->cells[i]);
	}
	free(t->cells);

	for (c = TAILQ_FIRST(&t->cPrevList);
	     c != TAILQ_END(&t->cPrevList);
	     c = cNext) {
		cNext = TAILQ_NEXT(c, cells_list);
		free(c);
	}
	free(t->cPrev);

	Free(t->cols);
}

AG_WidgetClass agTableClass = {
	{
		"Agar(Widget:Table)",
		sizeof(AG_Table),
		{ 1,0, AGC_TABLE, 0xE030 },
		Init,
		NULL,		/* reset */
		Destroy,
		NULL,		/* load */
		NULL,		/* save */
		NULL		/* edit */
	},
	Draw,
	SizeRequest,
	SizeAllocate,
	MouseButtonDown,
	MouseButtonUp,
	MouseMotion,
	KeyDown,
	KeyUp,
	NULL,			/* touch */
	NULL,			/* ctrl */
	NULL			/* joy */
};

#endif /* AG_WIDGETS */
