/* 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 "common/config-manager.h"
#include "agi/agi.h"
#include "agi/sprite.h"     // for commit_both()
#include "agi/graphics.h"
#include "agi/keyboard.h"
#include "agi/text.h"
#include "agi/systemui.h"
#include "agi/words.h"
#ifdef __DS__
#include "wordcompletion.h"
#endif

namespace Agi {

TextMgr::TextMgr(AgiEngine *vm, Words *words, GfxMgr *gfx) {
	_vm = vm;
	_words = words;
	_gfx = gfx;

	_systemUI = NULL;

	memset(&_messageState, 0, sizeof(_messageState));
	_textPos.row = 0;
	_textPos.column = 0;
	_reset_Column = 0;

	charAttrib_Set(15, 0);

	_messageState.wanted_TextPos.row = -1;
	_messageState.wanted_TextPos.column = -1;
	_messageState.wanted_Text_Width = -1;

	_textPosArrayCount = 0;
	memset(&_textPosArray, 0, sizeof(_textPosArray));
	_textAttribArrayCount = 0;
	memset(&_textAttribArray, 0, sizeof(_textAttribArray));

	_inputEditEnabled = false;
	_inputCursorChar = 0;

	_statusEnabled = false;
	_statusRow = 0;

	_promptRow = 0;
	promptDisable();
	promptReset();

	_inputStringRow = 0;
	_inputStringColumn = 0;
	_inputStringEntered = false;
	_inputStringMaxLen = 0;
	_inputStringCursorPos = 0;
	_inputString[0] = 0;

	configureScreen(2);

	_messageBoxCancelled = false;

	_optionCommandPromptWindow = false;

	if (ConfMan.getBool("commandpromptwindow")) {
		_optionCommandPromptWindow = true;
	}
}

TextMgr::~TextMgr() {
}

void TextMgr::init(SystemUI *systemUI) {
	_systemUI = systemUI;
}

void TextMgr::configureScreen(uint16 row_Min) {
	_window_Row_Min = row_Min;
	_window_Row_Max = row_Min + 21;

	// forward data to GfxMgr as well
	_gfx->setRenderStartOffset(row_Min * FONT_VISUAL_HEIGHT);
}
uint16 TextMgr::getWindowRowMin() {
	return _window_Row_Min;
}

void TextMgr::dialogueOpen() {
	_messageState.dialogue_Open = true;
}

void TextMgr::dialogueClose() {
	_messageState.dialogue_Open = false;
}

void TextMgr::charPos_Clip(int16 &row, int16 &column) {
	row = CLIP<int16>(row, 0, FONT_ROW_CHARACTERS - 1);
	column = CLIP<int16>(column, 0, FONT_COLUMN_CHARACTERS - 1);
}

void TextMgr::charPos_Set(int16 row, int16 column) {
	_textPos.row = row;
	_textPos.column = column;
}

void TextMgr::charPos_Set(TextPos_Struct *posPtr) {
	_textPos.row    = posPtr->row;
	_textPos.column = posPtr->column;
}

void TextMgr::charPos_Get(int16 &row, int16 &column) {
	row = _textPos.row;
	column = _textPos.column;
}

void TextMgr::charPos_Get(TextPos_Struct *posPtr) {
	posPtr->row    = _textPos.row;
	posPtr->column = _textPos.column;
}

void TextMgr::charPos_Push() {
	if (_textPosArrayCount < TEXTPOSARRAY_MAX) {
		charPos_Get(&_textPosArray[_textPosArrayCount]);
		_textPosArrayCount++;
	}
}

void TextMgr::charPos_Pop() {
	if (_textPosArrayCount > 0) {
		_textPosArrayCount--;
		charPos_Set(&_textPosArray[_textPosArrayCount]);
	}
}

void TextMgr::charPos_SetInsideWindow(int16 windowRow, int16 windowColumn) {
	if (!_messageState.window_Active)
		return;

	_textPos.row = _messageState.textPos.row + windowRow;
	_textPos.column = _messageState.textPos.column + windowColumn;
}

static byte charAttrib_CGA_Conversion[] = {
	0, 1, 1, 1, 2, 2, 2, 3, 3, 1, 1, 1, 2, 2, 2
};

void TextMgr::charAttrib_Set(byte foreground, byte background) {
	_textAttrib.foreground = foreground;
	_textAttrib.background = calculateTextBackground(background);

	if (!_vm->_game.gfxMode) {
		// Text-mode:
		// just use the given colors directly
		_textAttrib.combinedForeground = foreground;
		_textAttrib.combinedBackground = background;
	} else {
		// Graphics-mode:
		switch (_vm->_renderMode) {
		case Common::kRenderCGA:
			// CGA
			if (background) {
				_textAttrib.combinedForeground = 3;
				_textAttrib.combinedBackground = 8; // enable invert of colors
			} else {
				if (foreground > 14) {
					_textAttrib.combinedForeground = 3;
				} else {
					_textAttrib.combinedForeground = charAttrib_CGA_Conversion[foreground & 0x0F];
				}
				_textAttrib.combinedBackground = 0;
			}
			break;
		case Common::kRenderHercA:
		case Common::kRenderHercG:
			if (background) {
				_textAttrib.combinedForeground = 0;
				_textAttrib.combinedBackground = 1;
			} else {
				_textAttrib.combinedForeground = 1;
				_textAttrib.combinedBackground = 0;
			}
			break;
		default:
			// EGA-handling:
			if (background) {
				_textAttrib.combinedForeground = 15;
				_textAttrib.combinedBackground = 8; // enable invert of colors
			} else {
				_textAttrib.combinedForeground = foreground;
				_textAttrib.combinedBackground = 0;
			}
			break;
		}
	}
}

byte TextMgr::charAttrib_GetForeground() {
	return _textAttrib.foreground;
}
byte TextMgr::charAttrib_GetBackground() {
	return _textAttrib.background;
}

void TextMgr::charAttrib_Push() {
	if (_textAttribArrayCount < TEXTATTRIBARRAY_MAX) {
		memcpy(&_textAttribArray[_textAttribArrayCount], &_textAttrib, sizeof(_textAttrib));
		_textAttribArrayCount++;
	}
}

void TextMgr::charAttrib_Pop() {
	if (_textAttribArrayCount > 0) {
		_textAttribArrayCount--;
		memcpy(&_textAttrib, &_textAttribArray[_textAttribArrayCount], sizeof(_textAttrib));
	}
}

byte TextMgr::calculateTextBackground(byte background) {
	if ((_vm->_game.gfxMode) && (background)) {
		return 15; // interpreter sets 0xFF, but drawClearCharacter() would use upper 4 bits by shift
	}
	return 0;
}

void TextMgr::display(int16 textNr, int16 textRow, int16 textColumn) {
	const char *logicTextPtr = NULL;
	char *processedTextPtr   = NULL;

	charPos_Push();
	charPos_Set(textRow, textColumn);

	if (textNr >= 1 && textNr <= _vm->_game._curLogic->numTexts) {
		logicTextPtr = _vm->_game._curLogic->texts[textNr - 1];
		processedTextPtr = stringPrintf(logicTextPtr);
		processedTextPtr = stringWordWrap(processedTextPtr, 40);
		displayText(processedTextPtr);

		// Signal, that non-blocking text is shown at the moment
		if (textRow > 0) {
			// only signal, when it's not the status line (kq3)
			_vm->nonBlockingText_IsShown();
		}
	}
	charPos_Pop();
}

void TextMgr::displayTextInsideWindow(const char *textPtr, int16 windowRow, int16 windowColumn) {
	int16 textRow = 0;
	int16 textColumn = 0;

	if (!_messageState.window_Active)
		return;

	charPos_Push();
	textRow = _messageState.textPos.row + windowRow;
	textColumn = _messageState.textPos.column + windowColumn;
	charPos_Set(textRow, textColumn);
	displayText(textPtr);
	charPos_Pop();
}

void TextMgr::displayText(const char *textPtr, bool disabledLook) {
	const char *curTextPtr = textPtr;
	byte  curCharacter = 0;

	while (1) {
		curCharacter = *curTextPtr;
		if (!curCharacter)
			break;

		curTextPtr++;
		displayCharacter(curCharacter, disabledLook);
	}
}

void TextMgr::displayCharacter(byte character, bool disabledLook) {
	TextPos_Struct charCurPos;

	charPos_Get(&charCurPos);

	switch (character) {
	case 0x08: // backspace
		if (charCurPos.column) {
			charCurPos.column--;
		} else if (charCurPos.row > 21) {
			charCurPos.column = (FONT_COLUMN_CHARACTERS - 1);
			charCurPos.row--;
		}
		clearBlock(charCurPos.row, charCurPos.column, charCurPos.row, charCurPos.column, _textAttrib.background);
		charPos_Set(&charCurPos);
		break;

	case 0x0D:
	case 0x0A: // CR/LF
		if (charCurPos.row < (FONT_ROW_CHARACTERS - 1))
			charCurPos.row++;
		charCurPos.column = _reset_Column;
		charPos_Set(&charCurPos);
		break;
	default:
		// ch_attrib(state.text_comb, conversion);
		_gfx->drawCharacter(charCurPos.row, charCurPos.column, character, _textAttrib.combinedForeground, _textAttrib.combinedBackground, disabledLook);

		charCurPos.column++;
		if (charCurPos.column <= (FONT_COLUMN_CHARACTERS - 1)) {
			charPos_Set(&charCurPos);
		} else {
			displayCharacter(0x0D); // go to next line
		}
	}
}

void TextMgr::print(int16 textNr) {
	const char *logicTextPtr = NULL;
	if (textNr >= 1 && textNr <= _vm->_game._curLogic->numTexts) {
		logicTextPtr = _vm->_game._curLogic->texts[textNr - 1];
		messageBox(logicTextPtr);
	}
}

void TextMgr::printAt(int16 textNr, int16 textPos_Row, int16 textPos_Column, int16 text_Width) {
	// Sierra didn't do clipping, we do it for security
	charPos_Clip(textPos_Row, textPos_Column);

	_messageState.wanted_TextPos.row = textPos_Row;
	_messageState.wanted_TextPos.column = textPos_Column;
	_messageState.wanted_Text_Width = text_Width;

	if (_messageState.wanted_Text_Width == 0) {
		_messageState.wanted_Text_Width = 30;
	}
	print(textNr);

	_messageState.wanted_TextPos.row = -1;
	_messageState.wanted_TextPos.column = -1;
	_messageState.wanted_Text_Width = -1;
}

bool TextMgr::messageBox(const char *textPtr) {
	drawMessageBox(textPtr);

	if (_vm->getFlag(VM_FLAG_OUTPUT_MODE)) {
		// non-blocking window
		_vm->setFlag(VM_FLAG_OUTPUT_MODE, false);

		// Signal, that non-blocking text is shown at the moment
		_vm->nonBlockingText_IsShown();
		return true;
	}

	// blocking window
	_vm->_noSaveLoadAllowed = true;
	_vm->nonBlockingText_Forget();

	// timed window
	uint32 windowTimer = _vm->getVar(VM_VAR_WINDOW_AUTO_CLOSE_TIMER);
	debugC(3, kDebugLevelText, "blocking window v21=%d", windowTimer);

	windowTimer = windowTimer * 10; // 1 = 0.5 seconds
	_messageBoxCancelled = false;

	_vm->inGameTimerResetPassedCycles();
	_vm->cycleInnerLoopActive(CYCLE_INNERLOOP_MESSAGEBOX);
	do {
		_vm->processAGIEvents();
		_vm->inGameTimerUpdate();

		if (windowTimer > 0) {
			if (_vm->inGameTimerGetPassedCycles() >= windowTimer) {
				// Timer reached, close automatically
				_vm->cycleInnerLoopInactive();
			}
		}
	} while (_vm->cycleInnerLoopIsActive() && !(_vm->shouldQuit() || _vm->_restartGame));

	_vm->inGameTimerResetPassedCycles();

	_vm->setVar(VM_VAR_WINDOW_AUTO_CLOSE_TIMER, 0);

	closeWindow();
	_vm->_noSaveLoadAllowed = false;

	if (_messageBoxCancelled)
		return false;
	return true;
}

void TextMgr::messageBox_KeyPress(uint16 newKey) {
	switch (newKey) {
	case AGI_KEY_ENTER:
		_vm->cycleInnerLoopInactive(); // exit messagebox-loop
		break;
	case AGI_KEY_ESCAPE:
		_messageBoxCancelled = true;
		_vm->cycleInnerLoopInactive(); // exit messagebox-loop
		break;
	case AGI_MOUSE_BUTTON_LEFT: {
		// Check, if mouse cursor is within message box
		// If it is, take the click as ENTER.
		// That's what AGI on Amiga + Apple IIgs did.
		// On Atari ST at least via emulator it seems that the mouse cursor froze when messageboxes were diplayed.
		if (isMouseWithinMessageBox()) {
			_vm->cycleInnerLoopInactive(); // exit messagebox-loop
		}
		break;
	}
	default:
		break;
	}
}

void TextMgr::drawMessageBox(const char *textPtr, int16 forcedHeight, int16 wantedWidth, bool forcedWidth) {
	int16 maxWidth = wantedWidth;
	int16 startingRow = 0;
	char *processedTextPtr;

	if (_messageState.window_Active) {
		closeWindow();
	}
	charAttrib_Push();
	charPos_Push();
	charAttrib_Set(0, 15);

	if ((_messageState.wanted_Text_Width == -1) && (maxWidth == 0)) {
		maxWidth = 30;
	} else if (_messageState.wanted_Text_Width != -1) {
		maxWidth = _messageState.wanted_Text_Width;
	}

	processedTextPtr = stringPrintf(textPtr);

	int16 calculatedWidth = 0;
	int16 calculatedHeight = 0;

	processedTextPtr = stringWordWrap(processedTextPtr, maxWidth, &calculatedWidth, &calculatedHeight);
	_messageState.textSize_Width  = calculatedWidth;
	_messageState.textSize_Height = calculatedHeight;

	_messageState.printed_Height = _messageState.textSize_Height;

	// Caller wants to force specified width/height? set it
	if (forcedHeight)
		_messageState.textSize_Height = forcedHeight;

	if (forcedWidth) {
		if (wantedWidth)
			_messageState.textSize_Width = wantedWidth;
	}

	if (_messageState.wanted_TextPos.row == -1) {
		startingRow = ((HEIGHT_MAX - _messageState.textSize_Height - 1) / 2) + 1;
	} else {
		startingRow = _messageState.wanted_TextPos.row;
	}
	_messageState.textPos.row = startingRow + _window_Row_Min;
	_messageState.textPos_Edge.row = _messageState.textSize_Height + _messageState.textPos.row - 1;

	if (_messageState.wanted_TextPos.column == -1) {
		_messageState.textPos.column = (FONT_COLUMN_CHARACTERS - _messageState.textSize_Width) / 2;
	} else {
		_messageState.textPos.column = _messageState.wanted_TextPos.column;
	}
	_messageState.textPos_Edge.column = _messageState.textPos.column + _messageState.textSize_Width;

	charPos_Set(_messageState.textPos.row, _messageState.textPos.column);

	_messageState.backgroundSize_Width = (_messageState.textSize_Width * FONT_VISUAL_WIDTH) + 10;
	_messageState.backgroundSize_Height = (_messageState.textSize_Height * FONT_VISUAL_HEIGHT) + 10;
	_messageState.backgroundPos_x = (_messageState.textPos.column * FONT_VISUAL_WIDTH) - 5;
	_messageState.backgroundPos_y = (startingRow * FONT_VISUAL_HEIGHT) - 5;
	// original AGI used lowerY here, calculated using (_messageState.textPos_Edge.row - _window_Row_Min + 1) * FONT_VISUAL_HEIGHT + 4;

	// Hardcoded colors: white background and red lines
	_gfx->drawBox(_messageState.backgroundPos_x, _messageState.backgroundPos_y, _messageState.backgroundSize_Width, _messageState.backgroundSize_Height, 15, 4);

	_messageState.window_Active = true;

	_reset_Column = _messageState.textPos.column;
	displayText(processedTextPtr);
	_reset_Column = 0;

	charPos_Pop();
	charAttrib_Pop();

	_messageState.dialogue_Open = true;
}

void TextMgr::getMessageBoxInnerDisplayDimensions(int16 &x, int16 &y, int16 &width, int16 &height) {
	if (!_messageState.window_Active)
		return;

	y = _messageState.textPos.row;
	x = _messageState.textPos.column;
	width = _messageState.textSize_Width;
	height = _messageState.textSize_Height;
	_gfx->translateFontRectToDisplayScreen(x, y, width, height);
}

bool TextMgr::isMouseWithinMessageBox() {
	// Find out, where current mouse cursor actually is
	int16 mouseY = _vm->_mouse.pos.y;
	int16 mouseX = _vm->_mouse.pos.x;

	if (_messageState.window_Active) {
		_gfx->translateDisplayPosToGameScreen(mouseX, mouseY);

		if ((mouseX >= _messageState.backgroundPos_x) && (mouseX < (_messageState.backgroundPos_x + _messageState.backgroundSize_Width))) {
			if ((mouseY >= _messageState.backgroundPos_y) && (mouseY < (_messageState.backgroundPos_y + _messageState.backgroundSize_Height))) {
				return true;
			}
		}
	}
	return false;
}

void TextMgr::closeWindow() {
	if (_messageState.window_Active) {
		_gfx->render_Block(_messageState.backgroundPos_x, _messageState.backgroundPos_y, _messageState.backgroundSize_Width, _messageState.backgroundSize_Height);
	}
	_messageState.dialogue_Open = false;
	_messageState.window_Active = false;
}

void TextMgr::statusRow_Set(int16 row) {
	_statusRow = row;
}
int16 TextMgr::statusRow_Get() {
	return _statusRow;
}

void TextMgr::statusEnable() {
	_statusEnabled = true;
}
void TextMgr::statusDisable() {
	_statusEnabled = false;
}
bool TextMgr::statusEnabled() {
	return _statusEnabled;
}

void TextMgr::statusDraw() {
	char *statusTextPtr = NULL;

	charAttrib_Push();
	charPos_Push();

	if (_statusEnabled) {
		clearLine(_statusRow, 15);

		charAttrib_Set(0, 15);
		charPos_Set(_statusRow, 1);
		statusTextPtr = stringPrintf(_systemUI->getStatusTextScore());
		displayText(statusTextPtr);

		charPos_Set(_statusRow, 30);
		if (_vm->getFlag(VM_FLAG_SOUND_ON)) {
			statusTextPtr = stringPrintf(_systemUI->getStatusTextSoundOn());
		} else {
			statusTextPtr = stringPrintf(_systemUI->getStatusTextSoundOff());
		}
		displayText(statusTextPtr);
	}

	charPos_Pop();
	charAttrib_Pop();
}

void TextMgr::statusClear() {
	clearLine(_statusRow, 0);
}

void TextMgr::clearLine(int16 row, byte color) {
	clearLines(row, row, color);
}

void TextMgr::clearLines(int16 row_Upper, int16 row_Lower, byte color) {
	clearBlock(row_Upper, 0, row_Lower, FONT_COLUMN_CHARACTERS - 1, color);
}

void TextMgr::clearBlock(int16 row_Upper, int16 column_Upper, int16 row_Lower, int16 column_Lower, byte color) {
	// Sierra didn't do clipping of the coordinates, we do it for security
	//  and b/c there actually are some games, that call commands with invalid coordinates
	//  see cmdClearLines() comments.
	charPos_Clip(row_Upper, column_Upper);
	charPos_Clip(row_Lower, column_Lower);

	int16 x = column_Upper;
	int16 y = row_Upper;
	int16 width = (column_Lower + 1 - column_Upper);
	int16 height = (row_Lower + 1 - row_Upper);
	_gfx->translateFontRectToDisplayScreen(x, y, width, height);

	_gfx->drawDisplayRect(x, y, width, height, color);
}

void TextMgr::clearBlockInsideWindow(int16 windowRow, int16 windowColumn, int16 width, byte color) {
	int16 row;
	int16 column;
	if (!_messageState.window_Active)
		return;

	row = _messageState.textPos.row + windowRow;
	column = _messageState.textPos.column + windowColumn;
	clearBlock(row, column, row, column + width - 1, color);
}

bool TextMgr::inputGetEditStatus() {
	return _inputEditEnabled;
}

void TextMgr::inputEditOn() {
	if (!_inputEditEnabled) {
		_inputEditEnabled = true;
		if (_inputCursorChar) {
			displayCharacter(0x08); // backspace
		}
	}
}

void TextMgr::inputEditOff() {
	if (_inputEditEnabled) {
		_inputEditEnabled = false;
		if (_inputCursorChar) {
			displayCharacter(_inputCursorChar);
		}
	}
}

void TextMgr::inputSetCursorChar(int16 cursorChar) {
	_inputCursorChar = cursorChar;
}

byte TextMgr::inputGetCursorChar() {
	return _inputCursorChar;
}

void TextMgr::promptRow_Set(int16 row) {
	_promptRow = row;
}

int16 TextMgr::promptRow_Get() {
	return _promptRow;
}

void TextMgr::promptReset() {
	_promptCursorPos = 0;
	memset(_prompt, 0, sizeof(_prompt));
	memset(_promptPrevious, 0, sizeof(_promptPrevious));
}

void TextMgr::promptEnable() {
	_promptEnabled = true;
}
void TextMgr::promptDisable() {
	_promptEnabled = false;
}
bool TextMgr::promptIsEnabled() {
	return _promptEnabled;
}

void TextMgr::promptKeyPress(uint16 newKey) {
	int16 maxChars = 0;
	int16 scriptsInputLen = _vm->getVar(VM_VAR_MAX_INPUT_CHARACTERS);

	bool acceptableInput = false;

	// FEATURE: Sierra didn't check for valid characters (filtered out umlauts etc.)
	// In text-mode this sort of worked at least with the DOS interpreter
	// but as soon as invalid characters were used in graphics mode they weren't properly shown
	switch (_vm->getLanguage()) {
	case Common::RU_RUS:
		if (newKey >= 0x20)
			acceptableInput = true;
		break;
	default:
		if ((newKey >= 0x20) && (newKey <= 0x7f))
			acceptableInput = true;
		break;
	}

	if (_optionCommandPromptWindow) {
		// Forward to command prompt window, using last command
		if (acceptableInput) {
			promptCommandWindow(false, newKey);
		}
		return;
	}

	if (_messageState.dialogue_Open) {
		maxChars = TEXT_STRING_MAX_SIZE - 4;
	} else {
		maxChars = TEXT_STRING_MAX_SIZE - strlen(_vm->_game.strings[0]); // string 0 is the prompt string prefix
	}

	if (_promptCursorPos)
		maxChars--;

	if (scriptsInputLen < maxChars)
		maxChars = scriptsInputLen;

	inputEditOn();

	switch (newKey) {
	case AGI_KEY_BACKSPACE: {
		if (_promptCursorPos) {
			_promptCursorPos--;
			_prompt[_promptCursorPos] = 0;
			displayCharacter(newKey);

			promptRememberForAutoComplete();
		}
		break;
	}
	case 0x0A: // LF
		break;
	case AGI_KEY_ENTER: {
		if (_promptCursorPos) {
			// something got entered? -> process it and pass it to the scripts
			promptRememberForAutoComplete(true);

			memcpy(&_promptPrevious, &_prompt, sizeof(_prompt));
			// parse text
			_vm->_words->parseUsingDictionary((char *)&_prompt);

			_promptCursorPos = 0;
			_prompt[0] = 0;
			promptRedraw();
		}
		break;
	}
	default:
		if (maxChars > _promptCursorPos) {
			if (acceptableInput) {
				_prompt[_promptCursorPos] = newKey;
				_promptCursorPos++;
				_prompt[_promptCursorPos] = 0;
				displayCharacter(newKey);

				promptRememberForAutoComplete();
			}
		}
		break;
	}

	inputEditOff();
}

void TextMgr::promptCancelLine() {
	if (_optionCommandPromptWindow) {
		// Abort, in case command prompt window is active
		return;
	}

	while (_promptCursorPos) {
		promptKeyPress(0x08); // Backspace until prompt is empty
	}
}

void TextMgr::promptEchoLine() {
	int16 previousLen = strlen((char *)_promptPrevious);

	if (_optionCommandPromptWindow) {
		// Forward to command prompt window, using last command
		promptCommandWindow(true, 0);
		return;
	}

	if (_promptCursorPos < previousLen) {
		inputEditOn();

		while (_promptPrevious[_promptCursorPos]) {
			promptKeyPress(_promptPrevious[_promptCursorPos]);
		}
		promptRememberForAutoComplete();

		inputEditOff();
	}
}

void TextMgr::promptRedraw() {
	char *textPtr = nullptr;

	if (_promptEnabled) {
		if (_optionCommandPromptWindow) {
			// Abort, in case command prompt window is active
			return;
		}

		inputEditOn();
		clearLine(_promptRow, _textAttrib.background);
		charPos_Set(_promptRow, 0);
		// agi_printf(str_wordwrap(msg, state.string[0], 40) );

		textPtr = _vm->_game.strings[0];
		textPtr = stringPrintf(textPtr);
		textPtr = stringWordWrap(textPtr, 40);

		displayText(textPtr);
		displayText((char *)&_prompt);
		inputEditOff();
	}
}

// for AGI1
void TextMgr::promptClear() {
	if (_optionCommandPromptWindow) {
		// Abort, in case command prompt window is active
		return;
	}
	clearLine(_promptRow, _textAttrib.background);
}

void TextMgr::promptRememberForAutoComplete(bool entered) {
#ifdef __DS__
	DS::findWordCompletions((char *)_prompt);
#endif
}

void TextMgr::promptCommandWindow(bool recallLastCommand, uint16 newKey) {
	Common::String commandText;

	if (recallLastCommand) {
		commandText += Common::String((char *)_promptPrevious);
	}
	if (newKey) {
		if (newKey != ' ') {
			// Only add char, when it's not a space.
			// Original AGI did not filter space, but it makes no sense to start with a space.
			// Space would get filtered anyway during dictionary parsing.
			commandText += newKey;
		}
	}

	if (_systemUI->askForCommand(commandText)) {
		if (commandText.size()) {
			// Something actually was entered?
			strncpy((char *)&_prompt, commandText.c_str(), sizeof(_prompt));
			promptRememberForAutoComplete(true);
			memcpy(&_promptPrevious, &_prompt, sizeof(_prompt));
			// parse text
			_vm->_words->parseUsingDictionary((char *)&_prompt);

			_prompt[0] = 0;
		}
	}
}

bool TextMgr::stringWasEntered() {
	return _inputStringEntered;
}

void TextMgr::stringSet(const char *text) {
	strncpy((char *)_inputString, text, sizeof(_inputString));
	_inputString[sizeof(_inputString) - 1] = 0; // terminator
}

void TextMgr::stringPos_Get(int16 &row, int16 &column) {
	row = _inputStringRow;
	column = _inputStringColumn;
}
int16 TextMgr::stringGetMaxLen() {
	return _inputStringMaxLen;
}

void TextMgr::stringEdit(int16 stringMaxLen) {
	int16 inputStringLen = strlen((const char *)_inputString);

	// Remember current position for predictive dialog
	_inputStringRow = _textPos.row;
	_inputStringColumn = _textPos.column;

	if (_inputCursorChar) {
		// Cursor character is shown, which means we are one beyond the start of the input
		// Adjust the column for predictive input dialog
		_inputStringColumn--;
	}

	// Caller can set the input string
	_inputStringCursorPos = 0;
	while (_inputStringCursorPos < inputStringLen) {
		displayCharacter(_inputString[_inputStringCursorPos]);
		_inputStringCursorPos++;
	}

	// should never happen unless there is a coding glitch
	assert(_inputStringCursorPos <= stringMaxLen);

	_inputStringMaxLen = stringMaxLen;
	_inputStringEntered = false;

	inputEditOff();

	do {
		_vm->processAGIEvents();
	} while (_vm->cycleInnerLoopIsActive() && !(_vm->shouldQuit() || _vm->_restartGame));

	inputEditOn();

	// Forget non-blocking text, user was asked to enter something
	_vm->nonBlockingText_Forget();
}

void TextMgr::stringKeyPress(uint16 newKey) {
	inputEditOn();

	switch (newKey) {
	case 0x3:       // ctrl-c
	case 0x18: {    // ctrl-x
		// clear string
		while (_inputStringCursorPos) {
			_inputStringCursorPos--;
			_inputString[_inputStringCursorPos] = 0;
			displayCharacter(0x08);
		}
		break;
	}

	case AGI_KEY_BACKSPACE: {
		if (_inputStringCursorPos) {
			_inputStringCursorPos--;
			_inputString[_inputStringCursorPos] = 0;
			displayCharacter(newKey);

			stringRememberForAutoComplete();
		}
		break;
	}

	case AGI_KEY_ENTER: {
		stringRememberForAutoComplete(true);

		_inputStringEntered = true;

		_vm->cycleInnerLoopInactive(); // exit GetString-loop
		break;
	}

	case AGI_KEY_ESCAPE: {
		_inputString[0] = 0;
		_inputStringCursorPos = 0;
		_inputStringEntered = false;

		_vm->cycleInnerLoopInactive(); // exit GetString-loop
		break;
	}

	default:
		if (_inputStringMaxLen > _inputStringCursorPos) {
			bool acceptableInput = false;

			// FEATURE: Sierra didn't check for valid characters (filtered out umlauts etc.)
			// In text-mode this sort of worked at least with the DOS interpreter
			// but as soon as invalid characters were used in graphics mode they weren't properly shown
			switch (_vm->getLanguage()) {
			case Common::RU_RUS:
				if (newKey >= 0x20)
					acceptableInput = true;
				break;
			default:
				if ((newKey >= 0x20) && (newKey <= 0x7f))
					acceptableInput = true;
				break;
			}

			if (acceptableInput) {
				if ((_vm->_game.cycleInnerLoopType == CYCLE_INNERLOOP_GETSTRING) || ((newKey >= '0') && (newKey <= '9'))) {
					// Additionally check for GETNUMBER-mode, if character is a number
					// Sierra also did not do this
					_inputString[_inputStringCursorPos] = newKey;
					_inputStringCursorPos++;
					_inputString[_inputStringCursorPos] = 0;
					displayCharacter(newKey);

					stringRememberForAutoComplete();
				}
			}
		}
		break;
	}

	inputEditOff();
}

void TextMgr::stringRememberForAutoComplete(bool entered) {
#ifdef __DS__
	DS::findWordCompletions((char *)_inputString);
#endif
}

/**
 * Wraps text line to the specified width.
 * @param originalText  String to wrap.
 * @param maxWidth      Length of line.
 */
