/*
 *  libzvbi - Teletext formatter
 *
 *  Copyright (C) 2000, 2001 Michael H. Schimek
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/* $Id: teletext.c,v 1.23 2006/05/24 04:46:45 mschimek Exp $ */

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include "site_def.h"

#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <strings.h> /* strncasecmp */
#include <ctype.h>
#include <assert.h>

#include "bcd.h"
#include "vt.h"
#include "export.h"
#include "vbi.h"
#include "hamm.h"
#include "lang.h"

extern const char _zvbi_intl_domainname[];

#include "intl-priv.h"

#define DEBUG 0

#if DEBUG
#define printv(templ, args...) fprintf(stderr, templ ,##args)
#else
#define printv(templ, args...)						\
do {									\
	if (0)								\
		fprintf(stderr, templ ,##args);				\
} while (0)
#endif

#define ROWS			25
#define COLUMNS			40
#define EXT_COLUMNS		41
#define LAST_ROW		((ROWS - 1) * EXT_COLUMNS)

/*
 *  FLOF navigation
 */

static const vbi_color
flof_link_col[4] = { VBI_RED, VBI_GREEN, VBI_YELLOW, VBI_CYAN };

static inline void
flof_navigation_bar(vbi_page *pg, vt_page *vtp)
{
	vbi_char ac;
	int n, i, k, ii;

	memset(&ac, 0, sizeof(ac));

	ac.foreground	= VBI_WHITE;
	ac.background	= VBI_BLACK;
	ac.opacity	= pg->page_opacity[1];
	ac.unicode	= 0x0020;

	for (i = 0; i < EXT_COLUMNS; i++)
		pg->text[LAST_ROW + i] = ac;

	ac.link = TRUE;

	for (i = 0; i < 4; i++) {
		ii = i * 10 + 3;

		for (k = 0; k < 3; k++) {
			n = ((vtp->data.lop.link[i].pgno >> ((2 - k) * 4)) & 15) + '0';

			if (n > '9')
				n += 'A' - '9';

			ac.unicode = n;
			ac.foreground = flof_link_col[i];
			pg->text[LAST_ROW + ii + k] = ac;
			pg->nav_index[ii + k] = i;
		}

		pg->nav_link[i].pgno = vtp->data.lop.link[i].pgno;
		pg->nav_link[i].subno = vtp->data.lop.link[i].subno;
	}
}

static inline void
flof_links(vbi_page *pg, vt_page *vtp)
{
	vbi_char *acp = pg->text + LAST_ROW;
	int i, j, k, col = -1, start = 0;

	for (i = 0; i < COLUMNS + 1; i++) {
		if (i == COLUMNS || (acp[i].foreground & 7) != col) {
			for (k = 0; k < 4; k++)
				if ((int) flof_link_col[k] == col)
					break;

			if (k < 4 && !NO_PAGE(vtp->data.lop.link[k].pgno)) {
				/* Leading and trailing spaces not sensitive */

				for (j = i - 1; j >= start && acp[j].unicode == 0x0020; j--);

				for (; j >= start; j--) {
					acp[j].link = TRUE;
					pg->nav_index[j] = k;
				}

		    		pg->nav_link[k].pgno = vtp->data.lop.link[k].pgno;
		    		pg->nav_link[k].subno = vtp->data.lop.link[k].subno;
			}

			if (i >= COLUMNS)
				break;

			col = acp[i].foreground & 7;
			start = i;
		}

		if (start == i && acp[i].unicode == 0x0020)
			start++;
	}
}

/*
 *  TOP navigation
 */

static void character_set_designation(struct vbi_font_descr **font,
				      vt_extension *ext, vt_page *vtp);
static void screen_color(vbi_page *pg, int flags, int color);

static vbi_bool
top_label(vbi_decoder *vbi, vbi_page *pg, struct vbi_font_descr *font,
	  int index, int pgno, int foreground, int ff)
{
	int column = index * 13 + 1;
	vt_page *vtp;
	vbi_char *acp;
	ait_entry *ait;
	int i, j;

	acp = &pg->text[LAST_ROW + column];

	for (i = 0; i < 8; i++)
		if (vbi->vt.btt_link[i].type == 2) {
			vtp = vbi_cache_get(vbi,
					    vbi->vt.btt_link[i].pgno,
					    vbi->vt.btt_link[i].subno, 0x3f7f);
			if (!vtp) {
				printv("top ait page %x not cached\n", vbi->vt.btt_link[i].pgno);
				continue;
			} else if (vtp->function != PAGE_FUNCTION_AIT) {
				printv("no ait page %x\n", vtp->pgno);
				continue;
			}

			for (ait = vtp->data.ait, j = 0; j < 46; ait++, j++)
				if (ait->page.pgno == pgno) {
					pg->nav_link[index].pgno = pgno;
					pg->nav_link[index].subno = VBI_ANY_SUBNO;

					for (i = 11; i >= 0; i--)
						if (ait->text[i] > 0x20)
							break;

					if (ff && (i <= (11 - ff))) {
						acp += (11 - ff - i) >> 1;
						column += (11 - ff - i) >> 1;

						acp[i + 1].link = TRUE;
						pg->nav_index[column + i + 1] = index;

						acp[i + 2].unicode = 0x003E;
						acp[i + 2].foreground = foreground;
						acp[i + 2].link = TRUE;
						pg->nav_index[column + i + 2] = index;

						if (ff > 1) {
							acp[i + 3].unicode = 0x003E;
							acp[i + 3].foreground = foreground;
							acp[i + 3].link = TRUE;
							pg->nav_index[column + i + 3] = index;
						}
					} else {
						acp += (11 - i) >> 1;
						column += (11 - i) >> 1;
					}

					for (; i >= 0; i--) {
						acp[i].unicode = vbi_teletext_unicode(font->G0, font->subset,
							(ait->text[i] < 0x20) ? 0x20 : ait->text[i]);
						acp[i].foreground = foreground;
						acp[i].link = TRUE;
						pg->nav_index[column + i] = index;
					}

					return TRUE;
				}
		}

	return FALSE;
}

static __inline__ vbi_pgno
add_modulo			(vbi_pgno		pgno,
				 int			incr)
{
	return ((pgno - 0x100 + incr) & 0x7FF) + 0x100;
}

static inline void
top_navigation_bar(vbi_decoder *vbi, vbi_page *pg,
		   vt_page *vtp)
{
	vbi_char ac;
	vbi_pgno pgno1;
	int i, got;

	printv("PAGE MIP/BTT: %d\n", vbi->vt.page_info[vtp->pgno - 0x100].code);

	memset(&ac, 0, sizeof(ac));

	ac.foreground	= 32 + VBI_WHITE;
	ac.background	= 32 + VBI_BLACK;
	ac.opacity	= pg->page_opacity[1];
	ac.unicode	= 0x0020;

	for (i = 0; i < EXT_COLUMNS; i++)
		pg->text[LAST_ROW + i] = ac;

	if (pg->page_opacity[1] != VBI_OPAQUE)
		return;

	pgno1 = add_modulo (vtp->pgno, 1);

	for (i = vtp->pgno; i != pgno1; i = add_modulo (i, -1))
		if (vbi->vt.page_info[i - 0x100].code == VBI_TOP_BLOCK ||
		    vbi->vt.page_info[i - 0x100].code == VBI_TOP_GROUP) {
			top_label(vbi, pg, pg->font[0], 0, i, 32 + VBI_WHITE, 0);
			break;
		}

	for (i = pgno1, got = FALSE; i != vtp->pgno; i = add_modulo (i, 1))
		switch (vbi->vt.page_info[i - 0x100].code) {
		case VBI_TOP_BLOCK:
			top_label(vbi, pg, pg->font[0], 2, i, 32 + VBI_YELLOW, 2);
			return;

		case VBI_TOP_GROUP:
			if (!got) {
				top_label(vbi, pg, pg->font[0], 1, i, 32 + VBI_GREEN, 1);
				got = TRUE;
			}

			break;
		}
}

static ait_entry *
next_ait(vbi_decoder *vbi, int pgno, int subno, vt_page **mvtp)
{
	vt_page *vtp;
	ait_entry *ait, *mait = NULL;
	int mpgno = 0xFFF, msubno = 0xFFFF;
	int i, j;

	*mvtp = NULL;

	for (i = 0; i < 8; i++) {
		if (vbi->vt.btt_link[i].type == 2) {
			vtp = vbi_cache_get(vbi,
					    vbi->vt.btt_link[i].pgno, 
					    vbi->vt.btt_link[i].subno, 0x3f7f);

			if (!vtp) {
				printv("top ait page %x not cached\n", vbi->vt.btt_link[i].pgno);
				continue;
			} else if (vtp->function != PAGE_FUNCTION_AIT) {
				printv("no ait page %x\n", vtp->pgno);
				continue;
			}

			for (ait = vtp->data.ait, j = 0; j < 46; ait++, j++) {
				if (!ait->page.pgno)
					break;

				if (ait->page.pgno < pgno
				    || (ait->page.pgno == pgno && ait->page.subno <= subno))
					continue;

				if (ait->page.pgno > mpgno
				    || (ait->page.pgno == mpgno && ait->page.subno > msubno))
					continue;

				mait = ait;
				mpgno = ait->page.pgno;
				msubno = ait->page.subno;
				*mvtp = vtp;
			}
		}
	}

	return mait;
}

static int
top_index(vbi_decoder *vbi, vbi_page *pg, int subno)
{
	vt_page *vtp;
	vbi_char ac, *acp;
	ait_entry *ait;
	int i, j, k, n, lines;
	int xpgno, xsubno;
	vt_extension *ext;
	char *index_str;

	pg->vbi = vbi;

	subno = vbi_bcd2dec(subno);

	pg->rows = ROWS;
	pg->columns = EXT_COLUMNS;

	pg->dirty.y0 = 0;
	pg->dirty.y1 = ROWS - 1;
	pg->dirty.roll = 0;

	ext = &vbi->vt.magazine[0].extension;

	screen_color(pg, 0, 32 + VBI_BLUE);

	vbi_transp_colormap(vbi, pg->color_map, ext->color_map, 40);

	pg->drcs_clut = ext->drcs_clut;

	pg->page_opacity[0] = VBI_OPAQUE;
	pg->page_opacity[1] = VBI_OPAQUE;
	pg->boxed_opacity[0] = VBI_OPAQUE;
	pg->boxed_opacity[1] = VBI_OPAQUE;

	memset(pg->drcs, 0, sizeof(pg->drcs));

	memset(&ac, 0, sizeof(ac));

	ac.foreground	= VBI_BLACK; // 32 + VBI_BLACK;
	ac.background	= 32 + VBI_BLUE;
	ac.opacity	= VBI_OPAQUE;
	ac.unicode	= 0x0020;
	ac.size		= VBI_NORMAL_SIZE;

	for (i = 0; i < EXT_COLUMNS * ROWS; i++)
		pg->text[i] = ac;

	ac.size = VBI_DOUBLE_SIZE;

	/* FIXME */
	/* TRANSLATORS: Title of TOP Index page,
	   for now please Latin-1 or ASCII only */
	index_str = _("TOP Index");

	for (i = 0; index_str[i]; i++) {
		ac.unicode = index_str[i];
		pg->text[1 * EXT_COLUMNS + 2 + i * 2] = ac;
	}

	ac.size = VBI_NORMAL_SIZE;

	acp = &pg->text[4 * EXT_COLUMNS];
	lines = 17;
	xpgno = 0;
	xsubno = 0;

	while ((ait = next_ait(vbi, xpgno, xsubno, &vtp))) {
		xpgno = ait->page.pgno;
		xsubno = ait->page.subno;

		/* No docs, correct? */
		character_set_designation(pg->font, ext, vtp);

		if (subno > 0) {
			if (lines-- == 0) {
				subno--;
				lines = 17;
			}

			continue;
		} else if (lines-- <= 0)
			continue;

		for (i = 11; i >= 0; i--)
			if (ait->text[i] > 0x20)
				break;

		switch (vbi->vt.page_info[ait->page.pgno - 0x100].code) {
		case VBI_TOP_GROUP:
			k = 3;
			break;

		default:
    			k = 1;
		}

		for (j = 0; j <= i; j++) {
			acp[k + j].unicode = vbi_teletext_unicode(pg->font[0]->G0,
				pg->font[0]->subset, (ait->text[j] < 0x20) ? 0x20 : ait->text[j]);
		}

		for (k += i + 2; k <= 33; k++)
			acp[k].unicode = '.';

		for (j = 0; j < 3; j++) {
			n = ((ait->page.pgno >> ((2 - j) * 4)) & 15) + '0';

			if (n > '9')
				n += 'A' - '9';

			acp[j + 35].unicode = n;
 		}

		acp += EXT_COLUMNS;
	}

	return 1;
}

struct pex26 {
	signed			month		: 8;  /* 0 ... 11 */
	signed			day		: 8;  /* 0 ... 30 */
	signed			at1		: 16; /* min since 00:00 */
	signed			at2		: 16; /* min since 00:00 */
	signed			length		: 16; /* min */
	unsigned		x26_cni		: 16; /* see tables.c */
	unsigned		pty		: 8;
	signed			lto		: 8;  /* +- 1/4 hr */
	signed			row		: 8;  /* title 1 ... 23 */
	signed			column		: 8;  /* title 0 ... 39 */
	unsigned		caf		: 1;
	unsigned				: 15;
};

static void
dump_pex26(struct pex26 *pt, int n)
{
	int i;

	for (i = 0; i < n; i++, pt++)
		fprintf(stderr, "%2d: %02d-%02d %d:%02d (%d:%02d) +%d, "
			"cni=%04x pty=%02x lto=%d tit=%d:%d caf=%d\n",
			i, pt->month, pt->day,
			pt->at1 / 60, pt->at1 % 60,
			pt->at2 / 60, pt->at2 % 60,
			pt->length,
			pt->x26_cni, pt->pty, pt->lto,
			pt->row, pt->column,
			pt->caf);
}

#if 0

/*

type	pre	text		____      post       ____
                               /                         \
AT-1	+	zz.zz		+	%		<
AT-1	+	zz.zz-zz.zz	+	%		<
PTL	++	title		++	%%	::	<
AT-1	%	zz.zz		+	%		<
PW*)	%	hh
LTO	%	0zz		+	%		<
LTO	%	9zz		+	%		<
AT-2	%	zzzz		+	%		<
CNI*)	%	hhzzz		+	%		<
AD*)	%	zzzzzz		+	%		<
PTL	%%	title		++	%%	::	<
AT-2	:	zzzz		+	%		<
AD	:	zzzzzz		+	%		<
PW	:%	hh		+	%		<
PTY	:%	Fhh		+	%		<
AT-2	:%	zzzz		+	%		<
CNI	:%	hhzzz		+	%		<
AD	:%	zzzzzz		+	%		<

+  colour code
:  magenta
%  conceal

*) permitted when CNI, AD, PW combine

Note ETS 300 231 Table 4 is wrong: '%' = 0x18; ',' = '+' | '%' | '<'

*/

/* to be rewritten */

struct program_entry {
	int			start;
	int			stop;
	int			at2;
	int			ad;
	int			cni;
	int			pty;
	int			lto;
	uint16_t		title[200];
};

#define PMA_COLOUR /* 0x01, 0x02, 0x03, 0x04, 0x06, 0x07 */
#define PMA_MAGENTA 0x05
#define PMA_CONCEAL 0x18

#define IS_PMA_CTRL(c) (((c) >= 0x01 && (c) <= 0x07) || (c) == PMA_CONCEAL)

static int
bcd2time(int bcd)
{
	int sec = bcd & 15;
	int min = (bcd >> 8) & 15;

	if (sec > 9 || min > 9 || (bcd & 0x00FF) > 0x0059)
		return -1;
//#warning hour check

	return sec * 1 + min * 60
		+ ((bcd >> 4) & 15) * 10
		+ ((bcd >> 12) & 15) * 600;
}

static int
pdc_method_a(vbi_page *pg, vt_page *vtp, struct program_entry *pe)
{
	int row, column;
	int i;

//	memset(pe, -1, sizeof(*pe));

	i = 40;

	for (row = 1; row <= 23; row++) {
		for (column = 0; column <= 38;) {
			int ctrl1 = vbi_parity(vtp->data.lop.raw[row][column]);
			int ctrl2 = vbi_parity(vtp->data.lop.raw[row][column + 1]);

fprintf(stderr, "%d %d %02x %02x\n", row, column, ctrl1, ctrl2);

			if ((ctrl1 | ctrl2) < 0) {
				return 0; /* hamming error */
			} else if (!IS_PMA_CTRL(ctrl1)) {
				column++;
				continue;
			}

			if (ctrl1 == ctrl2 && ctrl1 != PMA_MAGENTA) {
fprintf(stderr, "PTL %d %d\n", row, column);
				/* title */
				column += 2;fprintf(stderr, "%d %d %02x %02x\n", row, column, ctrl1, ctrl2);


			} else {
				/* numeral */
				int digits, sep, value;
fprintf(stderr, "NUM %d %d\n", row, column);
				column += (ctrl1 == PMA_MAGENTA && ctrl2 == PMA_CONCEAL) ? 2 : 1;

				sep = 0;
				value = 0;

				for (digits = 0; column < 40; column++) {
					int c = vbi_parity(vtp->data.lop.raw[row][column]);

					if (IS_PMA_CTRL(c)) {
						break;
					} else if (c >= 0x30 && c <= 0x39) {
						value = value * 16 + c - 0x30;
						digits++;
					} else if (c >= 0x41 && c <= 0x46) {
						if (digits >= 3)
							goto invalid_pattern;
						value = value * 16 + c + (0x0A - 0x41);
						digits++;
					} else if (c == 0x2E) {
						if (digits != 2 && digits != 6)
							goto invalid_pattern;
						sep |= 1 << digits;
					} else if (c == 0x2D) {
						if (digits != 4)
							goto invalid_pattern;
						sep |= 1 << 4;
					} else
						goto invalid_pattern;
				}

				if (sep) {
					if (ctrl1 == PMA_MAGENTA)
						goto invalid_pattern;
					if (ctrl1 == PMA_CONCEAL && digits != 4)
						goto invalid_pattern;
				}

				switch (digits) {
					int start, stop;

				case 2:
					/* Actually ctrl1 only permitted when combined */
					if (ctrl1 != PMA_CONCEAL && ctrl2 != PMA_CONCEAL)
						goto invalid_pattern;
fprintf(stderr, "PW %02x\n", value);
					/* PW */
					break;

				case 3:
					if (ctrl1 == PMA_CONCEAL) {
						if (value >= 0x100 && value < 0x900)
							goto invalid_pattern;
fprintf(stderr, "LTO %03x\n", value);
						/* LTO */
					} else if (ctrl2 == PMA_CONCEAL) {
						if ((value -= 0xF00) < 0)
						goto invalid_pattern;
fprintf(stderr, "PTY %02x\n", value);
						/* PTY */
					} else
						goto invalid_pattern;

				case 4:
					start = bcd2time(value);

					if (start < 0)
						goto invalid_pattern;

					if (sep) {
						if (ctrl1 == PMA_MAGENTA)
							goto invalid_pattern;
fprintf(stderr, "AT-1 %04x\n", value);
						; /* AT-1 short */
					} else if (ctrl1 == PMA_MAGENTA || ctrl1 == PMA_CONCEAL) {
fprintf(stderr, "AT-2 %04x\n", value);
						; /* AT-2 */
					} else
						goto invalid_pattern;

					break;

				case 5:
					/* Actually ctrl1 only permitted when combined */
					if ((ctrl1 != PMA_CONCEAL && ctrl2 != PMA_CONCEAL)
					    || (value & 0x00F00) > 0x00900)
						goto invalid_pattern;

					/* CNI */
fprintf(stderr, "CNI %05x\n", value);
					break;

				case 6:
					/* Actually ctrl1 only permitted when combined */
					if (ctrl1 != PMA_CONCEAL && ctrl2 != PMA_CONCEAL)
						goto invalid_pattern;
					/* AD */
fprintf(stderr, "AD %06x\n", value);
					break;

				case 8:
					start = bcd2time(value >> 16); 
					stop = bcd2time(value);

					if ((start | stop) < 0
					    || ctrl1 == PMA_MAGENTA || ctrl1 == PMA_CONCEAL
					    || sep != ((1 << 2) + (1 << 4) + (1 << 6)))
						goto invalid_pattern;

					/* AT-1 long */
fprintf(stderr, "AT1 %08x\n", value);
					break;

				default:
				invalid_pattern:
					continue;
				}
			}
		}
	}

	return 0; /* invalid */
}

#endif

/*
 *  Zapzilla navigation
 */

static int
keyword(vbi_link *ld, uint8_t *p, int column,
	int pgno, int subno, int *back)
{
	uint8_t *s = p + column;
	int i, j, k, l;

	ld->type = VBI_LINK_NONE;
	ld->name[0] = 0;
	ld->url[0] = 0;
	ld->pgno = 0;
	ld->subno = VBI_ANY_SUBNO;
	*back = 0;

	if (isdigit(*s)) {
		for (i = 0; isdigit(s[i]); i++)
			ld->pgno = ld->pgno * 16 + (s[i] & 15);

		if (isdigit(s[-1]) || i > 3)
			return i;

		if (i == 3) {
			if (ld->pgno >= 0x100 && ld->pgno <= 0x899)
				ld->type = VBI_LINK_PAGE;

			return i;
		}

		if (s[i] != '/' && s[i] != ':')
			return i;

		s += i += 1;

		for (ld->subno = j = 0; isdigit(s[j]); j++)
			ld->subno = ld->subno * 16 + (s[j] & 15);

		if (j > 1 || subno != ld->pgno || ld->subno > 0x99)
			return i + j;

		if (ld->pgno == ld->subno)
			ld->subno = 0x01;
		else
			ld->subno = vbi_add_bcd(ld->pgno, 0x01);

		ld->type = VBI_LINK_SUBPAGE;
		ld->pgno = pgno;

		return i + j;
	} else if (!strncasecmp((char *) s, "https://", i = 8)) {
		ld->type = VBI_LINK_HTTP;
	} else if (!strncasecmp((char *) s, "http://", i = 7)) {
		ld->type = VBI_LINK_HTTP;
	} else if (!strncasecmp((char *) s, "www.", i = 4)) {
		ld->type = VBI_LINK_HTTP;
		strcpy((char *) ld->url, "http://");
	} else if (!strncasecmp((char *) s, "ftp://", i = 6)) {
		ld->type = VBI_LINK_FTP;
	} else if (*s == '@' || *s == 0xA7) {
		ld->type = VBI_LINK_EMAIL;
		strcpy((char *) ld->url, "mailto:");
		i = 1;
	} else if (!strncasecmp((char *) s, "(at)", i = 4)) {
		ld->type = VBI_LINK_EMAIL;
		strcpy((char *) ld->url, "mailto:");
	} else if (!strncasecmp((char *) s, "(a)", i = 3)) {
		ld->type = VBI_LINK_EMAIL;
		strcpy((char *) ld->url, "mailto:");
	} else
		return 1;

	for (j = k = l = 0;;) {
		// RFC 1738
		while (isalnum(s[i + j]) || strchr("%&/=?+-~:;@_", s[i + j])) {
			j++;
			l++;
		}

		if (s[i + j] == '.') {
			if (l < 1)
				return i;		
			l = 0;
			j++;
			k++;
		} else
			break;
	}

	if (k < 1 || l < 1) {
		ld->type = VBI_LINK_NONE;
		return i;
	}

	k = 0;

	if (ld->type == VBI_LINK_EMAIL) {
		for (; isalnum(s[k - 1]) || strchr("-~._", s[k - 1]); k--);

		if (k == 0) {
			ld->type = VBI_LINK_NONE;
			return i;
		}

		*back = k;

		strncat((char *) ld->url, (char *) s + k, -k);
		strcat((char *) ld->url, "@");
		strncat((char *) ld->url, (char *) s + i, j);
	} else
		strncat((char *) ld->url, (char *) s + k, i + j - k);

	return i + j;
}

static inline void
zap_links(vbi_page *pg, int row)
{
	unsigned char buffer[43]; /* One row, two spaces on the sides and NUL */
	vbi_link ld;
	vbi_char *acp;
	vbi_bool link[43];
	int i, j, n, b;

	acp = &pg->text[row * EXT_COLUMNS];

	for (i = j = 0; i < COLUMNS; i++) {
		if (acp[i].size == VBI_OVER_TOP || acp[i].size == VBI_OVER_BOTTOM)
			continue;
		buffer[j + 1] = (acp[i].unicode >= 0x20 && acp[i].unicode <= 0xFF) ?
			acp[i].unicode : 0x20;
		j++;
	}

	buffer[0] = ' '; 
	buffer[j + 1] = ' ';
	buffer[j + 2] = 0;

	for (i = 0; i < COLUMNS; i += n) { 
		n = keyword(&ld, buffer, i + 1,
			pg->pgno, pg->subno, &b);

		for (j = b; j < n; j++)
			link[i + j] = (ld.type != VBI_LINK_NONE);
	}

	for (i = j = 0; i < COLUMNS; i++) {
		acp[i].link = link[j];

		if (acp[i].size == VBI_OVER_TOP || acp[i].size == VBI_OVER_BOTTOM)
			continue;
		j++;
	}
}

/**
 * @param pg With vbi_fetch_vt_page() obtained vbi_page.
 * @param column Column 0 ... pg->columns - 1 of the character in question.
 * @param row Row 0 ... pg->rows - 1 of the character in question.
 * @param ld Place to store information about the link.
 * 
 * A vbi_page (in practice only Teletext pages) may contain hyperlinks
 * such as HTTP URLs, e-mail addresses or links to other pages. Characters
 * being part of a hyperlink have a set vbi_char->link flag, this function
 * returns a more verbose description of the link.
 */
void
vbi_resolve_link(vbi_page *pg, int column, int row, vbi_link *ld)
{
	unsigned char buffer[43];
	vbi_char *acp;
	int i, j, b;

	assert(column >= 0 && column < EXT_COLUMNS);

	ld->nuid = pg->nuid;

	acp = &pg->text[row * EXT_COLUMNS];

	if (row == (ROWS - 1) && acp[column].link) {
		i = pg->nav_index[column];

		ld->type = VBI_LINK_PAGE;
		ld->pgno = pg->nav_link[i].pgno;
		ld->subno = pg->nav_link[i].subno;

		return;
	}

	if (row < 1 || row > 23 || column >= COLUMNS || pg->pgno < 0x100) {
		ld->type = VBI_LINK_NONE;
		return;
	}

	for (i = j = b = 0; i < COLUMNS; i++) {
		if (acp[i].size == VBI_OVER_TOP || acp[i].size == VBI_OVER_BOTTOM)
			continue;
		if (i < column && !acp[i].link)
			j = b = -1;

		buffer[j + 1] = (acp[i].unicode >= 0x20 && acp[i].unicode <= 0xFF) ?
			acp[i].unicode : 0x20;

		if (b <= 0) {
			if (buffer[j + 1] == ')' && j > 2) {
				if (!strncasecmp((char *) buffer + j + 1 - 3, "(at", 3))
					b = j - 3;
				else if (!strncasecmp((char *) buffer + j + 1 - 2, "(a", 2))
					b = j - 2;
			} else if (buffer[j + 1] == '@' || buffer[j + 1] == 167)
				b = j;
		}

		j++;
	}

	buffer[0] = ' ';
	buffer[j + 1] = ' ';
	buffer[j + 2] = 0;

	keyword(ld, buffer, 1, pg->pgno, pg->subno, &i);

	if (ld->type == VBI_LINK_NONE)
		keyword(ld, buffer, b + 1, pg->pgno, pg->subno, &i);
}

/**
 * @param pg With vbi_fetch_vt_page() obtained vbi_page.
 * @param ld Place to store information about the link.
 * 
 * All Teletext pages have a built-in home link, by default
 * page 100, but can also be the magazine intro page or another
 * page selected by the editor.
 */
void
vbi_resolve_home(vbi_page *pg, vbi_link *ld)
{
	if (pg->pgno < 0x100) {
		ld->type = VBI_LINK_NONE;
		return;
	}

	ld->type = VBI_LINK_PAGE;
	ld->pgno = pg->nav_link[5].pgno;
	ld->subno = pg->nav_link[5].subno;
}

static inline void
ait_title(vbi_decoder *vbi, vt_page *vtp, ait_entry *ait, char *buf)
{
	struct vbi_font_descr *font[2];
	int i;

	character_set_designation(font, &vbi->vt.magazine[0].extension, vtp);

	for (i = 11; i >= 0; i--)
		if (ait->text[i] > 0x20)
			break;
	buf[i + 1] = 0;

	for (; i >= 0; i--) {
		unsigned int unicode = vbi_teletext_unicode(
			font[0]->G0, font[0]->subset,
			(ait->text[i] < 0x20) ?	0x20 : ait->text[i]);

		buf[i] = (unicode >= 0x20 && unicode <= 0xFF) ? unicode : 0x20;
	}
}

/**
 * @param vbi Initialized vbi decoding context.
 * @param pgno Page number, see vbi_pgno.
 * @param subno Subpage number.
 * @param buf Place to store the title, Latin-1 format, at least
 *   41 characters including the terminating zero.
 * 
 * Given a Teletext page number this function tries to deduce a
 * page title for bookmarks or other purposes, mainly from navigation
 * data. (XXX TODO: FLOF)
 * 
 * @return
 * @c TRUE if a title has been found.
 */
vbi_bool
vbi_page_title(vbi_decoder *vbi, int pgno, int subno, char *buf)
{
	vt_page *vtp;
	ait_entry *ait;
	int i, j;

	subno = subno;

	if (vbi->vt.top) {
		for (i = 0; i < 8; i++)
			if (vbi->vt.btt_link[i].type == 2) {
				vtp = vbi_cache_get(vbi,
						    vbi->vt.btt_link[i].pgno, 
						    vbi->vt.btt_link[i].subno, 0x3f7f);

				if (!vtp) {
					printv("p/t top ait page %x not cached\n", vbi->vt.btt_link[i].pgno);
					continue;
				} else if (vtp->function != PAGE_FUNCTION_AIT) {
					printv("p/t no ait page %x\n", vtp->pgno);
					continue;
				}

				for (ait = vtp->data.ait, j = 0; j < 46; ait++, j++)
					if (ait->page.pgno == pgno) {
						ait_title(vbi, vtp, ait, buf);
						return TRUE;
					}
			}
	} else {
		/* find a FLOF link and the corresponding label */
	}

	return FALSE;
}

/*
 *  Teletext page formatting
 */

static void
character_set_designation(struct vbi_font_descr **font,
			  vt_extension *ext, vt_page *vtp)
{
	int i;

#ifdef libzvbi_TTX_OVERRIDE_CHAR_SET

	font[0] = vbi_font_descriptors + libzvbi_TTX_OVERRIDE_CHAR_SET;
	font[1] = vbi_font_descriptors + libzvbi_TTX_OVERRIDE_CHAR_SET;

	fprintf(stderr, "override char set with %d\n",
		libzvbi_TTX_OVERRIDE_CHAR_SET);
#else

	font[0] = vbi_font_descriptors + 0;
	font[1] = vbi_font_descriptors + 0;

	for (i = 0; i < 2; i++) {
		int char_set = ext->char_set[i];

		if (VALID_CHARACTER_SET(char_set))
			font[i] = vbi_font_descriptors + char_set;

		char_set = (char_set & ~7) + vtp->national;

		if (VALID_CHARACTER_SET(char_set))
			font[i] = vbi_font_descriptors + char_set;
	}
#endif
}

static void
screen_color(vbi_page *pg, int flags, int color)
{ 
	pg->screen_color = color;

	if (color == VBI_TRANSPARENT_BLACK
	    || (flags & (C5_NEWSFLASH | C6_SUBTITLE)))
		pg->screen_opacity = VBI_TRANSPARENT_SPACE;
	else
		pg->screen_opacity = VBI_OPAQUE;
}

#define elements(array) (sizeof(array) / sizeof(array[0]))

static vt_triplet *
resolve_obj_address(vbi_decoder *vbi, object_type type,
	int pgno, object_address address, page_function function,
	int *remaining)
{
	int s1, packet, pointer;
	vt_page *vtp;
	vt_triplet *trip;
	int i;

	s1 = address & 15;
	packet = ((address >> 7) & 3);
	i = ((address >> 5) & 3) * 3 + type;

	printv("obj invocation, source page %03x/%04x, "
		"pointer packet %d triplet %d\n", pgno, s1, packet + 1, i);

	vtp = vbi_cache_get(vbi, pgno, s1, 0x000F);

	if (!vtp) {
		printv("... page not cached\n");
		return 0;
	}

	if (vtp->function == PAGE_FUNCTION_UNKNOWN) {
		if (!(vtp = vbi_convert_page(vbi, vtp, TRUE, function))) {
			printv("... no g/pop page or hamming error\n");
			return 0;
		}
	} else if (vtp->function == PAGE_FUNCTION_POP)
		vtp->function = function;
	else if (vtp->function != function) {
		printv("... source page wrong function %d, expected %d\n",
			vtp->function, function);
		return 0;
	}

	pointer = vtp->data.pop.pointer[packet * 24 + i * 2 + ((address >> 4) & 1)];

	printv("... triplet pointer %d\n", pointer);

	if (pointer > 506) {
		printv("... triplet pointer out of bounds (%d)\n", pointer);
		return 0;
	}

	if (DEBUG) {
		packet = (pointer / 13) + 3;

		if (packet <= 25)
			printv("... object start in packet %d, triplet %d (pointer %d)\n",
				packet, pointer % 13, pointer);
		else
			printv("... object start in packet 26/%d, triplet %d (pointer %d)\n",
				packet - 26, pointer % 13, pointer);	
	}

	trip = vtp->data.pop.triplet + pointer;
	*remaining = elements(vtp->data.pop.triplet) - (pointer+1);

	printv("... obj def: ad 0x%02x mo 0x%04x dat %d=0x%x\n",
		trip->address, trip->mode, trip->data, trip->data);

	address ^= trip->address << 7;
	address ^= trip->data;

	if (trip->mode != (type + 0x14) || (address & 0x1FF)) {
		printv("... no object definition\n");
		return 0;
	}

	return trip + 1;
}

/* FIXME: panels */

static vbi_bool
enhance(vbi_decoder *vbi, vt_magazine *mag, vt_extension *ext,
	vbi_page *pg, vt_page *vtp,
	object_type type, vt_triplet *p,
	int max_triplets,
	int inv_row, int inv_column,
	vbi_wst_level max_level, vbi_bool header_only,
	struct pex26 *ptable)
{
	vbi_char ac, mac, *acp;
	int active_column, active_row;
	int offset_column, offset_row;
	int row_color, next_row_color;
	struct vbi_font_descr *font;
	int invert;
	int drcs_s1[2];
	struct pex26 *pt, ptmp;
	int pdc_hr;

	/* XXX nested function not portable, to be removed */
	void
	flush(int column)
	{
		int row = inv_row + active_row;
		int i;

		if (row >= ROWS)
			return;

		if (type == OBJ_TYPE_PASSIVE && !mac.unicode) {
			active_column = column;
			return;
		}

		printv("flush [%04x%c,F%d%c,B%d%c,S%d%c,O%d%c,H%d%c] %d ... %d\n",
			ac.unicode, mac.unicode ? '*' : ' ',
			ac.foreground, mac.foreground ? '*' : ' ',
			ac.background, mac.background ? '*' : ' ',
			ac.size, mac.size ? '*' : ' ',
			ac.opacity, mac.opacity ? '*' : ' ',
			ac.flash, mac.flash ? '*' : ' ',
			active_column, column - 1);

		for (i = inv_column + active_column; i < inv_column + column;) {
			vbi_char c;

			if (i > 39)
				break;

			c = acp[i];

			if (mac.underline) {
				int u = ac.underline;

				if (!mac.unicode)
					ac.unicode = c.unicode;

				if (vbi_is_gfx(ac.unicode)) {
					if (u)
						ac.unicode &= ~0x20; /* separated */
					else
						ac.unicode |= 0x20; /* contiguous */
					mac.unicode = ~0;
					u = 0;
				}

				c.underline = u;
			}
			if (mac.foreground)
				c.foreground = (ac.foreground == VBI_TRANSPARENT_BLACK) ?
					row_color : ac.foreground;
			if (mac.background)
				c.background = (ac.background == VBI_TRANSPARENT_BLACK) ?
					row_color : ac.background;
			if (invert) {
				int t = c.foreground;

				c.foreground = c.background;
				c.background = t;
			}
			if (mac.opacity)
				c.opacity = ac.opacity;
			if (mac.flash)
				c.flash = ac.flash;
			if (mac.conceal)
				c.conceal = ac.conceal;
			if (mac.unicode) {
				c.unicode = ac.unicode;
				mac.unicode = 0;

				if (mac.size)
					c.size = ac.size;
				else if (c.size > VBI_DOUBLE_SIZE)
					c.size = VBI_NORMAL_SIZE;
			}

			acp[i] = c;

			if (type == OBJ_TYPE_PASSIVE)
				break;

			i++;

			if (type != OBJ_TYPE_PASSIVE
			    && type != OBJ_TYPE_ADAPTIVE) {
				int raw;

				raw = (row == 0 && i < 9) ?
					0x20 : vbi_unpar8 (vtp->data.lop.raw[row][i - 1]);

				/* set-after spacing attributes cancelling non-spacing */

				switch (raw) {
				case 0x00 ... 0x07:	/* alpha + foreground color */
				case 0x10 ... 0x17:	/* mosaic + foreground color */
					printv("... fg term %d %02x\n", i, raw);
					mac.foreground = 0;
					mac.conceal = 0;
					break;

				case 0x08:		/* flash */
					mac.flash = 0;
					break;

				case 0x0A:		/* end box */
				case 0x0B:		/* start box */
					if (i < COLUMNS && vbi_unpar8 (vtp->data.lop.raw[row][i]) == raw) {
						printv("... boxed term %d %02x\n", i, raw);
						mac.opacity = 0;
					}

					break;

				case 0x0D:		/* double height */
				case 0x0E:		/* double width */
				case 0x0F:		/* double size */
					printv("... size term %d %02x\n", i, raw);
					mac.size = 0;
					break;
				}

				if (i > 39)
					break;

				raw = (row == 0 && i < 8) ?
					0x20 : vbi_unpar8 (vtp->data.lop.raw[row][i]);

				/* set-at spacing attributes cancelling non-spacing */

				switch (raw) {
				case 0x09:		/* steady */
					mac.flash = 0;
					break;

				case 0x0C:		/* normal size */
					printv("... size term %d %02x\n", i, raw);
					mac.size = 0;
					break;

				case 0x18:		/* conceal */
					mac.conceal = 0;
					break;

					/*
					 *  Non-spacing underlined/separated display attribute
					 *  cannot be cancelled by a subsequent spacing attribute.
					 */

				case 0x1C:		/* black background */
				case 0x1D:		/* new background */
					printv("... bg term %d %02x\n", i, raw);
					mac.background = 0;
					break;
				}
			}
		}

		active_column = column;
	}

	/* XXX nested function not portable, to be removed */
	void
	flush_row(void)
	{
		if (type == OBJ_TYPE_PASSIVE || type == OBJ_TYPE_ADAPTIVE)
			flush(active_column + 1);
		else
			flush(COLUMNS);

		if (type != OBJ_TYPE_PASSIVE)
			memset(&mac, 0, sizeof(mac));
	}

	active_column = 0;
	active_row = 0;

	acp = &pg->text[(inv_row + 0) * EXT_COLUMNS];

	offset_column = 0;
	offset_row = 0;

	row_color =
	next_row_color = ext->def_row_color;

	drcs_s1[0] = 0; /* global */
	drcs_s1[1] = 0; /* normal */

	memset(&ac, 0, sizeof(ac));
	memset(&mac, 0, sizeof(mac));

	invert = 0;

	if (type == OBJ_TYPE_PASSIVE) {
		ac.foreground = VBI_WHITE;
		ac.background = VBI_BLACK;
		ac.opacity = pg->page_opacity[1];

		mac.foreground = ~0;
		mac.background = ~0;
		mac.opacity = ~0;
		mac.size = ~0;
		mac.underline = ~0;
		mac.conceal = ~0;
		mac.flash = ~0;
	}

	font = pg->font[0];

	if (ptable) {
		ptmp.month	= -1;
		ptmp.at1	= -1; /* n/a */
		ptmp.length	= 0;
		ptmp.x26_cni	= 0;
		ptmp.pty	= 0;
		ptmp.lto	= 0;

		pt = ptable - 1;
	} else
		pt = &ptmp;

	pdc_hr = 0;

	for (; max_triplets>0; p++, max_triplets--) {
		if (p->address >= COLUMNS) {
			/*
			 *  Row address triplets
			 */
			int s = p->data >> 5;
			int row = (p->address - COLUMNS) ? : (ROWS - 1);
			int column = 0;

			if (pdc_hr)
				return FALSE; /* invalid */

			switch (p->mode) {
			case 0x00:		/* full screen color */
				if (max_level >= VBI_WST_LEVEL_2p5
				    && s == 0 && type <= OBJ_TYPE_ACTIVE)
					screen_color(pg, vtp->flags, p->data & 0x1F);

				break;

			case 0x07:		/* address display row 0 */
				if (p->address != 0x3F)
					break; /* reserved, no position */

				row = 0;

				/* fall through */

			case 0x01:		/* full row color */
				row_color = next_row_color;

				if (s == 0) {
					row_color = p->data & 0x1F;
					next_row_color = ext->def_row_color;
				} else if (s == 3) {
					row_color =
					next_row_color = p->data & 0x1F;
				}

				goto set_active;

			case 0x02:		/* reserved */
			case 0x03:		/* reserved */
				break;

			case 0x04:		/* set active position */
				if (max_level >= VBI_WST_LEVEL_2p5) {
					if (p->data >= COLUMNS)
						break; /* reserved */

					column = p->data;
				}

				if (row > active_row)
					row_color = next_row_color;

			set_active:
				if (header_only && row > 0) {
					for (;max_triplets>1; p++, max_triplets--)
						if (p[1].address >= COLUMNS) {
							if (p[1].mode == 0x07)
								break;
							else if ((unsigned int) p[1].mode >= 0x1F)
								goto terminate;
						}
					break;
				}

				printv("enh set_active row %d col %d\n", row, column);

				if (row > active_row)
					flush_row();

				active_row = row;
				active_column = column;

				acp = &pg->text[(inv_row + active_row) * EXT_COLUMNS];

				break;

			case 0x05:		/* reserved */
			case 0x06:		/* reserved */
				break;

			case 0x08:		/* PDC data - Country of Origin and Programme Source */
				ptmp.x26_cni = p->address * 256 + p->data;
				break;

			case 0x09:		/* PDC data - Month and Day */
				ptmp.month = (p->address & 15) - 1;
				ptmp.day = (p->data >> 4) * 10 + (p->data & 15) - 1;
				break;

			case 0x0A:		/* PDC data - Cursor Row and Announced Starting Time Hours */
				if (!ptable) {
					break;
				} else if ((ptmp.month | ptmp.x26_cni) < 0) {
					return FALSE;
				} else if ((ptable - pt) > 22) {
					return FALSE;
				}

				*++pt = ptmp;

				/* fall through */

			case 0x0B:		/* PDC data - Cursor Row and Announced Finishing Time Hours */
				s = (p->data & 15) * 60;

				if (p->mode == 0x0A) {
					pt->at2 = ((p->data & 0x30) >> 4) * 600 + s;
					pt->length = 0;
					pt->row = row;
					pt->caf = !!(p->data & 0x40);
				} else {
					pt->length = ((p->data & 0x70) >> 4) * 600 + s;
				}

				pdc_hr = p->mode;

				break;

			case 0x0C:		/* PDC data - Cursor Row and Local Time Offset */
				ptmp.lto = (p->data & 0x40) ? ((~0x7F) | p->data) : p->data;
				break;

			case 0x0D:		/* PDC data - Series Identifier and Series Code */
				if (p->address == 0x30) {
					break;
				}
				pt->pty = 0x80 + p->data;
				break;

			case 0x0E:		/* reserved */
			case 0x0F:		/* reserved */
				break;

			case 0x10:		/* origin modifier */
				if (max_level < VBI_WST_LEVEL_2p5)
					break;

				if (p->data >= 72)
					break; /* invalid */

				offset_column = p->data;
				offset_row = p->address - COLUMNS;

				printv("enh origin modifier col %+d row %+d\n",
					offset_column, offset_row);

				break;

			case 0x11 ... 0x13:	/* object invocation */
			{
				int source = (p->address >> 3) & 3;
				object_type new_type = p->mode & 3;
				vt_triplet *trip;
				int remaining_max_triplets = 0;

				if (max_level < VBI_WST_LEVEL_2p5)
					break;

				printv("enh obj invocation source %d type %d\n", source, new_type);

				if (new_type <= type) { /* 13.2++ */
					printv("... priority violation\n");
					break;
				}

				if (source == 0) /* illegal */
					break;
				else if (source == 1) { /* local */
					int designation = (p->data >> 4) + ((p->address & 1) << 4);
					int triplet = p->data & 15;

					if (type != LOCAL_ENHANCEMENT_DATA || triplet > 12)
						break; /* invalid */

					printv("... local obj %d/%d\n", designation, triplet);

					if (!(vtp->enh_lines & 1)) {
						printv("... no packet %d\n", designation);
						return FALSE;
					}

					trip = vtp->data.enh_lop.enh + designation * 13 + triplet;
					remaining_max_triplets = elements(vtp->data.enh_lop.enh) - (designation* 13 + triplet);
				}
				else /* global / public */
				{
					page_function function;
					int pgno, i = 0;

					if (source == 3) {
						function = PAGE_FUNCTION_GPOP;
						pgno = vtp->data.lop.link[24].pgno;

						if (NO_PAGE(pgno)) {
							if (max_level < VBI_WST_LEVEL_3p5
							    || NO_PAGE(pgno = mag->pop_link[8].pgno))
								pgno = mag->pop_link[0].pgno;
						} else
							printv("... X/27/4 GPOP overrides MOT\n");
					} else {
						function = PAGE_FUNCTION_POP;
						pgno = vtp->data.lop.link[25].pgno;

						if (NO_PAGE(pgno)) {
							if ((i = mag->pop_lut[vtp->pgno & 0xFF]) == 0) {
								printv("... MOT pop_lut empty\n");
								return FALSE; /* has no link (yet) */
							}

							if (max_level < VBI_WST_LEVEL_3p5
							    || NO_PAGE(pgno = mag->pop_link[i + 8].pgno))
								pgno = mag->pop_link[i + 0].pgno;
						} else
							printv("... X/27/4 POP overrides MOT\n");
					}

					if (NO_PAGE(pgno)) {
						printv("... dead MOT link %d\n", i);
						return FALSE; /* has no link (yet) */
					}

					printv("... %s obj\n", (source == 3) ? "global" : "public");

					trip = resolve_obj_address(vbi, new_type, pgno,
						(p->address << 7) + p->data, function,
						&remaining_max_triplets);

					if (!trip)
						return FALSE;
				}

				row = inv_row + active_row;
				column = inv_column + active_column;

				if (!enhance(vbi, mag, ext, pg, vtp, new_type, trip,
					     remaining_max_triplets,
					     row + offset_row, column + offset_column,
					     max_level, header_only, NULL))
					return FALSE;

				printv("... object done\n");

				offset_row = 0;
				offset_column = 0;

				break;
			}

			case 0x14:		/* reserved */
				break;

			case 0x15 ... 0x17:	/* object definition */
				flush_row();
				printv("enh obj definition 0x%02x 0x%02x\n", p->mode, p->data);
				printv("enh terminated\n");
				goto swedish;

			case 0x18:		/* drcs mode */
				printv("enh DRCS mode 0x%02x\n", p->data);
				drcs_s1[p->data >> 6] = p->data & 15;
				break;

			case 0x19 ... 0x1E:	/* reserved */
				break;

			case 0x1F:		/* termination marker */
			default:
	                terminate:
				flush_row();
				printv("enh terminated %02x\n", p->mode);
				goto swedish;
			}
		} else {
			/*
			 *  Column address triplets
			 */
			int s = p->data >> 5;
			int column = p->address;
			int unicode;

			switch (p->mode) {
			case 0x00:		/* foreground color */
				if (max_level >= VBI_WST_LEVEL_2p5 && s == 0) {
					if (column > active_column)
						flush(column);

					ac.foreground = p->data & 0x1F;
					mac.foreground = ~0;

					printv("enh col %d foreground %d\n", active_column, ac.foreground);
				}

				break;

			case 0x01:		/* G1 block mosaic character */
				if (max_level >= VBI_WST_LEVEL_2p5) {
					if (column > active_column)
						flush(column);

					if (p->data & 0x20) {
						unicode = 0xEE00 + p->data; /* G1 contiguous */
						goto store;
					} else if (p->data >= 0x40) {
						unicode = vbi_teletext_unicode(
							font->G0, NO_SUBSET, p->data);
						goto store;
					}
				}

				break;

			case 0x0B:		/* G3 smooth mosaic or line drawing character */
				if (max_level < VBI_WST_LEVEL_2p5)
					break;

				/* fall through */

			case 0x02:		/* G3 smooth mosaic or line drawing character */
				if (p->data >= 0x20) {
					if (column > active_column)
						flush(column);

					unicode = 0xEF00 + p->data;
					goto store;
				}

				break;

			case 0x03:		/* background color */
				if (max_level >= VBI_WST_LEVEL_2p5 && s == 0) {
					if (column > active_column)
						flush(column);

					ac.background = p->data & 0x1F;
					mac.background = ~0;

					printv("enh col %d background %d\n", active_column, ac.background);
				}

				break;

			case 0x04:		/* reserved */
			case 0x05:		/* reserved */
				break;

			case 0x06:		/* PDC data - Cursor Column and Announced Starting */
						/* and Finishing Time Minutes */
				if (!ptable)
					break;

				s = (p->data >> 4) * 10 + (p->data & 15);

				if (pdc_hr == 0x0A) {
					pt->at2 += s;

					if (pt > ptable && pt[-1].length == 0) {
						pt[-1].length = pt->at2 - pt[-1].at2;

						if (pt->at2 < pt[-1].at2)
							pt[-1].length += 24 * 60;

						if (pt[-1].length >= 12 * 60) {
							/* bullshit */
							pt[-1] = pt[0];
							pt--;
						}
					}
				} else if (pdc_hr == 0x0B) {
					pt->length += s;

					if (pt->length >= 4 * 600) {
						pt->length -= 4 * 600;
					} else {
						if (pt->length < pt->at2)
							pt->length += 24 * 60;

						pt->length -= pt->at2;
					}
				} else {
					return FALSE;
				}

				pt->column = column;
				pdc_hr = 0;

				break;

			case 0x07:		/* additional flash functions */
				if (max_level >= VBI_WST_LEVEL_2p5) {
					if (column > active_column)
						flush(column);

					/*
					 *  Only one flash function (if any) implemented:
					 *  Mode 1 - Normal flash to background color
					 *  Rate 0 - Slow rate (1 Hz)
					 */
					ac.flash = !!(p->data & 3);
					mac.flash = ~0;

					printv("enh col %d flash 0x%02x\n", active_column, p->data);
				}

				break;

			case 0x08:		/* modified G0 and G2 character set designation */
				if (max_level >= VBI_WST_LEVEL_2p5) {
					if (column > active_column)
						flush(column);

					if (VALID_CHARACTER_SET(p->data))
						font = vbi_font_descriptors + p->data;
					else
						font = pg->font[0];

					printv("enh col %d modify character set %d\n", active_column, p->data);
				}

				break;

			case 0x09:		/* G0 character */
				if (max_level >= VBI_WST_LEVEL_2p5 && p->data >= 0x20) {
					if (column > active_column)
						flush(column);

					unicode = vbi_teletext_unicode(font->G0, NO_SUBSET, p->data);
					goto store;
				}

				break;

			case 0x0A:		/* reserved */
				break;

			case 0x0C:		/* display attributes */
				if (max_level < VBI_WST_LEVEL_2p5)
					break;

				if (column > active_column)
					flush(column);

				ac.size = ((p->data & 0x40) ? VBI_DOUBLE_WIDTH : 0)
					+ ((p->data & 1) ? VBI_DOUBLE_HEIGHT : 0);
				mac.size = ~0;

				if (p->data & 2) {
					if (vtp->flags & (C5_NEWSFLASH | C6_SUBTITLE))
						ac.opacity = VBI_SEMI_TRANSPARENT;
					else
						ac.opacity = VBI_TRANSPARENT_SPACE;
				} else
					ac.opacity = pg->page_opacity[1];
				mac.opacity = ~0;

				ac.conceal = !!(p->data & 4);
				mac.conceal = ~0;

				/* (p->data & 8) reserved */

				invert = p->data & 0x10;

				ac.underline = !!(p->data & 0x20);
				mac.underline = ~0;

				printv("enh col %d display attr 0x%02x\n", active_column, p->data);

				break;

			case 0x0D:		/* drcs character invocation */
			{
				int normal = p->data >> 6;
				int offset = p->data & 0x3F;
				vt_page *dvtp;
				page_function function;
				int pgno, page, i = 0;

				if (max_level < VBI_WST_LEVEL_2p5)
					break;

				if (offset >= 48)
					break; /* invalid */

				if (column > active_column)
					flush(column);

				page = normal * 16 + drcs_s1[normal];

				printv("enh col %d DRCS %d/0x%02x\n", active_column, page, p->data);

				/* if (!pg->drcs[page]) */ {
					if (!normal) {
						function = PAGE_FUNCTION_GDRCS;
						pgno = vtp->data.lop.link[26].pgno;

						if (NO_PAGE(pgno)) {
							if (max_level < VBI_WST_LEVEL_3p5
							    || NO_PAGE(pgno = mag->drcs_link[8]))
								pgno = mag->drcs_link[0];
						} else
							printv("... X/27/4 GDRCS overrides MOT\n");
					} else {
						function = PAGE_FUNCTION_DRCS;
						pgno = vtp->data.lop.link[25].pgno;

						if (NO_PAGE(pgno)) {
							if ((i = mag->drcs_lut[vtp->pgno & 0xFF]) == 0) {
								printv("... MOT drcs_lut empty\n");
								return FALSE; /* has no link (yet) */
							}

							if (max_level < VBI_WST_LEVEL_3p5
							    || NO_PAGE(pgno = mag->drcs_link[i + 8]))
								pgno = mag->drcs_link[i + 0];
						} else
							printv("... X/27/4 DRCS overrides MOT\n");
					}

					if (NO_PAGE(pgno)) {
						printv("... dead MOT link %d\n", i);
						return FALSE; /* has no link (yet) */
					}

					printv("... %s drcs from page %03x/%04x\n",
						normal ? "normal" : "global", pgno, drcs_s1[normal]);

					dvtp = vbi_cache_get(vbi,
						pgno, drcs_s1[normal], 0x000F);

					if (!dvtp) {
						printv("... page not cached\n");
						return FALSE;
					}

					if (dvtp->function == PAGE_FUNCTION_UNKNOWN) {
						if (!(dvtp = vbi_convert_page(vbi, dvtp, TRUE, function))) {
							printv("... no g/drcs page or hamming error\n");
							return FALSE;
						}
					} else if (dvtp->function == PAGE_FUNCTION_DRCS) {
						dvtp->function = function;
					} else if (dvtp->function != function) {
						printv("... source page wrong function %d, expected %d\n",
							dvtp->function, function);
						return FALSE;
					}

					if (dvtp->data.drcs.invalid & (1ULL << offset)) {
						printv("... invalid drcs, prob. tx error\n");
						return FALSE;
					}

					pg->drcs[page] = dvtp->data.drcs.bits[0];
				}

				unicode = 0xF000 + (page << 6) + offset;
				goto store;
			}

			case 0x0E:		/* font style */
			{
				int italic, bold, proportional;
				int col, row, count;
				vbi_char *acp;

				if (max_level < VBI_WST_LEVEL_3p5)
					break;

				row = inv_row + active_row;
				count = (p->data >> 4) + 1;
				acp = &pg->text[row * EXT_COLUMNS];

				proportional = (p->data >> 0) & 1;
				bold = (p->data >> 1) & 1;
				italic = (p->data >> 2) & 1;

				while (row < ROWS && count > 0) {
					for (col = inv_column + column; col < COLUMNS; col++) {
						acp[col].italic = italic;
		    				acp[col].bold = bold;
						acp[col].proportional = proportional;
					}

					acp += EXT_COLUMNS;
					row++;
					count--;
				}

				printv("enh col %d font style 0x%02x\n", active_column, p->data);

				break;
			}

			case 0x0F:		/* G2 character */
				if (p->data >= 0x20) {
					if (column > active_column)
						flush(column);

					unicode = vbi_teletext_unicode(font->G2, NO_SUBSET, p->data);
					goto store;
				}

				break;

			case 0x10 ... 0x1F:	/* characters including diacritical marks */
				if (p->data >= 0x20) {
					if (column > active_column)
						flush(column);

					unicode = vbi_teletext_composed_unicode(
						p->mode - 0x10, p->data);
			store:
					printv("enh row %d col %d print 0x%02x/0x%02x -> 0x%04x\n",
						active_row, active_column, p->mode, p->data,
						unicode);

					ac.unicode = unicode;
					mac.unicode = ~0;
				}

				break;
			}
		}
	}

swedish:

	if (ptable) {
		if (pt >= ptable && (pdc_hr || pt->length == 0))
			pt--; /* incomplete start or end tag */

		if (1)
			dump_pex26(ptable, pt - ptable + 1);
	}

	if (0) {
		acp = pg->text;

		for (active_row = 0; active_row < ROWS; active_row++) {
			printv("%2d: ", active_row);

			for (active_column = 0; active_column < COLUMNS; acp++, active_column++) {
				printv("%04x ", acp->unicode);
			}

			printv("\n");

			acp += EXT_COLUMNS - COLUMNS;
		}
	}

	return TRUE;
}

static void
post_enhance(vbi_page *pg, int display_rows)
{
	int last_row = MIN(display_rows, ROWS) - 2;
	vbi_char ac, *acp;
	int column, row;

	acp = pg->text;

	for (row = 0; row <= last_row; row++) {
		for (column = 0; column < COLUMNS; acp++, column++) {
			if (1)
				printv("%c", _vbi_to_ascii (acp->unicode));
			else
				printv("%04xF%dB%dS%dO%d ", acp->unicode,
				       acp->foreground, acp->background,
				       acp->size, acp->opacity);

			if (acp->opacity == VBI_TRANSPARENT_SPACE
			    || (acp->foreground == VBI_TRANSPARENT_BLACK
				&& acp->background == VBI_TRANSPARENT_BLACK)) {
				acp->opacity = VBI_TRANSPARENT_SPACE;
				acp->unicode = 0x0020;
			} else if (acp->background == VBI_TRANSPARENT_BLACK) {
				acp->opacity = VBI_SEMI_TRANSPARENT;
			}
			/* transparent foreground not implemented */

			switch (acp->size) {
			case VBI_NORMAL_SIZE:
				if (row < last_row
				    && (acp[EXT_COLUMNS].size == VBI_DOUBLE_HEIGHT2
					|| acp[EXT_COLUMNS].size == VBI_DOUBLE_SIZE2)) {
					acp[EXT_COLUMNS].unicode = 0x0020;
					acp[EXT_COLUMNS].size = VBI_NORMAL_SIZE;
				}

				if (column < 39
				    && (acp[1].size == VBI_OVER_TOP
					|| acp[1].size == VBI_OVER_BOTTOM)) {
					acp[1].unicode = 0x0020;
					acp[1].size = VBI_NORMAL_SIZE;
				}

				break;

			case VBI_DOUBLE_HEIGHT:
				if (row < last_row) {
					ac = acp[0];
					ac.size = VBI_DOUBLE_HEIGHT2;
					acp[EXT_COLUMNS] = ac;
				}
				break;

			case VBI_DOUBLE_SIZE:
				if (row < last_row) {
					ac = acp[0];
					ac.size = VBI_DOUBLE_SIZE2;
					acp[EXT_COLUMNS] = ac;
					ac.size = VBI_OVER_BOTTOM;
					acp[EXT_COLUMNS + 1] = ac;
				}

				/* fall through */

			case VBI_DOUBLE_WIDTH:
				if (column < 39) {
					ac = acp[0];
					ac.size = VBI_OVER_TOP;
					acp[1] = ac;
				}
				break;

			default:
				break;
			}
		}

		printv("\n");

		acp += EXT_COLUMNS - COLUMNS;
	}
}

static inline vbi_bool
default_object_invocation(vbi_decoder *vbi, vt_magazine *mag,
	vt_extension *ext, vbi_page *pg, vt_page *vtp,
	vbi_wst_level max_level, vbi_bool header_only)
{
	vt_pop_link *pop;
	int i, order;

	if (!(i = mag->pop_lut[vtp->pgno & 0xFF]))
		return FALSE; /* has no link (yet) */

	pop = mag->pop_link + i + 8;

	if (max_level < VBI_WST_LEVEL_3p5 || NO_PAGE(pop->pgno)) {
		pop = mag->pop_link + i;

		if (NO_PAGE(pop->pgno)) {
			printv("default object has dead MOT pop link %d\n", i);
			return FALSE;
		}
	}

	order = pop->default_obj[0].type > pop->default_obj[1].type;

	for (i = 0; i < 2; i++) {
		object_type type = pop->default_obj[i ^ order].type;
		vt_triplet *trip;
		int remaining_max_triplets;

		if (type == OBJ_TYPE_NONE)
			continue;

		printv("default object #%d invocation, type %d\n", i ^ order, type);

		trip = resolve_obj_address(vbi, type, pop->pgno,
			pop->default_obj[i ^ order].address, PAGE_FUNCTION_POP,
			&remaining_max_triplets);

		if (!trip)
			return FALSE;

		if (!enhance(vbi, mag, ext, pg, vtp, type, trip,
			     remaining_max_triplets, 0, 0, max_level,
			     header_only, NULL))
			return FALSE;
	}

	return TRUE;
}

/**
 * @internal
 * @param vbi Initialized vbi_decoder context.
 * @param pg Place to store the formatted page.
 * @param vtp Raw Teletext page. 
 * @param max_level Format the page at this Teletext implementation level.
 * @param display_rows Number of rows to format, between 1 ... 25.
 * @param navigation Analyse the page and add navigation links,
 *   including TOP and FLOF.
 * 
 * Format a page @a pg from a raw Teletext page @a vtp. This function is
 * used internally by libzvbi only.
 * 
 * @return
 * @c TRUE if the page could be formatted.
 */
int
vbi_format_vt_page(vbi_decoder *vbi,
		   vbi_page *pg, vt_page *vtp,
		   vbi_wst_level max_level,
		   int display_rows, vbi_bool navigation)
{
	char buf[16];
	vt_magazine *mag;
	vt_extension *ext;
	int column, row, i;

	if (vtp->function != PAGE_FUNCTION_LOP &&
	    vtp->function != PAGE_FUNCTION_TRIGGER)
		return FALSE;

	printv("\nFormatting page %03x/%04x pg=%p lev=%d rows=%d nav=%d\n",
	       vtp->pgno, vtp->subno, pg, max_level, display_rows, navigation);

	display_rows = SATURATE(display_rows, 1, ROWS);

	pg->vbi = vbi;

	pg->nuid = vbi->network.ev.network.nuid;

	pg->pgno = vtp->pgno;
	pg->subno = vtp->subno;

	pg->rows = display_rows;
	pg->columns = EXT_COLUMNS;

	pg->dirty.y0 = 0;
	pg->dirty.y1 = ROWS - 1;
	pg->dirty.roll = 0;

	mag = (max_level <= VBI_WST_LEVEL_1p5) ?
		vbi->vt.magazine : vbi->vt.magazine + (vtp->pgno >> 8);

	if (vtp->data.lop.ext)
		ext = &vtp->data.ext_lop.ext;
	else
		ext = &mag->extension;

	/* Character set designation */

	character_set_designation(pg->font, ext, vtp);

	/* Colors */

	screen_color(pg, vtp->flags, ext->def_screen_color);

	vbi_transp_colormap(vbi, pg->color_map, ext->color_map, 40);

	pg->drcs_clut = ext->drcs_clut;

	/* Opacity */

	pg->page_opacity[1] =
		(vtp->flags & (C5_NEWSFLASH | C6_SUBTITLE | C10_INHIBIT_DISPLAY)) ?
			VBI_TRANSPARENT_SPACE : VBI_OPAQUE;
	pg->boxed_opacity[1] =
		(vtp->flags & C10_INHIBIT_DISPLAY) ?
			VBI_TRANSPARENT_SPACE : VBI_SEMI_TRANSPARENT;

	if (vtp->flags & C7_SUPPRESS_HEADER) {
		pg->page_opacity[0] = VBI_TRANSPARENT_SPACE;
		pg->boxed_opacity[0] = VBI_TRANSPARENT_SPACE;
	} else {
		pg->page_opacity[0] = pg->page_opacity[1];
		pg->boxed_opacity[0] = pg->boxed_opacity[1];
	}

	/* DRCS */

	memset(pg->drcs, 0, sizeof(pg->drcs));

	/* Current page number in header */

	sprintf(buf, "\2%x.%02x\7", vtp->pgno, vtp->subno & 0xff);

	/* Level 1 formatting */

	i = 0;
	pg->double_height_lower = 0;

	for (row = 0; row < display_rows; row++) {
		struct vbi_font_descr *font;
		int mosaic_unicodes; /* 0xEE00 separate, 0xEE20 contiguous */
		int held_mosaic_unicode;
		int esc;
		vbi_bool hold, mosaic;
		vbi_bool double_height, wide_char;
		vbi_char ac, *acp = &pg->text[row * EXT_COLUMNS];

		held_mosaic_unicode = 0xEE20; /* G1 block mosaic, blank, contiguous */

		memset(&ac, 0, sizeof(ac));

		ac.unicode      = 0x0020;
		ac.foreground	= ext->foreground_clut + VBI_WHITE;
		ac.background	= ext->background_clut + VBI_BLACK;
		mosaic_unicodes	= 0xEE20; /* contiguous */
		ac.opacity	= pg->page_opacity[row > 0];
		font		= pg->font[0];
		esc		= 0;
		hold		= FALSE;
		mosaic		= FALSE;

		double_height	= FALSE;
		wide_char	= FALSE;

		acp[COLUMNS] = ac; /* artificial column 41 */

		for (column = 0; column < COLUMNS; ++column) {
			int raw;

			if (row == 0 && column < 8) {
				raw = buf[column];
				i++;
			} else if ((raw = vbi_unpar8 (vtp->data.lop.raw[0][i++])) < 0)
				raw = ' ';

			/* set-at spacing attributes */

			switch (raw) {
			case 0x09:		/* steady */
				ac.flash = FALSE;
				break;

			case 0x0C:		/* normal size */
				ac.size = VBI_NORMAL_SIZE;
				break;

			case 0x18:		/* conceal */
				ac.conceal = TRUE;
				break;

			case 0x19:		/* contiguous mosaics */
				mosaic_unicodes = 0xEE20;
				break;

			case 0x1A:		/* separated mosaics */
				mosaic_unicodes = 0xEE00;
				break;

			case 0x1C:		/* black background */
				ac.background = ext->background_clut + VBI_BLACK;
				break;

			case 0x1D:		/* new background */
				ac.background = ext->background_clut + (ac.foreground & 7);
				break;

			case 0x1E:		/* hold mosaic */
				hold = TRUE;
				break;
			}

			if (raw <= 0x1F)
				ac.unicode = (hold & mosaic) ? held_mosaic_unicode : 0x0020;
			else
				if (mosaic && (raw & 0x20)) {
					held_mosaic_unicode = mosaic_unicodes + raw - 0x20;
					ac.unicode = held_mosaic_unicode;
				} else
					ac.unicode = vbi_teletext_unicode(font->G0,
									  font->subset, raw);

			if (!wide_char) {
				acp[column] = ac;

				wide_char = /*!!*/(ac.size & VBI_DOUBLE_WIDTH);

				if (wide_char && column < (COLUMNS - 1)) {
					acp[column + 1] = ac;
					acp[column + 1].size = VBI_OVER_TOP;
				}
			} else
				wide_char = FALSE;

			/* set-after spacing attributes */

			switch (raw) {
			case 0x00 ... 0x07:	/* alpha + foreground color */
				ac.foreground = ext->foreground_clut + (raw & 7);
				ac.conceal = FALSE;
				mosaic = FALSE;
				break;

			case 0x08:		/* flash */
				ac.flash = TRUE;
				break;

			case 0x0A:		/* end box */
				if (column < (COLUMNS - 1)
				    && vbi_unpar8 (vtp->data.lop.raw[0][i]) == 0x0a)
					ac.opacity = pg->page_opacity[row > 0];
				break;

			case 0x0B:		/* start box */
				if (column < (COLUMNS - 1)
				    && vbi_unpar8 (vtp->data.lop.raw[0][i]) == 0x0b)
					ac.opacity = pg->boxed_opacity[row > 0];
				break;

			case 0x0D:		/* double height */
				if (row <= 0 || row >= 23)
					break;
				ac.size = VBI_DOUBLE_HEIGHT;
				double_height = TRUE;
				break;

			case 0x0E:		/* double width */
				printv("spacing col %d row %d double width\n", column, row);
				if (column < (COLUMNS - 1))
					ac.size = VBI_DOUBLE_WIDTH;
				break;

			case 0x0F:		/* double size */
				printv("spacing col %d row %d double size\n", column, row);
				if (column >= (COLUMNS - 1) || row <= 0 || row >= 23)
					break;
				ac.size = VBI_DOUBLE_SIZE;
				double_height = TRUE;

				break;

			case 0x10 ... 0x17:	/* mosaic + foreground color */
				ac.foreground = ext->foreground_clut + (raw & 7);
				ac.conceal = FALSE;
				mosaic = TRUE;
				break;

			case 0x1F:		/* release mosaic */
				hold = FALSE;
				break;

			case 0x1B:		/* ESC */
				font = pg->font[esc ^= 1];
				break;
			}
		}

		if (double_height) {
			for (column = 0; column < EXT_COLUMNS; column++) {
				ac = acp[column];

				switch (ac.size) {
				case VBI_DOUBLE_HEIGHT:
					ac.size = VBI_DOUBLE_HEIGHT2;
					acp[EXT_COLUMNS + column] = ac;
					break;
		
				case VBI_DOUBLE_SIZE:
					ac.size = VBI_DOUBLE_SIZE2;
					acp[EXT_COLUMNS + column] = ac;
					ac.size = VBI_OVER_BOTTOM;
					acp[EXT_COLUMNS + (++column)] = ac;
					break;

				default: /* NORMAL, DOUBLE_WIDTH, OVER_TOP */
					ac.size = VBI_NORMAL_SIZE;
					ac.unicode = 0x0020;
					acp[EXT_COLUMNS + column] = ac;
					break;
				}
			}

			i += COLUMNS;
			row++;

			pg->double_height_lower |= 1 << row;
		}
	}

	if (0) {
		if (row < ROWS) {
			vbi_char ac;

			memset(&ac, 0, sizeof(ac));

			ac.foreground	= ext->foreground_clut + VBI_WHITE;
			ac.background	= ext->background_clut + VBI_BLACK;
			ac.opacity	= pg->page_opacity[1];
			ac.unicode	= 0x0020;

			for (i = row * EXT_COLUMNS; i < ROWS * EXT_COLUMNS; i++)
				pg->text[i] = ac;
		}
	}

	/* Local enhancement data and objects */

	if (max_level >= VBI_WST_LEVEL_1p5 && display_rows > 0) {
		vbi_page page;
		vbi_bool success;

		memcpy(&page, pg, sizeof(page));

		if (!(vtp->flags & (C5_NEWSFLASH | C6_SUBTITLE))) {
			pg->boxed_opacity[0] = VBI_TRANSPARENT_SPACE;
			pg->boxed_opacity[1] = VBI_TRANSPARENT_SPACE;
		}

		if (vtp->enh_lines & 1) {
			printv("enhancement packets %08x\n", vtp->enh_lines);
			success = enhance(vbi, mag, ext, pg, vtp, LOCAL_ENHANCEMENT_DATA,
				vtp->data.enh_lop.enh, elements(vtp->data.enh_lop.enh),
				0, 0, max_level, display_rows == 1, NULL);
							   // here --^
		} else
			success = default_object_invocation(vbi, mag, ext, pg, vtp,
							    max_level, display_rows == 1);

		if (success) {
			if (max_level >= VBI_WST_LEVEL_2p5)
				post_enhance(pg, display_rows);
		} else
			memcpy(pg, &page, sizeof(*pg));
	}

	/* Navigation */

	if (navigation) {
		pg->nav_link[5].pgno = vbi->vt.initial_page.pgno;
		pg->nav_link[5].subno = vbi->vt.initial_page.subno;

		for (row = 1; row < MIN(ROWS - 1, display_rows); row++)
			zap_links(pg, row);

		if (display_rows >= ROWS) {
			if (vtp->data.lop.flof) {
				if (vtp->data.lop.link[5].pgno >= 0x100
				    && vtp->data.lop.link[5].pgno <= 0x899
				    && (vtp->data.lop.link[5].pgno & 0xFF) != 0xFF) {
					pg->nav_link[5].pgno = vtp->data.lop.link[5].pgno;
					pg->nav_link[5].subno = vtp->data.lop.link[5].subno;
				}

				if (vtp->lop_lines & (1 << 24))
					flof_links(pg, vtp);
				else
					flof_navigation_bar(pg, vtp);
			} else if (vbi->vt.top)
				top_navigation_bar(vbi, pg, vtp);

//			pdc_method_a(pg, vtp, NULL);
		}
	}

	if (0) {
		vbi_char *acp;

		for (row = ROWS - 1, acp = pg->text + EXT_COLUMNS * row; row < ROWS; row++) {
			fprintf(stderr, "%2d: ", row);

			for (column = 0; column < COLUMNS; acp++, column++) {
				fprintf(stderr, "%04x ", acp->unicode);
			}

			fprintf(stderr, "\n");

			acp += EXT_COLUMNS - COLUMNS;
		}
	}

	return TRUE;
}

/**
 * @param vbi Initialized vbi_decoder context.
 * @param pg Place to store the formatted page.
 * @param pgno Page number of the page to fetch, see vbi_pgno.
 * @param subno Subpage number to fetch (optional @c VBI_ANY_SUBNO).
 * @param max_level Format the page at this Teletext implementation level.
 * @param display_rows Number of rows to format, between 1 ... 25.
 * @param navigation Analyse the page and add navigation links,
 *   including TOP and FLOF.
 * 
 * Fetches a Teletext page designated by @a pgno and @a subno from the
 * cache, formats and stores it in @a pg. Formatting is limited to row
 * 0 ... @a display_rows - 1 inclusive. The really useful values
 * are 1 (format header only) or 25 (everything). Likewise
 * @a navigation can be used to save unnecessary formatting time.
 * 
 * Although safe to do, this function is not supposed to be called from
 * an event handler since rendering may block decoding for extended
 * periods of time.
 *
 * @return
 * @c FALSE if the page is not cached or could not be formatted
 * for other reasons, for instance is a data page not intended for
 * display. Level 2.5/3.5 pages which could not be formatted e. g.
 * due to referencing data pages not in cache are formatted at a
 * lower level.
 */
vbi_bool
vbi_fetch_vt_page(vbi_decoder *vbi, vbi_page *pg,
		  vbi_pgno pgno, vbi_subno subno,
		  vbi_wst_level max_level,
		  int display_rows, vbi_bool navigation)
{
	vt_page *vtp;
	int row;

	switch (pgno) {
	case 0x900:
		if (subno == VBI_ANY_SUBNO)
			subno = 0;

		if (!vbi->vt.top || !top_index(vbi, pg, subno))
			return FALSE;

		pg->nuid = vbi->network.ev.network.nuid;
		pg->pgno = 0x900;
		pg->subno = subno;

		post_enhance(pg, ROWS);

		for (row = 1; row < ROWS; row++)
			zap_links(pg, row);

		return TRUE;

	default:
		vtp = vbi_cache_get(vbi, pgno, subno, -1);

		if (!vtp)
			return FALSE;

		return vbi_format_vt_page(vbi, pg, vtp,
					  max_level, display_rows, navigation);
	}
}


syntax highlighted by Code2HTML, v. 0.9.1