/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "agi/agi.h"
#include "agi/sprite.h"		// for commit_both()
#include "agi/graphics.h"
#include "agi/keyboard.h"

namespace Agi {

void AgiEngine::printText2(int l, const char *msg, int foff, int xoff, int yoff,
				int len, int fg, int bg, bool checkerboard) {
	int x1, y1;
	int maxx, minx, ofoff;
	int update;
	// Note: Must be unsigned to use AGDS cyrillic characters!
#ifdef __DS__
	// On the DS, a compiler bug causes the text to render incorrectly, because
	// GCC tries to optimisie out writes to this pointer (tested on DevkitARM v19b and v20)
	// Making this pointer volatile fixes this.
	volatile const unsigned char *m;
#else
	const unsigned char *m;
#endif

	// kludge!
	update = 1;
	if (l == 2) {
		update = l = 0;
	}

	// FR: strings with len == 1 were not printed
	if (len == 1) {
		_gfx->putTextCharacter(l, xoff + foff, yoff, *msg, fg, bg, checkerboard);
		maxx = 1;
		minx = 0;
		ofoff = foff;
		y1 = 0;		// Check this
	} else {
		maxx = 0;
		minx = GFX_WIDTH;
		ofoff = foff;

		for (m = (const unsigned char *)msg, x1 = y1 = 0; *m; m++) {

			// Note: there were extra checks for *m being a cursor character
			// here (1, 2 or 3), which have been removed, as the cursor
			// character is no longer printed via this function.
			if (*m >= 0x20) {
				int ypos = (y1 * CHAR_LINES) + yoff;

				if ((x1 != (len - 1) || x1 == 39) && (ypos <= (GFX_HEIGHT - CHAR_LINES))) {
					int xpos = (x1 * CHAR_COLS) + xoff + foff;

					if (xpos >= GFX_WIDTH)
						continue;

					_gfx->putTextCharacter(l, xpos, ypos, *m, fg, bg, checkerboard);

					if (x1 > maxx)
						maxx = x1;
					if (x1 < minx)
						minx = x1;
				}

				x1++;

				// Change line if we've reached the end of this one, unless the next
				// character is a new line itself, or the end of the string
				if (x1 == len && m[1] != '\n' && m[1] != 0) {
					y1++;
					x1 = foff = 0;
				}
			} else {
				if (m[1] != 0) {
					y1++;
					x1 = foff = 0;
				}
			}
		}
	}

	if (l)
		return;

	if (maxx < minx)
		return;

	maxx *= CHAR_COLS;
	minx *= CHAR_COLS;

	if (update) {
		_gfx->scheduleUpdate(foff + xoff + minx, yoff, ofoff + xoff + maxx + CHAR_COLS - 1,
				yoff + y1 * CHAR_LINES + CHAR_LINES + 1);

		// Making synchronous text updates reduces CPU load
		// when updating status line and input area
		_gfx->doUpdate();
	}
}

//
// len is in characters, not pixels!!
//
void AgiEngine::blitTextbox(const char *p, int y, int x, int len) {
	// if x | y = -1, then center the box
	int xoff, yoff, lin, h, w;
	char *msg, *m;

	debugC(3, kDebugLevelText, "blitTextbox(): x=%d, y=%d, len=%d", x, y, len);
	if (_game.window.active)
		closeWindow();

	if (x == 0 && y == 0 && len == 0)
		x = y = -1;

	if (len <= 0)
		len = 30;

	xoff = x * CHAR_COLS;
	yoff = y * CHAR_LINES;

	m = msg = wordWrapString(agiSprintf(p), &len);

	for (lin = 1; *m; m++) {
		// Test \r for MacOS 8
		if (*m == '\n' || *m == '\r')
			lin++;
	}

	if (lin * CHAR_LINES > GFX_HEIGHT)
		lin = (GFX_HEIGHT / CHAR_LINES);

	w = (len + 2) * CHAR_COLS;
	h = (lin + 2) * CHAR_LINES;

	if (xoff < 0)
		xoff = (GFX_WIDTH - w - CHAR_COLS) / 2;
	else
		xoff -= CHAR_COLS;

	if (yoff < 0)
		yoff = (GFX_HEIGHT - 3 * CHAR_LINES - h) / 2;

	drawWindow(xoff, yoff, xoff + w - 1, yoff + h - 1);

	printText2(2, msg, 0, CHAR_COLS + xoff, CHAR_LINES + yoff,
			len + 1, MSG_BOX_TEXT, MSG_BOX_COLOR);

	free(msg);

	_gfx->doUpdate();
}

void AgiEngine::eraseTextbox() {
	if (!_game.window.active) {
		debugC(3, kDebugLevelText, "eraseTextbox(): no window active");
		return;
	}

	debugC(4, kDebugLevelText, "eraseTextbox(): x1=%d, y1=%d, x2=%d, y2=%d", _game.window.x1,
			_game.window.y1, _game.window.x2, _game.window.y2);

	_gfx->restoreBlock(_game.window.x1, _game.window.y1,
			_game.window.x2, _game.window.y2, _game.window.buffer);

	free(_game.window.buffer);
	_game.window.active = false;

	_gfx->doUpdate();
}

/*
 * Public functions
 */

/**
 * Print text in the AGI engine screen.
 */
void AgiEngine::printText(const char *msg, int f, int x, int y, int len, int fg, int bg, bool checkerboard) {
	f *= CHAR_COLS;
	x *= CHAR_COLS;
	y *= CHAR_LINES;

	debugC(4, kDebugLevelText, "printText(): %s, %d, %d, %d, %d, %d, %d", msg, f, x, y, len, fg, bg);
	printText2(0, agiSprintf(msg), f, x, y, len, fg, bg, checkerboard);
}

/**
 * Print text in the AGI engine console.
 */
void AgiEngine::printTextConsole(const char *msg, int x, int y, int len, int fg, int bg) {
	x *= CHAR_COLS;
	y *= 10;

	debugC(4, kDebugLevelText, "printTextConsole(): %s, %d, %d, %d, %d, %d", msg, x, y, len, fg, bg);
	printText2(1, msg, 0, x, y, len, fg, bg);
}

/**
 * Wrap text line to the specified width.
 * @param str  String to wrap.
 * @param len  Length of line.
 *
 * Based on GBAGI implementation with permission from the author
 */
char *AgiEngine::wordWrapString(const char *s, int *len) {
	char *outStr, *msgBuf;
	int maxWidth = *len;
	const char *pWord;
	int lnLen, wLen;

	// Allocate some extra space for the final buffer, as
	// outStr may end up being longer than s
	// 26 = 200 (screen height) / 8 (font height) + 1
	msgBuf = outStr = (char *)malloc(strlen(s) + 26);

	int msgWidth = 0;

	lnLen = 0;

	while (*s) {
		pWord = s;

		while (*s != '\0' && *s != ' ' && *s != '\n' && *s != '\r')
			s++;

		wLen = (int)(s - pWord);

		if (wLen && *s == '\n' && s[-1] == ' ')
			wLen--;

		if (wLen + lnLen >= maxWidth) {
			// Check if outStr isn't msgBuf. If this is the case, outStr hasn't advanced
			// yet, so no output has been written yet
			if (outStr != msgBuf) {
				if (outStr[-1] == ' ')
					outStr[-1] = '\n';
				else
					*outStr++ = '\n';
			}

			lnLen = 0;

			while (wLen >= maxWidth) {
				msgWidth = maxWidth;

				memcpy(outStr, pWord, maxWidth);

				wLen -= maxWidth;
				outStr += maxWidth;
				pWord  += maxWidth;
				*outStr++ = '\n';
			}
		}

		if (wLen) {
			memcpy(outStr, pWord, wLen);
			outStr += wLen;
		}
		lnLen += wLen+1;

		if (lnLen > msgWidth) {
			msgWidth = lnLen;

			if (*s == '\0' || *s == ' ' || *s == '\n' || *s == '\r')
				msgWidth--;
		}

		if (*s == '\n')
			lnLen = 0;

		if (*s)
			*outStr++ = *s++;
	}
	*outStr = '\0';
	*len = msgWidth;

	return msgBuf;
}

/**
 * Remove existing window, if any.
 */
void AgiEngine::closeWindow() {
	debugC(4, kDebugLevelText, "closeWindow()");

	_sprites->eraseBoth();
	eraseTextbox();	// remove window, if any
	_sprites->blitBoth();
	_sprites->commitBoth();		// redraw sprites
	_game.hasWindow = false;
}

/**
 * Display a message box.
 * This function displays the specified message in a text box
 * centered in the screen and waits until a key is pressed.
 * @param p The text to be displayed
 */
int AgiEngine::messageBox(const char *s) {
	int k;

	_sprites->eraseBoth();
	blitTextbox(s, -1, -1, -1);
	_sprites->blitBoth();
	k = waitKey();
	debugC(4, kDebugLevelText, "messageBox(): wait_key returned %02x", k);
	closeWindow();

	return k;
}

/**
 * Display a message box with buttons.
 * This function displays the specified message in a text box
 * centered in the screen and waits until a button is pressed.
 * @param p The text to be displayed
 * @param b NULL-terminated list of button labels
 */
int AgiEngine::selectionBox(const char *m, const char **b) {
	int numButtons = 0;
	int x, y, i, s;
	int bx[5], by[5];

	_noSaveLoadAllowed = true;

	_sprites->eraseBoth();
	blitTextbox(m, -1, -1, -1);

	x = _game.window.x1 + 5 * CHAR_COLS / 2;
	y = _game.window.y2 - 5 * CHAR_LINES / 2;
	s = _game.window.x2 - _game.window.x1 + 1 - 5 * CHAR_COLS;
	debugC(3, kDebugLevelText, "selectionBox(): s = %d", s);

	// Automatically position buttons
	for (i = 0; b[i]; i++) {
		numButtons++;
		s -= CHAR_COLS * strlen(b[i]);
	}

	if (i > 1) {
		debugC(3, kDebugLevelText, "selectionBox(): s / %d = %d", i - 1, s / (i - 1));
		s /= (i - 1);
	} else {
		x += s / 2;
	}

	for (i = 0; b[i]; i++) {
		bx[i] = x;
		by[i] = y;
		x += CHAR_COLS * strlen(b[i]) + s;
	}

	_sprites->blitBoth();

	clearKeyQueue();

	AllowSyntheticEvents on(this);

	debugC(4, kDebugLevelText, "selectionBox(): waiting...");
	int key, active = 0;
	int rc = -1;
	while (rc == -1 && !(shouldQuit() || _restartGame)) {
		for (i = 0; b[i]; i++)
			_gfx->drawCurrentStyleButton(bx[i], by[i], b[i], i == active, false, i == 0);

		pollTimer();
		key = doPollKeyboard();
		switch (key) {
		case KEY_ENTER:
			rc = active;
			debugC(4, kDebugLevelText, "selectionBox(): Button pressed: %d", rc);
			break;
		case KEY_RIGHT:
			active++;
			if (active >= numButtons)
				active = 0;
			break;
		case KEY_LEFT:
			active--;
			if (active < 0)
				active = numButtons - 1;
			break;
		case BUTTON_LEFT:
			for (i = 0; b[i]; i++) {
				if (_gfx->testButton(bx[i], by[i], b[i])) {
					rc = active = i;
					debugC(4, kDebugLevelText, "selectionBox(): Button pressed: %d", rc);
					break;
				}
			}
			break;
		case 0x09:	// Tab
			debugC(3, kDebugLevelText, "selectionBox(): Focus change");
			active++;
			active %= i;
			break;
		}
		_gfx->doUpdate();

		if (key == KEY_ESCAPE)
			break;
	}

	closeWindow();
	debugC(2, kDebugLevelText, "selectionBox(): Result = %d", rc);

	_noSaveLoadAllowed = false;

	return rc;
}

/**
 *
 */
int AgiEngine::print(const char *p, int lin, int col, int len) {
	if (p == NULL)
		return 0;

	debugC(4, kDebugLevelText, "print(): lin = %d, col = %d, len = %d", lin, col, len);

	blitTextbox(p, lin, col, len);

	if (getflag(fOutputMode)) {
		// non-blocking window
		setflag(fOutputMode, false);
		return 1;
	}

	// blocking

	_noSaveLoadAllowed = true;

	if (_game.vars[vWindowReset] == 0) {
		int k;
		setvar(vKey, 0);
		k = waitKey();
		closeWindow();

		_noSaveLoadAllowed = false;

		return k;
	}

	// timed window

	debugC(3, kDebugLevelText, "f15==0, v21==%d => timed", getvar(21));
	_game.msgBoxTicks = getvar(vWindowReset) * 10;
	setvar(vKey, 0);

	_menuSelected = false;

	do {
		if (getflag(fRestoreJustRan))
			break;

		if (_menuSelected)
			break;

		mainCycle();
		if (_game.keypress == KEY_ENTER) {
			debugC(4, kDebugLevelText, "KEY_ENTER");
			setvar(vWindowReset, 0);
			_game.keypress = 0;
			break;
		}
	} while (_game.msgBoxTicks > 0 && !(shouldQuit() || _restartGame));

	setvar(vWindowReset, 0);

	closeWindow();

	_noSaveLoadAllowed = false;

	return 0;
}

/**
 *
 */
void AgiEngine::printStatus(const char *message, ...) {
	va_list args;

	va_start(args, message);

	Common::String x = Common::String::vformat(message, args);

	va_end(args);

	debugC(4, kDebugLevelText, "fg=%d, bg=%d", STATUS_FG, STATUS_BG);
	printText(x.c_str(), 0, 0, _game.lineStatus, 40, STATUS_FG, STATUS_BG);
}

static void safeStrcat(Common::String &p, const char *t) {
	if (t != NULL)
		p += t;
}

/**
 * Formats AGI string.
 * This function turns a AGI string into a real string expanding values
 * according to the AGI format specifiers.
 * @param s  string containing the format specifier
 * @param n  logic number
 */
char *AgiEngine::agiSprintf(const char *s) {
	static char agiSprintf_buf[768];
	Common::String p;
	char z[16];

	debugC(3, kDebugLevelText, "logic %d, '%s'", _game.lognum, s);

	while (*s) {
		switch (*s) {
		case '%':
			s++;
			switch (*s++) {
				int i;
			case 'v':
				i = strtoul(s, NULL, 10);
				while (*s >= '0' && *s <= '9')
					s++;
				sprintf(z, "%015i", getvar(i));

				i = 99;
				if (*s == '|') {
					s++;
					i = strtoul(s, NULL, 10);
					while (*s >= '0' && *s <= '9')
						s++;
				}

				if (i == 99) {
					// remove all leading 0
					// don't remove the 3rd zero if 000
					for (i = 0; z[i] == '0' && i < 14; i++)
						;
				} else {
					i = 15 - i;
				}
				safeStrcat(p, z + i);
				break;
			case '0':
				i = strtoul(s, NULL, 10) - 1;
				safeStrcat(p, objectName(i));
				break;
			case 'g':
				i = strtoul(s, NULL, 10) - 1;
				safeStrcat(p, _game.logics[0].texts[i]);
				break;
			case 'w':
				i = strtoul(s, NULL, 10) - 1;
				safeStrcat(p, _game.egoWords[i].word);
				break;
			case 's':
				i = strtoul(s, NULL, 10);
				safeStrcat(p, agiSprintf(_game.strings[i]));
				break;
			case 'm':
				i = strtoul(s, NULL, 10) - 1;
				if (_game.logics[_game.lognum].numTexts > i)
					safeStrcat(p, agiSprintf(_game.logics[_game.lognum].texts[i]));
				break;
			}

			while (*s >= '0' && *s <= '9')
				s++;
			break;

		case '\\':
			s++;
			// FALL THROUGH

		default:
			p += *s++;
			break;
		}
	}

	assert(p.size() < sizeof(agiSprintf_buf));
	strcpy(agiSprintf_buf, p.c_str());
	return agiSprintf_buf;
}

/**
 * Write the status line.
 */
void AgiEngine::writeStatus() {
	char x[64];

	if (_debug.statusline) {
		printStatus("%3d(%03d) %3d,%3d(%3d,%3d)               ",
				getvar(0), getvar(1), _game.viewTable[0].xPos,
				_game.viewTable[0].yPos, WIN_TO_PIC_X(_mouse.x),
				WIN_TO_PIC_Y(_mouse.y));
		return;
	}

	if (!_game.statusLine) {
		clearLines(_game.lineStatus, _game.lineStatus, 0);
		flushLines(_game.lineStatus, _game.lineStatus);

#if 0
		// FIXME: Breaks wrist watch prompt in SQ2

		// Clear the user input line as well when clearing the status line
		// Fixes bug #1893564 - AGI: Texts messed out in Naturette 1
		clearLines(_game.lineUserInput, _game.lineUserInput, 0);
		flushLines(_game.lineUserInput, _game.lineUserInput);
#endif
		return;
	}

	switch (getLanguage()) {
	case Common::RU_RUS:
		sprintf(x, " \x91\xe7\xa5\xe2: %i \xa8\xa7 %-3i", _game.vars[vScore], _game.vars[vMaxScore]);
		printStatus("%-17s              \x87\xa2\xe3\xaa:%s", x, getflag(fSoundOn) ? "\xa2\xaa\xab " : "\xa2\xeb\xaa\xab");
		break;
	default:
		sprintf(x, " Score:%i of %-3i", _game.vars[vScore], _game.vars[vMaxScore]);
		printStatus("%-17s             Sound:%s ", x, getflag(fSoundOn) ? "on " : "off");
		break;
	}
}

/**
 * Print user input prompt.
 */
void AgiEngine::writePrompt() {
	int l, fg, bg, pos;
	int promptLength = strlen(agiSprintf(_game.strings[0]));

	if (!_game.inputEnabled || _game.inputMode != INPUT_NORMAL)
		return;

	l = _game.lineUserInput;
	fg = _game.colorFg;
	bg = _game.colorBg;
	pos = _game.cursorPos;

	debugC(4, kDebugLevelText, "erase line %d", l);
	clearLines(l, l, _game.colorBg);

	debugC(4, kDebugLevelText, "prompt = '%s'", agiSprintf(_game.strings[0]));
	printText(_game.strings[0], 0, 0, l, promptLength + 1, fg, bg);
	printText((char *)_game.inputBuffer, 0, promptLength, l, pos + 1, fg, bg);
	_gfx->printCharacter(pos + promptLength, l, _game.cursorChar, fg, bg);

	flushLines(l, l);
	_gfx->doUpdate();
}

void AgiEngine::clearPrompt(bool useBlackBg) {
	int l;

	l = _game.lineUserInput;
	clearLines(l, l, useBlackBg ? 0 : _game.colorBg);
	flushLines(l, l);

	_gfx->doUpdate();
}

/**
 * Clear text lines in the screen.
 * @param l1  start line
 * @param l2  end line
 * @param c   color
 */
void AgiEngine::clearLines(int l1, int l2, int c) {
	// do we need to adjust for +8 on topline?
	// inc for endline so it matches the correct num
	// ie, from 22 to 24 is 3 lines, not 2 lines.

	debugC(4, kDebugLevelText, "clearLines(%d, %d, %d)", l1, l2, c);

	l1 *= CHAR_LINES;
	l2 *= CHAR_LINES;
	l2 += CHAR_LINES - 1;

	_gfx->drawRectangle(0, l1, GFX_WIDTH - 1, l2, c);
}

/**
 *
 */
void AgiEngine::flushLines(int l1, int l2) {
	l1 *= CHAR_LINES;
	l2 *= CHAR_LINES;
	l2 += CHAR_LINES - 1;

	_gfx->flushBlock(0, l1, GFX_WIDTH - 1, l2);
}

/**
 *
 */
void AgiEngine::drawWindow(int x1, int y1, int x2, int y2) {
	_game.window.active = true;
	_game.window.x1 = x1;
	_game.window.y1 = y1;
	_game.window.x2 = x2;
	_game.window.y2 = y2;
	_game.window.buffer = (uint8 *)malloc((x2 - x1 + 1) * (y2 - y1 + 1));

	debugC(4, kDebugLevelText, "x1=%d, y1=%d, x2=%d, y2=%d", x1, y1, x2, y2);
	_gfx->saveBlock(x1, y1, x2, y2, _game.window.buffer);
	_gfx->drawBox(x1, y1, x2, y2, MSG_BOX_COLOR, MSG_BOX_LINE, 2);
}

} // End of namespace Agi
