/*
 * yaepg.c: A plugin for the Video Disk Recorder
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: yaepg.c,v 1.15 2004/11/08 01:41:20 bball Exp $
 */

#include "yaepg.h"
#include <vdr/device.h>

#define EPGWinHandle		areas[0]
#define InfoWinHandle		areas[1]

static const tColor blue		= (tColor) 0xFF004180;
static const tColor tint		= (tColor) 0xFF002850;
static const tColor highlight		= (tColor) 0xFF505050;
static const tColor lightgrey		= (tColor) 0xFFAAAAAA;

#define fontYaepg			fontSml
#define fontChanBox			fontYaepg
#define fontTimeBox			fontYaepg
#define fontEventBox			fontYaepg
#define fontInfoBox			fontYaepg
#define fontDateBox			fontYaepg
#define fontTitleBox			fontYaepg

/*
 * Lots of hard-coding here based on the font size.  Should be made more dynamic
 * based on font
 */
#define TBOX_MAX_LINES			16
#define TBOX_DEF_XBORDER		8
#define TBOX_DEF_YBORDER		0
#define TBOX_DEF_XALIGN			TBOX_TXT_CENTER
#define TBOX_DEF_YSPACING		0

#define TBOX_TXT_HEIGHT			20
#define TBOX_TXT_LEFT			0
#define TBOX_TXT_CENTER			1
#define TBOX_TXT_RIGHT			2

#define ARROW_LEFT			1
#define ARROW_RIGHT			2

/* Order of channels (UP or DOWN) */
#define CHANNEL_ORDER_UP      0
#define CHANNEL_ORDER_DOWN      1

/* Manner in which channel is changed while in YAEPG */
#define CHANNEL_CHANGE_MANUAL      0
#define CHANNEL_CHANGE_MANUAL_INEPG   1
#define CHANNEL_CHANGE_AUTO_INEPG   2

/*
 * PAL/NTSC Output
 */
#define TV_NTSC				0
#define TV_PAL				1
#define TV_PAL_W			720
#define TV_PAL_H			576
#define TV_NTSC_W			720
#define TV_NTSC_H			480
#define CHANNELS_PAL			6
#define CHANNELS_NTSC			5

#define TIME_24HR			0
#define TIME_12HR			1
#define FMT_12HR(_hr)			((_hr) % 12 == 0 ? 12 : (_hr) % 12)
#define FMT_AMPM(_hr)			((_hr) >= 12 ? "PM" : "AM")

int iTvFormat				= TV_NTSC;
int iChannelCount			= CHANNELS_NTSC;
int iHideMenuEntry			= false;
int iChannelChange                      = CHANNEL_CHANGE_MANUAL;
int iTimeFormat				= TIME_24HR;
int iChannelOrder                       = CHANNEL_ORDER_DOWN;
static const char *TV_FORMATS[2]	= { "NTSC", "PAL" };
static const char *TIME_FORMATS[2]	= { "24", "12" };
static const char *CH_ORDER_FORMATS[2]   = { "UP", "DOWN" };
static const char *CH_CHANGE_MODES[3]   = { "MANUAL", "MANUAL IN EPG", "AUTO IN EPG" };

int iOutputResW				= TV_NTSC_W;  
int iOutputResH				= TV_NTSC_H;  

int iRecBoxWidth			= TV_NTSC_W/2;
int iRecBoxHeight			= TV_NTSC_H/4;

class cTextBox {
private:
	int		width;
	int		height;
	int		xBorder;
	int		yBorder;
	int		xCoord;
	int		yCoord;

	tColor	fgColor;
	tColor	bgColor;
	tColor	hlColor;
	bool		hl;

	const cFont	*font;
	eDvbFont	fontC;
	int		xAlign;
	int		ySpacing;

	int		numLines;
	int		startNum;
	char		*text[TBOX_MAX_LINES];
	char		*abrevtext;

	cBitmap		*bitmap;
	cBitmap		*destBitmap;

protected:
	bool		initialized;
	int		arrow;

public:
	cTextBox(void);
	cTextBox(int, int, tColor, tColor, eDvbFont);
	~cTextBox();
	void	Init(int, int, tColor, tColor, eDvbFont);
	int	Height(void);
	int	Width(void);
	int	SetText(int, ...);
	void	AddLine(char *);
	int	SetXAlign(int);
	int	Draw(int, int, cBitmap *);
	int	Draw(void);
	int	SetDest(int, int, cBitmap *);
	int	SetHLColor(tColor);
	int	SetHighLight(bool);
	void	SetYSpacing(int yspc) { ySpacing = yspc; }
	void	DrawLine(int, int, int, int, tColor);
};

cTextBox::cTextBox(void)
{
}

cTextBox::cTextBox(int w, int h, tColor fg, tColor bg, eDvbFont f)
{
	width = w;
	height = h;
	xBorder = TBOX_DEF_XBORDER;
	yBorder = TBOX_DEF_YBORDER;
	xCoord = 0;
	yCoord = 0;

	fgColor = fg;
	bgColor = bg;
	hlColor = bg;
	hl = false;

	font = cFont::GetFont(f);
	fontC = f;
	xAlign = TBOX_DEF_XALIGN;
	ySpacing = TBOX_DEF_YSPACING;
	
	numLines = 0;
	startNum = 0;
	bzero(text, (sizeof(char *) * TBOX_MAX_LINES));
	abrevtext = NULL;

	bitmap = new cBitmap(width, height, 2);
	destBitmap = NULL;

	initialized = true;
	arrow = 0;
}

cTextBox::~cTextBox()
{
	int i = 0;

	for (i = 0; i < numLines; i++) {
		if (text[i] != NULL) {
			free(text[i]);
		}
	}

	if (bitmap) {
		delete bitmap;
	}
}

void
cTextBox::Init(int w, int h, tColor fg, tColor bg, eDvbFont f)
{
	width = w;
	height = h;
	xBorder = TBOX_DEF_XBORDER;
	yBorder = TBOX_DEF_YBORDER;
	xCoord = 0;
	yCoord = 0;

	fgColor = fg;
	bgColor = bg;
	hlColor = bg;
	hl = false;

	font = cFont::GetFont(f);
	fontC = f;
	xAlign = TBOX_DEF_XALIGN;
	ySpacing = TBOX_DEF_YSPACING;
	
	numLines = 0;
	bzero(text, (sizeof(char *) * TBOX_MAX_LINES));
	abrevtext = NULL;

	bitmap = new cBitmap(width, height, 2);
	destBitmap = NULL;

	initialized = true;
	arrow = 0;
}

int
cTextBox::SetText(int num, ...)
{
	va_list ap;
	int i;

	if (num > TBOX_MAX_LINES) {
		return (-1);
	}

	/*
	 * Free the old text.
	 */
	for (i = 0; i < numLines; i++) {
		if (text[i] != NULL) {
			free(text[i]);
		}
	}

	/*
	 * Copy the new text.
	 */
	va_start(ap, num);
	for (numLines = 0; numLines < num; numLines++) {
		char *s = va_arg(ap, char *);
		if (s == NULL) {
			s = "";
		}
		text[numLines] = strdup(s);
		if (text[numLines] == NULL) {
			va_end(ap);
			fprintf(stderr, "Out of memory!\n");
			return (-1);
		}
	}
	va_end(ap);

	return (0);
}

void
cTextBox::AddLine(char *s)
{
	if (numLines == TBOX_MAX_LINES) {
		return;
	}

	text[numLines] = strdup(s);
	numLines++;
	return;
}