char *TextMgr::stringWordWrap(const char *originalText, int16 maxWidth, int16 *calculatedWidthPtr, int16 *calculatedHeightPtr) {
	static char resultWrappedBuffer[2000];
	int16 boxWidth = 0;
	int16 boxHeight = 0;
	int16 lineWidth = 0; // width of current line

	int16 lineWidthLeft = maxWidth; // width left of current line

	int16 wordStartPos = 0;
	int16 wordLen = 0;
	int16 curReadPos = 0;
	int16 curWritePos = 0;
	byte  wordEndChar = 0;

	//memset(resultWrappedBuffer, 0, sizeof(resultWrappedBuffer)); for debugging

	// Good testcases:
	// King's Quest 1 intro:              the scrolling text is filled up with spaces, so that old lines are erased
	// Apple IIgs restart system UI:      spaces used to make the window larger
	// Gold Rush Stagecoach path room 60: "  Lake Michigan!", with max length 9 -> should get split into "  Lake" / "Michigan!"

	while (originalText[curReadPos]) {
		// Try to find out length of next word

		// If first character is a space, skip it, so that we process at least this space
		if (originalText[curReadPos] == ' ')
			curReadPos++;

		while (originalText[curReadPos]) {
			if (originalText[curReadPos] == ' ')
				break;
			if (originalText[curReadPos] == 0x0A)
				break;
			curReadPos++;
		}
		wordEndChar = originalText[curReadPos];

		// Calculate word length
		wordLen = curReadPos - wordStartPos;

		if (wordLen >= lineWidthLeft) {
			// Not enough space left

			// If first character right after the new line is a space, skip over it
			if (wordLen) {
				if (originalText[wordStartPos] == ' ') {
					wordStartPos++;
					wordLen--;
				}
			}

			if (wordLen > maxWidth) {
				// Word way too long, split it in half
				curReadPos = curReadPos - (wordLen - maxWidth);
				wordLen = maxWidth;
			}

			// Add new line
			resultWrappedBuffer[curWritePos++] = 0x0A;
			if (lineWidth > boxWidth)
				boxWidth = lineWidth;
			boxHeight++; lineWidth = 0;
			lineWidthLeft = maxWidth;

			// Reached absolute maximum? -> exit now
			if (boxHeight >= HEIGHT_MAX)
				break;
		}

		// Copy current word over
		memcpy(&resultWrappedBuffer[curWritePos], &originalText[wordStartPos], wordLen);
		lineWidth += wordLen;
		lineWidthLeft -= wordLen;
		curWritePos += wordLen;

		if (wordEndChar == 0x0A) {
			// original text had a new line, so force it
			curReadPos++;

			resultWrappedBuffer[curWritePos++] = 0x0A;
			if (lineWidth > boxWidth)
				boxWidth = lineWidth;
			boxHeight++; lineWidth = 0;
			lineWidthLeft = maxWidth;

			// Reached absolute maximum? -> exit now
			if (boxHeight >= HEIGHT_MAX)
				break;
		}

		wordStartPos = curReadPos;
	}

	resultWrappedBuffer[curWritePos] = 0;

	if (curReadPos > 0) {
		if (lineWidth > boxWidth)
			boxWidth = lineWidth;
		boxHeight++;
	}

	if (calculatedWidthPtr) {
		*calculatedWidthPtr = boxWidth;
	}
	if (calculatedHeightPtr) {
		*calculatedHeightPtr = boxHeight;
	}
	return resultWrappedBuffer;
}

