/* core.c - the interaction state machine, independent of any backend. */
#include "core.h"
#include "draw.h"
#include "font.h"

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

#define MAX_LABEL 24

struct rect { int x, y, w, h; };

struct cell {
	struct rect r;		/* global coordinates */
	int screen;
	char label[MAX_LABEL];
};

enum action { ACT_NONE, ACT_LEFT, ACT_MIDDLE, ACT_RIGHT, ACT_DOUBLE, ACT_DRAG };

/* ---- labels ---------------------------------------------------------- */

static int label_len(int base, int n)
{
	int len = 1;
	long cap = base;
	while (cap < n) {
		cap *= base;
		len++;
	}
	return len;
}

static void make_label(char *out, const char *alpha, int base, int len, int idx)
{
	out[len] = 0;
	for (int i = len - 1; i >= 0; i--) {
		out[i] = alpha[idx % base];
		idx /= base;
	}
}

/* ---- geometry -------------------------------------------------------- */

static int fit_scale(int cw, int ch, int len)
{
	int wunit = len * FONT_W + (len - 1);	/* label width at scale 1 */
	int sw = (cw * 8 / 10) / wunit;		/* fit 80% of cell width */
	int sh = (ch * 6 / 10) / FONT_H;	/* fit 60% of cell height */
	int s = sw < sh ? sw : sh;
	if (s < 1)
		s = 1;
	if (s > 8)
		s = 8;
	return s;
}

static int screen_at(struct platform *p, int x, int y)
{
	for (int s = 0; s < p->n_screens; s++) {
		struct mb_screen c = p->screens[s];
		if (x >= c.x && x < c.x + c.w && y >= c.y && y < c.y + c.h)
			return s;
	}
	return -1;
}

static int build_cells(const struct config *cfg, struct platform *p,
		       struct cell **out, int *out_len)
{
	int base = (int)strlen(cfg->alphabet);
	int total = 0;
	int cols[MB_MAX_SCREENS], rows[MB_MAX_SCREENS];

	for (int s = 0; s < p->n_screens; s++) {
		struct mb_screen sc = p->screens[s];
		int c, r;
		if (cfg->cols > 0 && cfg->rows > 0) {
			c = cfg->cols;
			r = cfg->rows;
		} else {
			int cs = cfg->cell_size > 0 ? cfg->cell_size : 100;
			c = (sc.w + cs / 2) / cs;
			r = (sc.h + cs / 2) / cs;
		}
		if (c < 1)
			c = 1;
		if (r < 1)
			r = 1;
		if (c > MB_MAX_GRID_DIM)
			c = MB_MAX_GRID_DIM;
		if (r > MB_MAX_GRID_DIM)
			r = MB_MAX_GRID_DIM;
		cols[s] = c;
		rows[s] = r;
		total += c * r;
	}
	if (total <= 0)
		return 0;

	int len = label_len(base, total);
	if (len >= MAX_LABEL)
		len = MAX_LABEL - 1;

	struct cell *cells = calloc((size_t)total, sizeof *cells);
	if (!cells)
		return 0;

	int idx = 0;
	for (int s = 0; s < p->n_screens; s++) {
		struct mb_screen sc = p->screens[s];
		for (int cy = 0; cy < rows[s]; cy++) {
			for (int cx = 0; cx < cols[s]; cx++) {
				int x0 = sc.x + cx * sc.w / cols[s];
				int x1 = sc.x + (cx + 1) * sc.w / cols[s];
				int y0 = sc.y + cy * sc.h / rows[s];
				int y1 = sc.y + (cy + 1) * sc.h / rows[s];
				struct cell *cl = &cells[idx];
				cl->r = (struct rect){ x0, y0, x1 - x0,
						       y1 - y0 };
				cl->screen = s;
				make_label(cl->label, cfg->alphabet, base, len,
					   idx);
				idx++;
			}
		}
	}

	*out = cells;
	*out_len = len;
	return total;
}

/* ---- rendering ------------------------------------------------------- */

/* Begin every screen's overlay and paint the dim backdrop, filling bufs[]. */
static int frame_begin_dim(struct platform *p, const struct config *cfg,
			   struct mb_buffer **bufs)
{
	for (int s = 0; s < p->n_screens; s++) {
		bufs[s] = p->frame_begin(p, s);
		if (!bufs[s])
			return -1;
		draw_fill(bufs[s], 0, 0, bufs[s]->w, bufs[s]->h, cfg->color_bg);
	}
	return 0;
}