int
cTextBox::Draw(int bx, int by, cBitmap *destBmp)
{
	int i, x, y, textHeight, textWidth;

	/*
	 * First time this textbox has been draw.  Stash away the bitmap its
	 * being drawn on and its coordinates so it can be redraw later without
	 * having to know this information.
	 */
	if (destBitmap == NULL) {
		xCoord = bx;
		yCoord = by;
		destBitmap = destBmp;
	}

	bitmap->DrawRectangle(0, 0, width, height, (hl ? hlColor : bgColor));

	textHeight = (TBOX_TXT_HEIGHT * numLines) + ((numLines - 1) * ySpacing);
	y = (height - textHeight) / 2;
	if (y < yBorder) {
		y = yBorder;
	}

	for (i = 0; i < numLines; i++) {
		switch (xAlign) {
		case TBOX_TXT_CENTER:
			textWidth = font->Width(text[i]);
			x = (width - textWidth) / 2;
			if (x < xBorder) {
				x = xBorder;
			}
			break;

		case TBOX_TXT_LEFT:
			x = xBorder;
			break;

		case TBOX_TXT_RIGHT:
			textWidth = font->Width(text[i]);
			x = (width - xBorder) - textWidth;
			if (x < xBorder) {
				x = xBorder;
			}
			break;
		default:
			fprintf(stderr, "Unknown text alignment!\n");
			return (-1);
		}

		bitmap->DrawText(x, y, text[i], fgColor,
				(hl ? hlColor : bgColor), font);
		y += TBOX_TXT_HEIGHT + ySpacing;
	}

	/*
	 * Fill in the right hand border of the box and the bottom border.
	 */
	bitmap->DrawRectangle((width - xBorder), 0, width, height, (hl ? hlColor : bgColor));
	bitmap->DrawRectangle(0, (height - yBorder), width, height, (hl ? hlColor : bgColor));

	/*
	 * Add the arrows.
	 */
	if (arrow & ARROW_LEFT) {
		DrawLine(0, height/2, 8, 0, lightgrey);
		DrawLine(0, height/2, 8, height, lightgrey);
		DrawLine(1, height/2, 9, 0, lightgrey);
		DrawLine(1, height/2, 9, height, lightgrey);
	}
	if (arrow & ARROW_RIGHT) {
		DrawLine(width-1, height/2, width-9, 0, lightgrey);
		DrawLine(width-1, height/2, width-9, height, lightgrey);
		DrawLine(width-2, height/2, width-10, 0, lightgrey);
		DrawLine(width-2, height/2, width-10, height, lightgrey);
	}

	destBmp->DrawBitmap(bx, by, *bitmap);

	return (0);
}

int
cTextBox::Draw(void)
{
	if (destBitmap != NULL) {
		return Draw(xCoord, yCoord, destBitmap);
	}

	fprintf(stderr, "Trying to draw a textbox that doesn't have dest!\n");
	return -1;
}

int
cTextBox::SetXAlign(int xa)
{
	xAlign = xa;

	return (0);
}

int
cTextBox::Width(void)
{
	return (width);
}

int
cTextBox::Height(void)
{
	return (height);
}

int
cTextBox::SetHLColor(tColor hl)
{
	hlColor = hl;
	return (0);
}

int
cTextBox::SetHighLight(bool h)
{
	hl = h;
	return (0);
}

/*
 * This line drawing code was lifted from someone's webpage :-)
 */
void
cTextBox::DrawLine(int x0, int y0, int x1, int y1, tColor color)
{
	int i;
	int steep = 1;
	int sx, sy;
	int dx, dy;
	int e;

	int tmpswap;
#define SWAP(a,b) tmpswap = a; a = b; b = tmpswap;

	dx = abs(x1 - x0);
	sx = ((x1 - x0) > 0) ? 1 : -1;
	dy = abs(y1 - y0);
	sy = ((y1 - y0) > 0) ? 1 : -1;
	if (dy > dx) {
		steep = 0;
		SWAP(x0, y0);
		SWAP(dx, dy);
		SWAP(sx, sy);
	}
	e = (dy << 1) - dx;
	for (i = 0; i < dx; i++) {
		if (steep) {
			bitmap->DrawPixel(y0, x0, color);
		}
		else {
			bitmap->DrawPixel(y0, x0, color);
		}
		while (e >= 0) {
			y0 += sy;
			e -= (dx << 1);
		}
		x0 += sx;
		e += (dy << 1);
	}
}

#define CHBOX_WIDTH				68
#define CHBOX_HEIGHT				36

class cChanBox : public cTextBox {
private:
	cChannel chan;

public:
	cChanBox(void);
	cChanBox(cChannel *);
	~cChanBox();
	int SetChan(cChannel *);
	tChannelID GetChanID(void);
	int GetChanNum(void);
};

cChanBox::cChanBox(void)
	: cTextBox(CHBOX_WIDTH, CHBOX_HEIGHT, lightgrey, tint, fontChanBox)
{
}

cChanBox::cChanBox(cChannel *c)
	: cTextBox(CHBOX_WIDTH, CHBOX_HEIGHT, lightgrey, tint, fontChanBox)
{
	char numStr[8];

	chan = *c;
	SetXAlign(TBOX_TXT_CENTER);
	SetYSpacing(-3);
	snprintf(numStr, 8, "%d", c->Number());
	SetText(2, c->Name(), numStr);
}

cChanBox::~cChanBox()
{
}

int
cChanBox::SetChan(cChannel *c)
{
	char numStr[8];

	chan = *c;
	SetXAlign(TBOX_TXT_CENTER);
	snprintf(numStr, 8, "%d", c->Number());
	SetText(2, c->Name(), numStr);

	return (0);
}

tChannelID
cChanBox::GetChanID(void)
{
	return (chan.GetChannelID());
}

int
cChanBox::GetChanNum(void)
{
	return (chan.Number());
}

#define TMBOX_WIDTH			144
#define TMBOX_HEIGHT			18

class cTimeBox : public cTextBox {
private:
	int hour;
	int minute;

public:
	struct tm tmTime;

	cTimeBox(void);
	cTimeBox(struct tm *, int);
	~cTimeBox();
	int SetTime(struct tm *, int);
};

cTimeBox::cTimeBox(void)
	: cTextBox(TMBOX_WIDTH, TMBOX_HEIGHT, lightgrey, tint, fontTimeBox)
{
	SetXAlign(TBOX_TXT_LEFT);
}

cTimeBox::cTimeBox(struct tm *currTime, int offset)
	: cTextBox(TMBOX_WIDTH, TMBOX_HEIGHT, lightgrey, tint, fontTimeBox)
{
	char timeStr[16];
	time_t t1 = mktime(currTime) + (offset *60);

	(void) localtime_r(&t1, &tmTime);
	if (tmTime.tm_min >= 30) {
		tmTime.tm_min = 30;
	}
	else {
		tmTime.tm_min = 0;
	}
	tmTime.tm_sec = 0;
	hour = tmTime.tm_hour;
	minute = tmTime.tm_min;
		
	SetXAlign(TBOX_TXT_LEFT);
	/* Print time using the configured time format */
	if (iTimeFormat == TIME_24HR) {
		snprintf(timeStr, 16, "%02d:%02d", hour, minute);
	}
	else {
		snprintf(timeStr, 16, "%d:%02d %s", FMT_12HR(hour), minute,
				FMT_AMPM(hour));
	}
	SetText(1, timeStr);
}

cTimeBox::~cTimeBox()
{
}