// ===============================================================

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 *TextMgr::stringPrintf(const char *originalText) {
	static char resultPrintfBuffer[2000];
	Common::String resultString;
	char z[16];

	debugC(3, kDebugLevelText, "logic %d, '%s'", _vm->_game.curLogicNr, originalText);

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

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

				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(resultString, z + i);
				break;
			case '0':
				i = strtoul(originalText, NULL, 10) - 1;
				safeStrcat(resultString, _vm->objectName(i));
				break;
			case 'g':
				i = strtoul(originalText, NULL, 10) - 1;
				safeStrcat(resultString, _vm->_game.logics[0].texts[i]);
				break;
			case 'w':
				i = strtoul(originalText, NULL, 10) - 1;
				safeStrcat(resultString, _vm->_words->getEgoWord(i));
				break;
			case 's':
				i = strtoul(originalText, NULL, 10);
				safeStrcat(resultString, stringPrintf(_vm->_game.strings[i]));
				break;
			case 'm':
				i = strtoul(originalText, NULL, 10) - 1;
				if (_vm->_game.logics[_vm->_game.curLogicNr].numTexts > i)
					safeStrcat(resultString, stringPrintf(_vm->_game.logics[_vm->_game.curLogicNr].texts[i]));
				break;
			}

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

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

		default:
			resultString += *originalText++;
			break;
		}
	}

	assert(resultString.size() < sizeof(resultPrintfBuffer));
	Common::strlcpy(resultPrintfBuffer, resultString.c_str(), 2000);
	return resultPrintfBuffer;
}

} // End of namespace Agi