static int render_grid(struct platform *p, const struct config *cfg,
		       struct cell *cells, int ncells, int len,
		       const char *typed)
{
	struct mb_buffer *bufs[MB_MAX_SCREENS];
	if (frame_begin_dim(p, cfg, bufs))
		return -1;

	int tlen = (int)strlen(typed);
	for (int i = 0; i < ncells; i++) {
		struct cell *cl = &cells[i];
		struct mb_buffer *b = bufs[cl->screen];
		struct mb_screen sc = p->screens[cl->screen];
		int lx = cl->r.x - sc.x, ly = cl->r.y - sc.y;

		draw_rect(b, lx, ly, cl->r.w, cl->r.h, 1, cfg->color_grid);
		if (tlen && strncmp(cl->label, typed, (size_t)tlen) != 0)
			continue;

		int scale = cfg->font_scale > 0 ? cfg->font_scale
					: fit_scale(cl->r.w, cl->r.h, len);
		const char *lab = cl->label;
		int tx = lx + (cl->r.w - text_width(lab, scale)) / 2;
		int ty = ly + (cl->r.h - text_height(scale)) / 2;

		if (tlen) {
			char pre[MAX_LABEL];
			memcpy(pre, lab, (size_t)tlen);
			pre[tlen] = 0;
			draw_text(b, tx, ty, pre, scale, cfg->color_typed);
			int off = text_width(pre, scale) + scale;
			draw_text(b, tx + off, ty, lab + tlen, scale,
				  cfg->color_label);
		} else {
			draw_text(b, tx, ty, lab, scale, cfg->color_label);
		}
	}
	return p->frame_commit(p);
}

/* Draw a refine key centred in its quadrant, while the quadrant is large enough
 * for the glyph to be legible; it shrinks away as the region is bisected. */
static void draw_quad_key(struct mb_buffer *b, int qx, int qy, int qw, int qh,
			  char key, uint32_t fg)
{
	int smin = qw < qh ? qw : qh;
	int scale = (smin / 2) / FONT_W;
	if (scale < 1)
		return;
	if (scale > 6)
		scale = 6;
	char s[2] = { key, 0 };
	int tx = qx + (qw - text_width(s, scale)) / 2;
	int ty = qy + (qh - text_height(scale)) / 2;
	draw_text(b, tx, ty, s, scale, fg);
}

static int render_refine(struct platform *p, const struct config *cfg,
			 struct rect cur)
{
	struct mb_buffer *bufs[MB_MAX_SCREENS];
	if (frame_begin_dim(p, cfg, bufs))
		return -1;

	int cx = cur.x + cur.w / 2, cy = cur.y + cur.h / 2;
	int s = screen_at(p, cx, cy);
	if (s < 0)
		s = 0;
	struct mb_buffer *b = bufs[s];
	struct mb_screen sc = p->screens[s];
	int lx = cur.x - sc.x, ly = cur.y - sc.y;
	uint32_t ac = cfg->color_active;
	/* Guide lines at ~38% of the active alpha, so they track opacity. */
	uint32_t guide = (ac & 0x00ffffff) | (((ac >> 24) * 0x60 / 0xff) << 24);

	draw_rect(b, lx, ly, cur.w, cur.h, 2, ac);
	draw_vline(b, lx + cur.w / 2, ly, cur.h, 1, guide);
	draw_hline(b, lx, ly + cur.h / 2, cur.w, 1, guide);

	int hw = cur.w / 2, hh = cur.h / 2;
	int rw = cur.w - hw, rh = cur.h - hh;
	draw_quad_key(b, lx, ly, hw, hh, cfg->k_tl, cfg->color_label);
	draw_quad_key(b, lx + hw, ly, rw, hh, cfg->k_tr, cfg->color_label);
	draw_quad_key(b, lx, ly + hh, hw, rh, cfg->k_bl, cfg->color_label);
	draw_quad_key(b, lx + hw, ly + hh, rw, rh, cfg->k_br, cfg->color_label);

	int gx = lx + cur.w / 2, gy = ly + cur.h / 2;
	draw_hline(b, gx - 12, gy - 1, 25, 2, ac);
	draw_vline(b, gx - 1, gy - 12, 25, 2, ac);
	return p->frame_commit(p);
}

/* ---- actions --------------------------------------------------------- */