int
cTimeBox::SetTime(struct tm *currTime, int offset)
{
	char timeStr[8];
	time_t t1 = mktime(currTime) + (offset *60);

	if (initialized == false) {
		Init(TMBOX_WIDTH, TMBOX_HEIGHT, lightgrey, tint, fontTimeBox);
	}

	(void) localtime_r(&t1, &tmTime);
	if (tmTime.tm_min > 30) {
		tmTime.tm_min = 30;
	}
	else {
		tmTime.tm_min = 0;
	}
	tmTime.tm_sec = 0;
	hour = tmTime.tm_hour;
	minute = tmTime.tm_min;
		
	SetXAlign(TBOX_TXT_LEFT);
	snprintf(timeStr, 8, "%02d:%02d", hour, minute);
	SetText(1, timeStr);

	return (0);
}

#define EVBOX_HEIGHT			CHBOX_HEIGHT

class cEventBox : public cTextBox {
private:
	const cEvent *event;
	int xOff;

public:
	cEventBox(void);
	cEventBox(const cEvent *, time_t);
	~cEventBox();
	const cEvent *GetEvent(void) { return (event); }
	int XOff(void) { return (xOff); }
};

cEventBox::cEventBox(void)
{
}

cEventBox::cEventBox(const cEvent *e, time_t t)
{
	time_t tLen;
	int w, arrowFlag = 0;

	event = e;

	/*
	 * Figure out how long the event is.  The length is adjusted to ensure
	 * that it fits on the screen.
	 */
	tLen = e->Duration();

	if (e->StartTime() < t) {
		arrowFlag |= ARROW_LEFT;
		tLen -= (t - e->StartTime());
	}
	if ((e->StartTime() + e->Duration()) > (t + 7200)) {
		arrowFlag |= ARROW_RIGHT;
		tLen -= ((e->StartTime() + e->Duration()) - (t + 7200));
	}

	/*
	 * We use 5 minute granularity for an event's width.  We subtract two
	 * pixels for the border.
	 */
	if (tLen >= 300) {
		w = (tLen / 300) * 24 -2;
	}
	else {
		w = 4;
	}

	/*
	 * Calculate the x offset based on the event's start time.  We move it
	 * over one for the left side of the border.  The right border is taken
	 * care of above.
	 */
	xOff = (((e->StartTime() - t) + iOutputResH / 2) / 300) * 24;
	if (xOff <= 0) {
		xOff = 1;
	}
	else {
		xOff++;
	}

	/*
	 * Now that we have the width, initialize the rest of the fields
	 */
	Init(w, EVBOX_HEIGHT, lightgrey, tint, fontEventBox);
	arrow = arrowFlag;
	SetHLColor(highlight);
	SetText(1, e->Title());
}

cEventBox::~cEventBox()
{
}

struct sEventRow {
	int		rowLen;
	cEventBox	*event[8];
};

class cNoInfoEvent : public cEvent {
private:
	time_t			startTime;

public:
	cNoInfoEvent(time_t);
	~cNoInfoEvent();
};

cNoInfoEvent::cNoInfoEvent(time_t startTime)
	: cEvent(0)
{
	SetStartTime(startTime);
	SetDuration(9000);
	SetTitle(tr("No Info"));
	SetDescription(tr("No Info"));
}

cNoInfoEvent::~cNoInfoEvent()
{
}

#define INFBOX_WIDTH			314
#define INFBOX_HEIGHT			iOutputResH / 2 - 104 // TODO

class cInfoBox : public cTextBox {
private:
	const cEvent		*event;
	void FormatText(void);

public:
	cInfoBox(void);
	cInfoBox(const cEvent *);
	~cInfoBox();
};

cInfoBox::cInfoBox(void)
{
}

cInfoBox::cInfoBox(const cEvent *e)
	: cTextBox(INFBOX_WIDTH, INFBOX_HEIGHT, lightgrey, tint, fontInfoBox)
{
	if (e == NULL) {
		fprintf(stderr, "e == NULL!\n");
		return;
	}
	event = e;
	SetYSpacing(2);
	FormatText();
}

cInfoBox::~cInfoBox()
{
}

#define		INFBOX_TXT_WIDTH		34

void
cInfoBox::FormatText(void)
{
	char *desc, *token, *sp, line[INFBOX_TXT_WIDTH];

	if (event->Description() != NULL) {
		desc = strdup(event->Description());
		if (desc == NULL) {
			fprintf(stderr, "strdup() returned NULL!\n");
			return;
		}
	}
	else {
		desc = strdup(tr("No Info"));
	}

	bzero(line, INFBOX_TXT_WIDTH);
	for (token = strtok_r(desc, " ", &sp);
			token;
			token = strtok_r(NULL, " ", &sp)) {
		if (strlen(line) + strlen(token) + 1 > INFBOX_TXT_WIDTH) {
			AddLine(line);
			bzero(line, INFBOX_TXT_WIDTH);
		}
		strncat(line, " ", INFBOX_TXT_WIDTH);
		strncat(line, token, INFBOX_TXT_WIDTH);
	}

	/*
	 * If there is data in line, make sure it gets added.
	 */
	if (strlen(line) != 0) {
		AddLine(line);
	}

	free(desc);
}

#define DTBOX_WIDTH			314
#define DTBOX_HEIGHT			20

char *numToDay[7] = {
	"Sun",
	"Mon",
	"Tue",
	"Wed",
	"Thr",
	"Fri",
	"Sat"
};

class cDateBox : public cTextBox {
private:
	struct tm	dateTm;
	time_t		date;

public:
	cDateBox(void);
	cDateBox(time_t);
	~cDateBox();
};

cDateBox::cDateBox(void)
{
}

cDateBox::cDateBox(time_t time)
	:cTextBox(DTBOX_WIDTH, DTBOX_HEIGHT, lightgrey, tint, fontDateBox)
{
	char dateStr[30];
	date = time;
	(void) localtime_r(&date, &dateTm);

	SetXAlign(TBOX_TXT_CENTER);
	if (iTimeFormat == TIME_24HR) {
		snprintf(dateStr, 30, "%02d:%02d %s %d/%d/%d",
				dateTm.tm_hour, dateTm.tm_min,
				tr(numToDay[dateTm.tm_wday]), dateTm.tm_mon+1,
				dateTm.tm_mday, dateTm.tm_year+1900);
	}
	else {
		snprintf(dateStr, 30, "%2d:%02d %s %s %d/%d/%d",
				FMT_12HR(dateTm.tm_hour), dateTm.tm_min,
				FMT_AMPM(dateTm.tm_hour), tr(numToDay[dateTm.tm_wday]),
				dateTm.tm_mon+1, dateTm.tm_mday, dateTm.tm_year+1900);
	}
	SetText(1, dateStr);
}

cDateBox::~cDateBox()
{
}

#define TIBOX_WIDTH			314
#define TIBOX_HEIGHT			40

class cTitleBox : public cTextBox {
private:
public:
	cTitleBox(void) {};
	cTitleBox(const cEvent *);
	~cTitleBox() {};
};

cTitleBox::cTitleBox(const cEvent *e)
	:cTextBox(TIBOX_WIDTH, TIBOX_HEIGHT, lightgrey, tint, fontTitleBox)
{
	char timeStr[80];
	time_t t;
	struct tm startTm, endTm;

	t = e->StartTime();
	(void) localtime_r(&t, &startTm);
	t += e->Duration();
	(void) localtime_r(&t, &endTm);
	if (iTimeFormat == TIME_24HR) {
		snprintf(timeStr, 80, "%02d:%02d - %02d:%02d", startTm.tm_hour,
				startTm.tm_min, endTm.tm_hour, endTm.tm_min);
	}
	else {
		snprintf(timeStr, 80, "%2d:%02d %s - %2d:%02d %s",
				FMT_12HR(startTm.tm_hour), startTm.tm_min,
				FMT_AMPM(startTm.tm_hour),
				FMT_12HR(endTm.tm_hour), endTm.tm_min,
				FMT_AMPM(endTm.tm_hour));
	}
	SetText(2, e->Title(), timeStr);
}

const char *recFreqStr[4] = {
	"Once",
	"Every",
	"Mon-Fri",
	"Sun-Sat"
};

class cRecDlgBox {
private:
	const cEvent	*recEvent;
	cBitmap			*recBmp;
	cTextBox		*recWidgets[7];
	struct tm		recCurrStart;
	struct tm		recCurrStop;
	int			recCurrFreq;
	int			recCursor;

public:
	cRecDlgBox(void) {};
	cRecDlgBox(const cEvent *);
	~cRecDlgBox();
	void		Draw(void);
	cBitmap		*GetBmp(void) { return (recBmp); }
	bool		ProcessKey(eKeys);
	bool		AddTimer(void);
};

cRecDlgBox::cRecDlgBox(const cEvent *event)
	: recEvent(event)
{
	time_t tmpT;

	tmpT = recEvent->StartTime() - Setup.MarginStart * 60;
	(void) localtime_r(&tmpT, &recCurrStart);
	tmpT = recEvent->StartTime() + recEvent->Duration() + Setup.MarginStop * 60;
	(void) localtime_r(&tmpT, &recCurrStop);
	recCurrFreq = 0;
	recCursor = 0;

	recBmp = new cBitmap(iRecBoxWidth, iRecBoxHeight, 2);

	recWidgets[0] = new cTextBox(352, 26, lightgrey, tint, fontTimeBox);
	recWidgets[0]->SetXAlign(TBOX_TXT_CENTER);
	recWidgets[0]->SetText(1, recEvent->Title());

	recWidgets[1] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[1]->SetXAlign(TBOX_TXT_LEFT);
	recWidgets[1]->SetText(1, tr("Start:"));

	recWidgets[2] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[2]->SetXAlign(TBOX_TXT_RIGHT);
	recWidgets[2]->SetHLColor(highlight);

	recWidgets[3] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[3]->SetXAlign(TBOX_TXT_LEFT);
	recWidgets[3]->SetText(1, tr("Stop:"));

	recWidgets[4] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[4]->SetXAlign(TBOX_TXT_RIGHT);
	recWidgets[4]->SetHLColor(highlight);

	recWidgets[5] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[5]->SetXAlign(TBOX_TXT_LEFT);
	recWidgets[5]->SetText(1, tr("Frequency:"));

	recWidgets[6] = new cTextBox(175, 26, lightgrey, tint, fontTimeBox);
	recWidgets[6]->SetXAlign(TBOX_TXT_RIGHT);
	recWidgets[6]->SetHLColor(highlight);
}

cRecDlgBox::~cRecDlgBox()
{
	delete recBmp;
	for (int i = 0; i < 7; i++) {
		delete recWidgets[i];
	}
}

void
cRecDlgBox::Draw(void)
{
	char tmpStr[64];

	recBmp->DrawRectangle(0, 0, iRecBoxWidth, iRecBoxHeight, lightgrey);
	recBmp->DrawRectangle(2, 2, iRecBoxWidth-3, iRecBoxHeight-3, blue);

	recWidgets[0]->Draw(4, 4, recBmp);
	recWidgets[1]->Draw(4, 32, recBmp);
	if (iTimeFormat == TIME_24HR) {
		snprintf(tmpStr, 64, "%02d:%02d", recCurrStart.tm_hour,
				recCurrStart.tm_min);
	}
	else {
		snprintf(tmpStr, 64, "%d:%02d %s", FMT_12HR(recCurrStart.tm_hour),
				recCurrStart.tm_min, FMT_AMPM(recCurrStart.tm_hour));
	}
	recWidgets[2]->SetText(1, tmpStr);
	if (recCursor == 0) {
		recWidgets[2]->SetHighLight(true);
	}
	else {
		recWidgets[2]->SetHighLight(false);
	}
	recWidgets[2]->Draw(181, 32, recBmp);
	recWidgets[3]->Draw(4, 60, recBmp);
	if (iTimeFormat == TIME_24HR) {
		snprintf(tmpStr, 64, "%02d:%02d", recCurrStop.tm_hour,
				recCurrStop.tm_min);
	}
	else {
		snprintf(tmpStr, 64, "%d:%02d %s", FMT_12HR(recCurrStop.tm_hour),
				recCurrStop.tm_min, FMT_AMPM(recCurrStop.tm_hour));
	}
	recWidgets[4]->SetText(1, tmpStr);
	if (recCursor == 1) {
		recWidgets[4]->SetHighLight(true);
	}
	else {
		recWidgets[4]->SetHighLight(false);
	}
	recWidgets[4]->Draw(181, 60, recBmp);
	recWidgets[5]->Draw(4, 88, recBmp);
	if (recCurrFreq != 1) {
		snprintf(tmpStr, 64, "%s", tr(recFreqStr[recCurrFreq]));
	}
	else {
		snprintf(tmpStr, 64, "%s %s", tr(recFreqStr[recCurrFreq]),
				tr(numToDay[recCurrStart.tm_wday]));
	}
	recWidgets[6]->SetText(1, tmpStr);
	if (recCursor == 2) {
		recWidgets[6]->SetHighLight(true);
	}
	else {
		recWidgets[6]->SetHighLight(false);
	}
	recWidgets[6]->Draw(181, 88, recBmp);
}

bool
cRecDlgBox::ProcessKey(eKeys Key)
{
	bool retVal = false;
	time_t t1, t2;

	t1 = mktime(&recCurrStart);
	t2 = mktime(&recCurrStop);

	switch (Key & ~k_Repeat) {
	case kDown:
		if (recCursor < 2) {
			recCursor++;
			retVal = true;
		}
		break;

	case kUp:
		if (recCursor > 0) {
			recCursor--;
			retVal = true;
		}
		break;

	case kLeft:
		if (recCursor == 0) {
			t1 -= 60;
			(void) localtime_r(&t1, &recCurrStart);
			retVal = true;
		}
		else if (recCursor == 1) {
			if ((t2 - 60) > t1) {
				t2 -= 60;
				(void) localtime_r(&t2, &recCurrStop);
				retVal = true;
			}
		}
		else {
			if (recCurrFreq > 0) {
				recCurrFreq--;
				retVal = true;
			}
		}
		break;

	case kRight:
		if (recCursor == 0) {
			if ((t1 + 60) < t2) {
				t1 += 60;
				(void) localtime_r(&t1, &recCurrStart);
				retVal = true;
			}
		}
		else if (recCursor == 1) {
			t2 += 60;
			(void) localtime_r(&t2, &recCurrStop);
			retVal = true;
		}
		else {
			if (recCurrFreq < 3) {
				recCurrFreq++;
				retVal = true;
			}
		}
		break;

	default:
		retVal = false;
		break;
	}

	return (retVal);
}

static const char *wdayToFreq[7] = {
	"------S",
	"M------",
	"-T-----",
	"--W----",
	"---T---",
	"----F--",
	"-----S-",
};