static enum action action_for(const struct config *cfg, uint32_t k)
{
	if (k == MB_KEY_ENTER)
		return ACT_LEFT;
	if (k > 0x7e)
		return ACT_NONE;
	char c = (char)k;
	if (c == cfg->k_left)
		return ACT_LEFT;
	if (c == cfg->k_middle)
		return ACT_MIDDLE;
	if (c == cfg->k_right)
		return ACT_RIGHT;
	if (c == cfg->k_double)
		return ACT_DOUBLE;
	if (c == cfg->k_drag)
		return ACT_DRAG;
	return ACT_NONE;
}

#define CLICK_MS 12 /* down->up and inter-click gap; 0ms clicks get missed */

static int do_click(struct platform *p, enum mb_button b, int times)
{
	for (int i = 0; i < times; i++) {
		if (i)
			platform_sleep_ms(CLICK_MS);
		if (p->pointer_button(p, b, 1)) {
			p->pointer_button(p, b, 0);
			return -1;
		}
		platform_sleep_ms(CLICK_MS);
		if (p->pointer_button(p, b, 0))
			return -1;
	}
	return 0;
}

static int emit(struct platform *p, int dry, int x, int y, enum action a)
{
	static const char *name[] = { "none", "left", "middle",
				      "right", "double", "drag" };
	if (dry) {
		printf("%d %d %s\n", x, y, name[a]);
		return 0;
	}
	if (p->pointer_move(p, x, y))
		return -1;
	switch (a) {
	case ACT_LEFT:
		return do_click(p, MB_LEFT, 1);
	case ACT_MIDDLE:
		return do_click(p, MB_MIDDLE, 1);
	case ACT_RIGHT:
		return do_click(p, MB_RIGHT, 1);
	case ACT_DOUBLE:
		return do_click(p, MB_LEFT, 2);
	default:
		return 0;
	}
}

/* ---- main flow ------------------------------------------------------- */