bool
cRecDlgBox::AddTimer(void)
{
	cTimer *recTimer;
	char dayStr[8], file[MaxFileName], eventStr[256];
	int flags, channel, start, stop, priority, lifetime;

	/* Create the timer */
	recTimer = new cTimer;

	/* Construct the string that represents the event */
	flags = tfActive;
	channel = Channels.GetByChannelID(recEvent->ChannelID(), true)->Number();
	switch (recCurrFreq) {
	case 0:
		snprintf(dayStr, 8, "%d", recCurrStart.tm_mday);
		break;
	case 1:
		strncpy(dayStr, wdayToFreq[recCurrStart.tm_wday], 7);
		break;
	case 2:
		strncpy(dayStr, "MTWTF--", 7);
		break;
	case 3:
		strncat(dayStr, "MTWTFSS", 7);
		break;
	default:
		delete recTimer;
		return false;
	}
	start = (recCurrStart.tm_hour * 100) + recCurrStart.tm_min;
	stop = (recCurrStop.tm_hour * 100) + recCurrStop.tm_min;
	priority = Setup.DefaultPriority;
	lifetime = Setup.DefaultLifetime;
	*file = '\0';
	if (!isempty(recEvent->Title())) {
		strncpy(file, recEvent->Title(), sizeof(file));
	}
	snprintf(eventStr, 256, "%d:%d:%s:%04d:%04d:%d:%d:%s:",
			flags, channel, dayStr, start, stop,
			priority, lifetime, file);
	/* fprintf(stderr, "EVENT: %s\n", eventStr); */

	/* Parse the string to fill in the timer */
	if (recTimer->Parse(eventStr) == false) {
		delete recTimer;
		return false;
	}

	/* Add the timer to the timer list and flush to disk */
	Timers.Add(recTimer);
	Timers.Save();

	return true;
}

#define MSGBOX_WIDTH				360
#define MSGBOX_HEIGHT				42

class cMessageBox {
private:
	cBitmap		*msgBmp;
	cTextBox	*msgBox;

public:
	cMessageBox(const char *);
	~cMessageBox();
	void		Draw(void);
	cBitmap		*GetBmp(void) { return (msgBmp); }
};

cMessageBox::cMessageBox(const char *msg)
{
	msgBmp = new cBitmap(MSGBOX_WIDTH, MSGBOX_HEIGHT, 2);

	msgBox = new cTextBox(MSGBOX_WIDTH - 8, MSGBOX_HEIGHT - 8,
			lightgrey, tint, fontTimeBox);
	msgBox->SetXAlign(TBOX_TXT_CENTER);
	msgBox->SetText(1, msg);
}

cMessageBox::~cMessageBox()
{
	/* delete msgBmp; */
	/* delete msgBox; */
}

void
cMessageBox::Draw(void)
{
	msgBmp->DrawRectangle(0, 0, MSGBOX_WIDTH, MSGBOX_HEIGHT, lightgrey);
	msgBmp->DrawRectangle(2, 2, MSGBOX_WIDTH-3, MSGBOX_HEIGHT-3, blue);
	msgBox->Draw(4, 4, msgBmp);
}

#define CURSOR_UP			0
#define CURSOR_DOWN			1
#define CURSOR_LEFT			2
#define CURSOR_RIGHT			3

#define SCROLL_LEFT			1
#define SCROLL_RIGHT			2

class cYaepg: public cOsdObject {
private:
	cOsd			*osd;
	tArea			areas[3];
	cBitmap			*EPGBitmap;
	cBitmap			*InfoBitmap;

	int			cursorX;
	int			cursorY;
	int			scrollDir;
	unsigned short		prevEventId;

	/*
	 * EPG window stuff.
	 */
	cTimeBox		*timebox[4];
	cChanBox		*chans[6]; //TODO
	sEventRow		eventgrid[6]; //TODO
        
	int                     last_channel, first_channel;
	
	/*
	 * Info window stuff.
	 */
	cTitleBox		*titlebox;
	cInfoBox		*infobox;
	cDateBox		*datebox;

	cRecDlgBox		*recDlgBox;
	uint64_t			msgboxStart;
	cMessageBox		*msgbox;

	/*
	 * Times:
	 * 	first - The earliest time that can be displayed in the grid.
	 * 	start - The earliest time that is currently displayed in the grid.
	 * 	curr - The current time.
	 * 	lastup - The last time the display was updated.
	 */
	time_t			firstT;
	struct tm		firstTime;
	time_t			startT;
	struct tm		startTime;
	time_t			currT;
	struct tm		currTime;
	time_t			lastupT;
	int			startChan;

	int			selChan;
	const cEvent	*selEvent;

	/*
	 * Direct Channel Change
	 */
	int			currChan;
	uint64_t			lastInput;

	/*
	 * The last possible starting channel for the grid.
	 */
	int			lastChan;

	/*
	 * Debugging
	 */
	static FILE		*log;

	/*
	 * Round a time down to a 30-minute boundary.
	 */
	time_t AdjustTime(time_t);

public:
	cYaepg(void);
	~cYaepg();
	virtual void Show(void);
	virtual eOSState ProcessKey(eKeys);
	void Draw(void);
	void SwitchToCurrentChannel(void);
	void MoveCursor(int);
	void ChangeChan(int);
	int ChangeTime(int);
	void GetChannelListEnds(void);
	void UpdateTime(void);
	static void LogMsg(eYlog_t, const char *fmt, ...);
};

FILE *cYaepg::log = NULL;

cYaepg::cYaepg(void)
{
	osd = NULL;
	EPGBitmap = NULL;
	InfoBitmap = NULL;

	cursorX = 0;
	cursorY = 0;
	scrollDir = 0;
	prevEventId = 0;

	selChan = 0;

	log = fopen("yaepg.log", "w+");
	if (log == NULL) {
		fprintf(stderr, "Could not open log file!\n");
	}
}

cYaepg::~cYaepg()
{
	if (osd != NULL) {
		delete osd;
	}
	if (EPGBitmap != NULL) {
		delete EPGBitmap;
	}
	if (InfoBitmap != NULL) {
		delete InfoBitmap;
	}

	if (log) {
		fflush(log);
		fclose(log);
	}
}

void
cYaepg::Show(void)
{
	osd = cOsdProvider::NewOsd(0, 0);
	if (osd == NULL) {
		fprintf(stderr, "OpenRaw() returned NULL!\n");
		return;
	}
		
	/*
	 * Create the video window
	 */
	osd->vidWin.x1 = 0;
	osd->vidWin.y1 = 0;
	osd->vidWin.x2 = iOutputResW / 2;
	osd->vidWin.y2 = iOutputResH / 2;
	osd->vidWin.bpp = 12;

	/*
	 * Create the EPG window
	 */
	EPGWinHandle.x1 = 0;
	EPGWinHandle.y1 = iOutputResH / 2;
	EPGWinHandle.x2 = iOutputResW - 1;
	EPGWinHandle.y2 = iOutputResH - 1;
	EPGWinHandle.bpp = 2;
	EPGBitmap = new cBitmap(iOutputResW, iOutputResH / 2, 2);

	/*
	 * Create the info window
	 */
	InfoWinHandle.x1 = iOutputResW / 2;
	InfoWinHandle.y1 = 0;
	InfoWinHandle.x2 = iOutputResW - 1;
	InfoWinHandle.y2 = iOutputResH / 2 - 1;
	InfoWinHandle.bpp = 2;

	osd->SetAreas(areas, 2);
	InfoBitmap = new cBitmap(iOutputResW / 2, iOutputResH / 2, 2);


	(void) time(&currT);
	(void) localtime_r(&currT, &currTime);

	firstT = startT = AdjustTime(currT);
	(void) localtime_r(&firstT, &firstTime);
	(void) localtime_r(&startT, &startTime);

	startChan = cDevice::CurrentChannel();
	currChan = 0;
	recDlgBox = NULL;
	msgbox = NULL;

	/*
	 * The maximum start channel is the last channel - the number of
	 * channels displayed in the grid.  Ensure that this isn't violated, and
	 * adjust the initial cursor position as needed.
	 */
	cChannel *c = Channels.Last();
	YAEPG_INFO("Channels.Last() = %d\n", c->Number());
	for (int i = 0; i < (iChannelCount - 1); i++) {
		while ((c = (cChannel *)c->Prev()) && (c->GroupSep())) {
			;
		}
	}
	if (c == NULL) {
		YAEPG_ERROR("Can't find %d valid channels!\n", iChannelCount);
		return;
	}
	lastChan = c->Number();
	YAEPG_INFO("lastChan = %d\n", lastChan);
	if (startChan > lastChan) {
		cursorY = startChan - lastChan;
		startChan = lastChan;
	}

	Draw();
}

void
cYaepg::SwitchToCurrentChannel(void)
{
	if (selChan != cDevice::CurrentChannel()) {
		/*
		 * The "Channel not availaible message" will
		 * cause vdr to crash. Do a "lower level"
		 * channel switch to avoid the error message.
		 */
		const cChannel *c = Channels.GetByNumber(selChan);
		eSetChannelResult ret;

		if (c != NULL) {
			ret = cDevice::PrimaryDevice()->SetChannel(c, true);
			if (ret != scrOk) {
				fprintf(stderr, "SetChannel(): %d\n", ret);
			}
		}
	}
	return;	
}

void
cYaepg::GetChannelListEnds()
{
        // Set the first and last channel numbers
        cChannel *c = Channels.First();
        first_channel = c->Number();
        c = Channels.Last();
        last_channel = c->Number();
}

void
cYaepg::Draw(void)
{
	cChannel *c;
	int i, j, cx, cy;

	/*
	 * Draw the EPG window
	 */
	EPGBitmap->DrawRectangle(0, 0, iOutputResW, iOutputResH / 2, blue);

	/*
	 * Draw the date/current channel box box.
	 */
	cx = 36, cy = 2;
	cTextBox *datebox;
	datebox = new cTextBox(CHBOX_WIDTH, TMBOX_HEIGHT, lightgrey, tint, fontTimeBox);
	datebox->SetXAlign(TBOX_TXT_CENTER);
	char dateStr[16];
	if (currChan == 0) {
		snprintf(dateStr, 16, "%d/%d", startTime.tm_mon+1, startTime.tm_mday);
	}
	else {
		snprintf(dateStr, 16, "%d-", currChan);
	}
	datebox->SetText(1, dateStr);
	datebox->Draw(cx, cy, EPGBitmap);

	/*
	 * Draw the time bar and the timeline.
	 */
	if (startT <= currT) {
		int min;
		min = currTime.tm_min - ((startTime.tm_min / 30) * 30);
		cx = 106 + (int)(((float)min * 4.86) + 0.5);
		EPGBitmap->DrawRectangle(cx, 20, cx+1, 212, lightgrey);
	}
	cx = 106, cy = 2;
	for (i = 0; i < 4; i++) {
		timebox[i] = new cTimeBox(&startTime, i*30);
		timebox[i]->Draw(cx, cy, EPGBitmap);
		cx += timebox[i]->Width();
	}

	/*
	 * Draw the channel bar
	 */
	cx = 36, cy = 22;
	c = Channels.GetByNumber(startChan);
	GetChannelListEnds();
	for (i = 0; i < iChannelCount; i++) {
		if (c != NULL) {
			chans[i] = new cChanBox(c);
			chans[i]->Draw(cx, cy, EPGBitmap);
			cy += chans[i]->Height() + 2;
		}
                if (iChannelOrder == CHANNEL_ORDER_UP) {
                   while ((c = (cChannel *)c->Prev()) && (c->GroupSep()));
                   if (c == NULL)
                      c = Channels.GetByNumber(last_channel);
                } else {
                   while ((c = (cChannel *)c->Next()) && (c->GroupSep()));
                   if (c == NULL) 
                      c = Channels.GetByNumber(first_channel);
		}
	}

	/*
	 * Draw the event grid
	 */
	cSchedulesLock SchedulesLock;
	const cSchedules* Schedules = cSchedules::Schedules(SchedulesLock);
	cy = 22;
	time_t grTime, t0 = mktime(&timebox[0]->tmTime);
	const cSchedule *currSched;
	const cEvent *currEvent;
	for (i = 0; i < iChannelCount; i++) {
		cx = 106;
		currSched = Schedules->GetSchedule(chans[i]->GetChanID());

		/*
		 * I assume that there won't be a channel that has more than 8
		 * events in a 2 hour time period.  This seems reasonable, if
		 * there are more than 8 it just won't show them.
		 */
		j = 0;
		grTime = t0;
		eventgrid[i].rowLen = 0;
		while ((j < 8) && (grTime < (mktime(&timebox[3]->tmTime) + 1800))) {
			if (currSched != NULL) {
				currEvent = currSched->GetEventAround(grTime);
				if ((currEvent != NULL) &&
					(currEvent->StartTime() + currEvent->Duration()) <= grTime) {
					currEvent = NULL;
				}
			}
			else {
				currEvent = NULL;
			}

			if (currEvent == NULL) {
				/*
				 * If we didn't find an event at this time try
				 * make a "No Info" evnet.
				 */
				currEvent = new cNoInfoEvent(grTime);
				if (currEvent == NULL) {
					fprintf(stderr, "NULL\n");
				}
			}

#if 0
			fprintf(stderr, "%d %d %s\n",
					currEvent->StartTime(),
					currEvent->Duration(),
					currEvent->Title());
#endif

			eventgrid[i].event[j] = new cEventBox(currEvent, t0);
			eventgrid[i].rowLen++;
			eventgrid[i].event[j]->Draw(
					cx + eventgrid[i].event[j]->XOff(),
					cy, EPGBitmap);
			grTime = currEvent->StartTime() +
				currEvent->Duration();
			j++;
		}
		cy += CHBOX_HEIGHT + 2;
	}

	if ((scrollDir == SCROLL_LEFT) ||
			(cursorX >= eventgrid[cursorY].rowLen)) {
		cursorX = (eventgrid[cursorY].rowLen-1);
	}
	eventgrid[cursorY].event[cursorX]->SetHighLight(true);
	eventgrid[cursorY].event[cursorX]->Draw();
	selEvent = eventgrid[cursorY].event[cursorX]->GetEvent();

	selChan = chans[cursorY]->GetChanNum();

	osd->DrawBitmap(0, iOutputResH / 2, *EPGBitmap);

	/*
	 * Draw the info window
	 */
	InfoBitmap->DrawRectangle(0, 0, iOutputResW / 2, iOutputResH / 2 - 1, blue);

	cx = 8;
	cy = 32;
	titlebox = new cTitleBox(eventgrid[cursorY].event[cursorX]->GetEvent());
	titlebox->Draw(cx, cy, InfoBitmap);

	cx = 8;
	cy = 76;
	infobox = new cInfoBox(eventgrid[cursorY].event[cursorX]->GetEvent());
	infobox->Draw(cx, cy, InfoBitmap);

	cx = 8;
	cy = iOutputResH / 2 - 24; // TODO
	datebox = new cDateBox(currT);
	datebox->Draw(cx, cy, InfoBitmap);

	osd->DrawBitmap(iOutputResW / 2, 0, *InfoBitmap);

	/*
	 * Draw the message box if needed.
	 */
	if (msgbox != NULL) {
		msgbox->Draw();
		osd->DrawBitmap((iOutputResW - MSGBOX_WIDTH) / 2,
				((iOutputResH / 2) + ((iOutputResH / 2) - MSGBOX_HEIGHT) / 2),
				*(msgbox->GetBmp()));
	}

	/*
	 * Draw the record dialog box if needed.
	 */
	if (recDlgBox != NULL) {
		recDlgBox->Draw();
		osd->DrawBitmap(iOutputResW / 4,
				(iOutputResH * 5) / 8,
				*(recDlgBox->GetBmp()));
	}

	/*
	 * Flush everything to the screen.
	 */
	osd->Flush();

	/*
	 * Free everything.  It would be a lot more efficient to just leave
	 * these things allocated and update the info in them.
	 */
	for (int i = 0; i < 4; i++) {
		delete timebox[i];
	}

	for (int i = 0; i < iChannelCount; i++) {
		delete chans[i];
		for (int j = 0; j < eventgrid[i].rowLen; j++) {
			delete eventgrid[i].event[j];
		}
	}

	delete titlebox;
	delete infobox;
	delete datebox;
}