int core_run(struct platform *p, const struct config *cfg, int dry)
{
	if (cfg->cols > 0 && cfg->rows > 0) {
		for (int s = 0; s < p->n_screens; s++) {
			if (cfg->cols > p->screens[s].w ||
			    cfg->rows > p->screens[s].h) {
				fprintf(stderr,
					"mouseboard: grid exceeds screen dimensions\n");
				return 2;
			}
		}
	}
	struct cell *cells;
	int len;
	int ncells = build_cells(cfg, p, &cells, &len);
	if (ncells <= 0) {
		fprintf(stderr, "mouseboard: no screen cells\n");
		return 2;
	}

	enum { GRID, REFINE } state = GRID;
	char typed[MAX_LABEL] = "";
	int tn = 0;
	struct rect stack[64];
	int sp = 0;
	int drag = 0;
	int rc = 2;

	if (render_grid(p, cfg, cells, ncells, len, typed))
		goto out;

	for (;;) {
		uint32_t k = p->next_key(p);
		if (k >= 'A' && k <= 'Z')
			k += 'a' - 'A'; /* labels and bindings are lowercase;
					   makes Caps Lock harmless */
		if (k == MB_KEY_NONE) {
			rc = 2;
			break;
		}
		if (k == MB_KEY_ESC) {
			rc = 1;
			break;
		}

		if (state == GRID) {
			if (k == MB_KEY_BACKSPACE) {
				if (tn) {
					typed[--tn] = 0;
					if (render_grid(p, cfg, cells, ncells, len,
							 typed))
						break;
				}
				continue;
			}
			if (k < 0x20 || k > 0x7e || tn >= MAX_LABEL - 1)
				continue;

			typed[tn] = (char)k;
			typed[tn + 1] = 0;
			int matches = 0, exact = -1;
			for (int i = 0; i < ncells; i++) {
				if (strncmp(cells[i].label, typed,
					    (size_t)(tn + 1)))
					continue;
				matches++;
				if ((int)strlen(cells[i].label) == tn + 1)
					exact = i;
			}
			if (matches == 0) {
				typed[tn] = 0; /* reject, keep prompt */
				continue;
			}
			tn++;
			if (exact >= 0 && tn == len) {
				stack[0] = cells[exact].r;
				sp = 1;
				state = REFINE;
				struct rect c = stack[0];
				if (!dry) {
					if (p->pointer_move(p, c.x + c.w / 2,
							    c.y + c.h / 2))
						break;
				}
				if (render_refine(p, cfg, c))
					break;
			} else {
				if (render_grid(p, cfg, cells, ncells, len, typed))
					break;
			}
			continue;
		}

		/* REFINE */
		struct rect cur = stack[sp - 1];
		if (k == MB_KEY_BACKSPACE) {
			if (sp > 1) {
				sp--;
				cur = stack[sp - 1];
				if (!dry) {
					if (p->pointer_move(p, cur.x + cur.w / 2,
							    cur.y + cur.h / 2))
						break;
				}
				if (render_refine(p, cfg, cur))
					break;
			} else {
				state = GRID;
				tn = 0;
				typed[0] = 0;
				if (render_grid(p, cfg, cells, ncells, len, typed))
					break;
			}
			continue;
		}

		char c = (k <= 0x7e) ? (char)k : 0;
		int hw = cur.w / 2, hh = cur.h / 2;
		int rw = cur.w - hw, rh = cur.h - hh;
		struct rect nr;
		int split = 1;
		if (c && c == cfg->k_tl)
			nr = (struct rect){ cur.x, cur.y, hw, hh };
		else if (c && c == cfg->k_tr)
			nr = (struct rect){ cur.x + hw, cur.y, rw, hh };
		else if (c && c == cfg->k_bl)
			nr = (struct rect){ cur.x, cur.y + hh, hw, rh };
		else if (c && c == cfg->k_br)
			nr = (struct rect){ cur.x + hw, cur.y + hh, rw, rh };
		else
			split = 0;

		if (split) {
			if (sp >= (int)(sizeof stack / sizeof stack[0]))
				continue; /* depth limit; the region has been
					     1x1 for dozens of splits by now */
			if (nr.w < 1)
				nr.w = 1;
			if (nr.h < 1)
				nr.h = 1;
			stack[sp++] = nr;
			cur = nr;
			if (!dry) {
				if (p->pointer_move(p, cur.x + cur.w / 2,
						    cur.y + cur.h / 2))
					break;
			}
			if (render_refine(p, cfg, cur))
				break;
			continue;
		}

		enum action a = action_for(cfg, k);
		if (a == ACT_NONE)
			continue;

		int tx = cur.x + cur.w / 2, ty = cur.y + cur.h / 2;
		if (a == ACT_DRAG && !drag) {
			if (dry)
				printf("%d %d drag-start\n", tx, ty);
			else {
				if (p->pointer_move(p, tx, ty))
					break;
				if (p->pointer_button(p, MB_LEFT, 1)) {
					p->pointer_button(p, MB_LEFT, 0);
					break;
				}
			}
			drag = 1;
			state = GRID;
			tn = 0;
			typed[0] = 0;
			if (render_grid(p, cfg, cells, ncells, len, typed))
				break;
			continue;
		}
		if (drag) {
			if (dry)
				printf("%d %d drag-end\n", tx, ty);
			else {
				if (p->pointer_move(p, tx, ty) ||
				    p->pointer_button(p, MB_LEFT, 0))
					break;
			}
			drag = 0;
			rc = 0;
			break;
		}
		if (emit(p, dry, tx, ty, a))
			break;
		rc = 0;
		break;
	}

out:
	if (drag && !dry && p->pointer_button(p, MB_LEFT, 0))
		rc = 2;
	free(cells);
	return rc;
}

/* ---- offline self-test ---------------------------------------------- */

static int st_fail;

static void check(int cond, const char *what)
{
	if (!cond) {
		st_fail++;
		printf("FAIL: %s\n", what);
	} else {
		printf("ok:   %s\n", what);
	}
}

struct test_platform {
	uint32_t keys[8];
	int key_count;
	int key_pos;
	int commits;
	int moves;
	int buttons;
	int last_x, last_y;
	int fail_commit;
	int fail_move;
	int fail_button_down;
	struct mb_buffer buffer;
	uint32_t pixels[100 * 80];
};

static struct mb_buffer *test_frame_begin(struct platform *p, int screen)
{
	(void)screen;
	return &((struct test_platform *)p->priv)->buffer;
}

static int test_frame_commit(struct platform *p)
{
	struct test_platform *t = p->priv;
	t->commits++;
	return t->fail_commit ? -1 : 0;
}

static uint32_t test_next_key(struct platform *p)
{
	struct test_platform *t = p->priv;
	if (t->key_pos >= t->key_count)
		return MB_KEY_NONE;
	return t->keys[t->key_pos++];
}

static int test_pointer_move(struct platform *p, int x, int y)
{
	struct test_platform *t = p->priv;
	t->moves++;
	t->last_x = x;
	t->last_y = y;
	return t->fail_move ? -1 : 0;
}

static int test_pointer_button(struct platform *p, enum mb_button b, int press)
{
	(void)b;
	struct test_platform *t = p->priv;
	t->buttons++;
	if (press && t->fail_button_down)
		return -1;
	return 0;
}

static void test_platform_init(struct platform *p, struct test_platform *t)
{
	memset(t, 0, sizeof *t);
	*p = (struct platform){ 0 };
	p->n_screens = 1;
	p->screens[0] = (struct mb_screen){ 0, 0, 100, 80 };
	p->frame_begin = test_frame_begin;
	p->frame_commit = test_frame_commit;
	p->next_key = test_next_key;
	p->pointer_move = test_pointer_move;
	p->pointer_button = test_pointer_button;
	p->priv = t;
	t->buffer = (struct mb_buffer){ t->pixels, 100, 80, 100, 1 };
}

int core_selftest(void)
{
	st_fail = 0;

	/* label length and base conversion */
	check(label_len(2, 4) == 2, "label_len(2,4)==2");
	check(label_len(26, 26) == 1, "label_len(26,26)==1");
	check(label_len(26, 27) == 2, "label_len(26,27)==2");

	const char *alpha = "abcdef";
	int base = 6, len = label_len(base, 36);
	check(len == 2, "len for 36 over base 6 == 2");
	char l0[8], l1[8], l35[8];
	make_label(l0, alpha, base, len, 0);
	make_label(l1, alpha, base, len, 1);
	make_label(l35, alpha, base, len, 35);
	check(!strcmp(l0, "aa"), "label 0 == aa");
	check(!strcmp(l1, "ab"), "label 1 == ab");
	check(!strcmp(l35, "ff"), "label 35 == ff");

	/* labels are unique across a range */
	int dup = 0;
	char seen[36][8];
	for (int i = 0; i < 36; i++)
		make_label(seen[i], alpha, base, len, i);
	for (int i = 0; i < 36 && !dup; i++)
		for (int j = i + 1; j < 36; j++)
			if (!strcmp(seen[i], seen[j]))
				dup = 1;
	check(!dup, "labels unique over full range");

	/* bisection converges and stays inside the cell */
	struct rect r = { 0, 0, 1000, 800 };
	struct rect cur = r;
	for (int i = 0; i < 12; i++) {
		int hw = cur.w / 2, hh = cur.h / 2;
		cur = (struct rect){ cur.x, cur.y, hw, hh }; /* keep TL */
	}
	check(cur.w <= 1 && cur.h <= 1, "bisection shrinks to a point");
	check(cur.x >= r.x && cur.y >= r.y, "bisection stays inside cell");

	/* config parsing */
	struct config cfg;
	config_defaults(&cfg);
	check(config_set(&cfg, "color_active", "#112233") == 0,
	      "parse #RRGGBB");
	check(cfg.color_active == 0xff112233, "color #RRGGBB -> 0xAARRGGBB");
	check(config_set(&cfg, "color_bg", "#11223344") == 0,
	      "parse #RRGGBBAA");
	check(cfg.color_bg == 0x44112233, "color #RRGGBBAA -> 0xAARRGGBB");
	check(config_set(&cfg, "click_left", "z") == 0, "parse binding");
	check(cfg.k_left == 'z', "binding applied");
	check(config_set(&cfg, "bogus", "x") == -1, "unknown key rejected");
	check(config_set(&cfg, "cell_size", "abc") == -2, "bad int rejected");
	check(config_set(&cfg, "click_left", "Z") == -2,
	      "uppercase binding rejected");
	check(config_set(&cfg, "alphabet", "abC") == -2,
	      "uppercase alphabet rejected");
	check(config_set(&cfg, "opacity_bg", "50") == 0, "parse opacity_bg");
	check(config_set(&cfg, "opacity_fg", "150") == -2,
	      "opacity >100 rejected");
	check(config_validate(&cfg) == NULL, "default config is valid");
	struct config bad = cfg;
	bad.rows = 2;
	check(config_validate(&bad) != NULL, "incomplete grid rejected");
	bad = cfg;
	bad.k_drag = bad.k_tl;
	check(config_validate(&bad) != NULL, "binding collision rejected");
	bad = cfg;
	bad.font_scale = MB_MAX_FONT_SCALE + 1;
	check(config_validate(&bad) != NULL, "excessive font scale rejected");

	/* opacity_bg drives the backdrop, opacity_fg everything else; the
	   full-alpha default colours make 100 mean solid */
	struct config oc;
	config_defaults(&oc);
	config_apply_opacity(&oc);
	check((oc.color_bg >> 24) == 0xff * 33 / 100,
	      "opacity_bg scales backdrop alpha");
	check((oc.color_label >> 24) == 0xff * 65 / 100,
	      "opacity_fg scales label alpha");
	struct config os;
	config_defaults(&os);
	os.opacity_bg = 100;
	config_apply_opacity(&os);
	check((os.color_bg >> 24) == 0xff, "opacity_bg 100 is solid");

	/* build_cells tiles every screen with uniquely labelled cells */
	struct platform pl = { 0 };
	pl.n_screens = 2;
	pl.screens[0] = (struct mb_screen){ 0, 0, 1000, 800 };
	pl.screens[1] = (struct mb_screen){ 1000, 0, 1920, 1080 };
	struct config c2;
	config_defaults(&c2);
	struct cell *cells;
	int clen;
	int n = build_cells(&c2, &pl, &cells, &clen);
	check(n > 0, "build_cells returns cells");
	long area[2] = { 0, 0 };
	int inside = 1, unique = 1;
	for (int i = 0; i < n; i++) {
		struct mb_screen sc = pl.screens[cells[i].screen];
		struct rect cr = cells[i].r;
		if (cr.x < sc.x || cr.y < sc.y || cr.x + cr.w > sc.x + sc.w ||
		    cr.y + cr.h > sc.y + sc.h)
			inside = 0;
		area[cells[i].screen] += (long)cr.w * cr.h;
		for (int j = i + 1; j < n && unique; j++)
			if (!strcmp(cells[i].label, cells[j].label))
				unique = 0;
	}
	check(inside, "cells stay inside their screen");
	check(area[0] == 1000L * 800 && area[1] == 1920L * 1080,
	      "cells tile each screen exactly");
	check(unique, "cell labels unique across screens");
	check(n > 0 && (int)strlen(cells[0].label) == clen,
	      "label length matches build_cells");
	free(cells);

	/* Complete state-machine flows through a fake dependency-free backend. */
	struct platform tp;
	struct test_platform ts;
	struct config tc;
	config_defaults(&tc);
	tc.cols = tc.rows = 1;
	test_platform_init(&tp, &ts);
	ts.keys[0] = (uint32_t)tc.alphabet[0];
	ts.keys[1] = MB_KEY_ENTER;
	ts.key_count = 2;
	check(core_run(&tp, &tc, 0) == 0, "core click flow succeeds");
	check(ts.moves == 2 && ts.buttons == 2, "click emits move, down and up");
	check(ts.last_x == 50 && ts.last_y == 40, "click targets cell centre");

	test_platform_init(&tp, &ts);
	ts.keys[0] = (uint32_t)tc.alphabet[0];
	ts.keys[1] = (uint32_t)tc.k_drag;
	ts.keys[2] = MB_KEY_ESC;
	ts.key_count = 3;
	check(core_run(&tp, &tc, 0) == 1, "drag cancellation reports cancel");
	check(ts.buttons == 2, "drag cancellation releases held button");

	test_platform_init(&tp, &ts);
	ts.keys[0] = (uint32_t)tc.alphabet[0];
	ts.key_count = 1;
	ts.fail_move = 1;
	check(core_run(&tp, &tc, 0) == 2, "pointer failure reaches caller");
	check(ts.buttons == 0, "pointer failure emits no click");

	test_platform_init(&tp, &ts);
	ts.fail_commit = 1;
	check(core_run(&tp, &tc, 0) == 2, "presentation failure reaches caller");

	test_platform_init(&tp, &ts);
	ts.keys[0] = (uint32_t)tc.alphabet[0];
	ts.keys[1] = MB_KEY_ENTER;
	ts.key_count = 2;
	ts.fail_button_down = 1;
	check(core_run(&tp, &tc, 0) == 2, "button failure reaches caller");
	check(ts.buttons == 2, "failed button press gets release attempt");

	printf(st_fail ? "\n%d FAILED\n" : "\nall passed\n", st_fail);
	return st_fail ? 1 : 0;
}