eOSState
cYaepg::ProcessKey(eKeys Key)
{
	eOSState state = cOsdObject::ProcessKey(Key);
	if (recDlgBox != NULL && state == osUnknown) {
		switch (Key & ~k_Repeat) {
		case kOk:
			/* Add the timer */
			recDlgBox->AddTimer();
			delete recDlgBox;
			recDlgBox = NULL;
			
			/* Display the "Timer added" message" */
			msgbox = new cMessageBox(tr("Timer added"));
			msgboxStart = cTimeMs::Now();

			Draw();
			break;
		case kBack:
			/* Cancel adding a timer */
			delete recDlgBox;
			recDlgBox = NULL;

			/* Display the "Timer cancelled" message */
			msgbox = new cMessageBox(tr("Timer cancelled"));
			msgboxStart = cTimeMs::Now();

			Draw();
			break;
		default:
			if (recDlgBox->ProcessKey(Key) == true) {
				Draw();
			}
			break;
		}

		return (osContinue);
	}
	if (state == osUnknown) {
		switch (Key & ~k_Repeat) {
		case kBack:
			state = osEnd;
			break;

		case kBlue:
			/* Can this be fixed to work with NTSC ? */
#if VDRVERSNUM >= 10338
			cDevice::PrimaryDevice()->GrabImageFile("img.jpg",
#else
			cDevice::PrimaryDevice()->GrabImage("img.jpg",
#endif
					true, 256, -1, -1);
			state = osContinue;
			break;

		case kLeft:
			MoveCursor(CURSOR_LEFT);
			Draw();
			state = osContinue;
			break;

		case kRight:
			MoveCursor(CURSOR_RIGHT);
			Draw();
			state = osContinue;
			break;

		case kUp:
			MoveCursor(CURSOR_UP);
			Draw();

			if (iChannelChange == CHANNEL_CHANGE_AUTO_INEPG) {
				SwitchToCurrentChannel();
                        }

			state = osContinue;
			break;

		case kDown:
			MoveCursor(CURSOR_DOWN);
			Draw(); 

			if (iChannelChange == CHANNEL_CHANGE_AUTO_INEPG) {
				SwitchToCurrentChannel();
			}
			
			state = osContinue;
			break;

		case kOk:
			/*
			 * If OK is pressed on an event that hasn't started yet,
			 * then we want to bring up the "record" dialog box.
			 */
			if (selEvent->StartTime() <= time(NULL)) {
				SwitchToCurrentChannel();
				if (iChannelChange == CHANNEL_CHANGE_MANUAL_INEPG)
				   state = osContinue;
				else
				   state = osEnd;
			}
			else {
				recDlgBox = new cRecDlgBox(selEvent);
				Draw();
			}
			break;

		case kRed:
		case kChanUp:
			ChangeChan((iChannelOrder == CHANNEL_ORDER_UP ? 1 : -1) * iChannelCount * 1);
			Draw();
			state = osContinue;
			break;

		case kGreen:
			SwitchToCurrentChannel();
			state = osContinue;
			break;

		case kYellow:
		case kChanDn:
			ChangeChan((iChannelOrder == CHANNEL_ORDER_UP ? 1 : -1) * iChannelCount * -1);
			Draw();
			state = osContinue;
			break;

		case k0 ... k9:
			/* Direct Channel Input */
			if ((currChan) || (Key != k0)) {
				cChannel *chan = Channels.GetByNumber(currChan);
				cChannel *ch = chan;

				currChan = (currChan * 10) + (Key - k0);
				int n = chan ? currChan * 10 : 0;
				while (ch && (ch = Channels.Next(ch)) != NULL) {
					if (n <= ch->Number() && ch->Number() <= (n + 9)) {
						n = 0;
						break;
					}
					if (ch->Number() > n) {
						n *= 10;
					}
				}
				if (n > 0) {
					if (Channels.GetByNumber(currChan)) {
						startChan = currChan;
						Draw();
					}
					state = osContinue;
					break;
				}

				lastInput = cTimeMs::Now();
				Draw();
			}
			break;

		default:
			break;
		}
	}

	/*
	 * Check for timer realted stuff on ANY key press.  Note, this code used
	 * to be part of kNone key processing but if users keep pressing keys we
	 * won't get a kNnone for a while.  This makes the timer related
	 * operations more responsive.
	 */
	time_t nowT;
	struct tm nowTm;
	bool needDraw = false;

	(void) time(&nowT);
	(void) localtime_r(&nowT, &nowTm);

	/* Update the current time line ? */
	if (nowTm.tm_min != startTime.tm_min) {
		UpdateTime();
		needDraw = true;
	}

	/* Cancel direct channel input ? */
	if (currChan && (cTimeMs::Now() - lastInput) > 1000) {
		/* fprintf(stderr, "Timeout! %d\n", currChan); */
		for (int i = 0; i < 100; i++) {
			if (Channels.GetByNumber(currChan-i)) {
				cursorY = 0;
				startChan = currChan-i;
				needDraw = true;
				break;
			}
		}
		currChan = 0;
	}

	/* Did the message box expire ? */
	if (msgbox != NULL && (cTimeMs::Now() - msgboxStart) > 1000) {
		delete msgbox;
		msgbox = NULL;
		needDraw = true;
	}

	/* Did something happen that requires a redraw ? */
	if (needDraw == true) {
		Draw();
	}

	return state;
}

void
cYaepg::ChangeChan(int change)
{
	cChannel *c;

	c = Channels.GetByNumber(startChan);
	GetChannelListEnds();
	if (change > 0) {
		for (int i = 0; i < change; i++) {
			while ((c = (cChannel *)c->Next()) && (c->GroupSep())) {
				;
			}
			if (c == NULL) {
				c = Channels.GetByNumber(first_channel);
			}
			startChan = c->Number();
			/*
			 * Ensure we don't start the grid past the last possible
			 * channel that we can display in the grid.
			 */
			if (startChan > lastChan) {
				startChan = lastChan;
			}
		}
	}
	else if (change < 0) {
	        if (startChan < first_channel) {
			startChan = last_channel;
			return;
		}
		for (int i = 0; i > change; i--) {
			while ((c = (cChannel *)c->Prev()) && (c->GroupSep())) {
				;
			}
			if (c == NULL) {
				c = Channels.GetByNumber(last_channel);
			}
			startChan = c->Number();
		}
	}
}

int
cYaepg::ChangeTime(int shift)
{
	/*
	 * Don't let the guide go back in time.
	 */
	if ((startT + shift) < firstT) {
		return -1;
	}

	startT += shift;
	(void) localtime_r(&startT, &startTime);

	return 0;
}

void
cYaepg::MoveCursor(int dir)
{
	prevEventId = 0;
	scrollDir = 0;

	switch (dir) {
	case CURSOR_UP:
		if (cursorY == 0) {
			ChangeChan(1 * (iChannelOrder == CHANNEL_ORDER_UP ? 1 : -1));
		}
		else {
			cursorY--;
		}
		break;

	case CURSOR_DOWN:
		if (cursorY == iChannelCount - 1) {
			ChangeChan(-1 * (iChannelOrder == CHANNEL_ORDER_UP ? 1 : -1));
		}
		else {
			cursorY++;
		}
		break;

	case CURSOR_LEFT:
		if (cursorX == 0) {
			if (ChangeTime(-5400) == 0) {
				scrollDir = SCROLL_LEFT;
			}
		}
		else {
			cursorX--;
		}
		break;

	case CURSOR_RIGHT:
		if (cursorX == (eventgrid[cursorY].rowLen - 1)) {
			(void) ChangeTime(5400);
			scrollDir = SCROLL_RIGHT;
			cursorX = 0;
		}
		else {
			cursorX++;
		}
		break;

	default:
		break;
	}

	if (cursorX >= eventgrid[cursorY].rowLen) {
		cursorX = eventgrid[cursorY].rowLen - 1;
	}
}

void
cYaepg::UpdateTime(void)
{
	(void) time(&currT);
	(void) localtime_r(&currT, &currTime);

	if ((currTime.tm_min == 0) || (currTime.tm_min == 30)) {
		firstT = AdjustTime(currT);
		(void) localtime_r(&firstT, &firstTime);
		/*
		 * Do we need to update starTime ?
		 */
		if (startT < firstT) {
			startT = firstT;
			(void) localtime_r(&startT, &startTime);
		}
	}
}

void
cYaepg::LogMsg(eYlog_t logType, const char *fmt, ...)
{
	va_list ap;

	if (log) {
		switch (logType) {
		case YLOG_ERR:
			fprintf(log, "ERROR: ");
			break;
		case YLOG_INF:
			fprintf(log, "INFO: ");
			break;
		default:
			return;
		}
		va_start(ap, fmt);
		vfprintf(log, fmt, ap);
		va_end(ap);
		fflush(log);
	}
	else {
		fprintf(stderr, "LOG = NULL!!!\n");
	}
}

/*
 * Round the time down to a 30 minute boundary
 */
time_t
cYaepg::AdjustTime(time_t t)
{
	struct tm tm;

	(void) localtime_r(&t, &tm);
	tm.tm_sec = 0;
	if (tm.tm_min < 30) {
		tm.tm_min = 0;
	}
	else {
		tm.tm_min = 30;
	}

	return mktime(&tm);
}

class cPluginYaepg : public cPlugin {
private:
	// Add any member variables or functions you may need here.
	cYaepg			*yaepg;
	
public:
	cPluginYaepg(void);
	virtual ~cPluginYaepg();
	virtual const char *Version(void) { return VERSION; }
	virtual const char *Description(void) { return DESCRIPTION; }
	virtual const char *CommandLineHelp(void);
	virtual bool ProcessArgs(int argc, char *argv[]);
	virtual bool Initialize(void);
	virtual bool Start(void);
	virtual void Housekeeping(void);
	virtual const char *MainMenuEntry(void) { return iHideMenuEntry ? NULL : MAINMENUENTRY; }
	virtual cOsdObject *MainMenuAction(void);
	virtual cMenuSetupPage *SetupMenu(void);
	virtual bool SetupParse(const char *Name, const char *Value);
};

class cMenuSetupYaepg : public cMenuSetupPage {
private:
	int iNewTvFormat;
	int iNewHideMenuEntry;
	int iNewChannelChange;
	int iNewTimeFormat;
	int iNewChannelOrder;

protected:
	virtual void Store(void);

public:
	cMenuSetupYaepg(void);
};

cPluginYaepg::cPluginYaepg(void)
{
	// Initialize any member variables here.
	// DON'T DO ANYTHING ELSE THAT MAY HAVE SIDE EFFECTS, REQUIRE GLOBAL
	// VDR OBJECTS TO EXIST OR PRODUCE ANY OUTPUT!
}

cPluginYaepg::~cPluginYaepg()
{
	// Clean up after yourself!
}

const char *cPluginYaepg::CommandLineHelp(void)
{
	// Return a string that describes all known command line options.
	return NULL;
}

bool cPluginYaepg::ProcessArgs(int argc, char *argv[])
{
	// Implement command line argument processing here if applicable.
	return true;
}

bool cPluginYaepg::Initialize(void)
{
	// Initialize any background activities the plugin shall perform.
	return true;
}

bool cPluginYaepg::Start(void)
{
	// Start any background activities the plugin shall perform.
#if APIVERSNUM < 10507
	RegisterI18n(Phrases);
#endif
	return true;
}

void cPluginYaepg::Housekeeping(void)
{
	// Perform any cleanup or other regular tasks.
}

cOsdObject *cPluginYaepg::MainMenuAction(void)
{
	// Perform the action when selected from the main VDR menu.
	yaepg = new cYaepg;
	
	if (iTvFormat == TV_NTSC)
	{
		iOutputResW	= TV_NTSC_W;
		iOutputResH	= TV_NTSC_H;
		iChannelCount	= CHANNELS_NTSC;
	}
	else
	{
		iOutputResW	= TV_PAL_W;
		iOutputResH	= TV_PAL_H;
		iChannelCount	= CHANNELS_PAL;
	}
	
	return yaepg;
}

// Plugin setup menu
void cMenuSetupYaepg::Store(void)
{
	iTvFormat		= iNewTvFormat;
	iHideMenuEntry		= iNewHideMenuEntry;
	iChannelChange          = iNewChannelChange;
	iTimeFormat		= iNewTimeFormat;
	iChannelOrder           = iNewChannelOrder;
	
	SetupStore("Format",		iTvFormat);
	SetupStore("HideMenuEntry",	iHideMenuEntry);
	SetupStore("ChannelChange",     iChannelChange);
	SetupStore("TimeFormat",	iTimeFormat);
	SetupStore("ChannelOrder",      iChannelOrder);
}

cMenuSetupYaepg::cMenuSetupYaepg(void)
{
	iNewTvFormat		= iTvFormat;
	iNewHideMenuEntry	= iHideMenuEntry;
	iNewChannelChange       = iChannelChange;
	iNewTimeFormat		= iTimeFormat;
	iNewChannelOrder        = iChannelOrder;

	Add(new cMenuEditStraItem (tr("TV output format"), &iNewTvFormat, 2, TV_FORMATS));
	Add(new cMenuEditBoolItem (tr("Hide mainmenu entry"), &iNewHideMenuEntry));
	Add(new cMenuEditStraItem (tr("Channel Change"), &iNewChannelChange, 3, CH_CHANGE_MODES));
	Add(new cMenuEditStraItem (tr("Time format"), &iNewTimeFormat, 2, TIME_FORMATS));
	Add(new cMenuEditStraItem (tr("Channel Order"), &iNewChannelOrder, 2, CH_ORDER_FORMATS));
}

cMenuSetupPage *cPluginYaepg::SetupMenu(void)
{
	// Return a setup menu in case the plugin supports one.
	return new cMenuSetupYaepg;
}

bool cPluginYaepg::SetupParse(const char *Name, const char *Value)
{
	// Parse your own setup parameters and store their values.
	if (!strcasecmp(Name, "Format")) {
		iTvFormat = atoi(Value);
	}
	else if (!strcasecmp(Name, "HideMenuEntry")) {
		iHideMenuEntry = atoi(Value);
	}
	else if (!strcasecmp(Name, "ChannelChange")) {
		iChannelChange = atoi(Value);
	}
	else if (!strcasecmp(Name, "TimeFormat")) {
		iTimeFormat = atoi(Value);
	}
	else if (!strcasecmp(Name, "ChannelOrder")) {
		iChannelOrder = atoi(Value);
	}
	else {
		return false;
	}

	return true;
}

VDRPLUGINCREATOR(cPluginYaepg); // Don't touch this!
