/* 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.
 *
 */

/*
 * This code is based on the original source code of Lord Avalot d'Argent version 1.3.
 * Copyright (c) 1994-1995 Mike, Mark and Thomas Thurman.
 */

 /* SCROLLS		The scroll driver. */

#include "avalanche/avalanche.h"
#include "avalanche/dialogs.h"

#include "common/random.h"

namespace Avalanche {

const Dialogs::TuneType Dialogs::kTune = {
	kPitchHigher, kPitchHigher, kPitchLower, kPitchSame, kPitchHigher, kPitchHigher, kPitchLower, kPitchHigher, kPitchHigher, kPitchHigher,
	kPitchLower, kPitchHigher, kPitchHigher, kPitchSame, kPitchHigher, kPitchLower, kPitchLower, kPitchLower, kPitchLower, kPitchHigher,
	kPitchHigher, kPitchLower, kPitchLower, kPitchLower, kPitchLower, kPitchSame, kPitchLower, kPitchHigher, kPitchSame, kPitchLower, kPitchHigher
};

// A quasiped defines how people who aren't sprites talk. For example, quasiped
// "A" is Dogfood. The rooms aren't stored because I'm leaving that to context.
const QuasipedType Dialogs::kQuasipeds[16] = {
	//_whichPed, _foregroundColor,   _room,      _backgroundColor,     _who
	{1, kColorLightgray,    kRoomArgentPub,    kColorBrown,    kPeopleDogfood},   // A: Dogfood (screen 19).
	{2, kColorGreen,        kRoomArgentPub,    kColorWhite,    kPeopleIbythneth}, // B: Ibythneth (screen 19).
	{2, kColorWhite,        kRoomYours,        kColorMagenta,  kPeopleArkata},    // C: Arkata (screen 1).
	{2, kColorBlack,        kRoomLustiesRoom,  kColorRed,      kPeopleInvisible}, // D: Hawk (screen 23).
	{2, kColorLightgreen,   kRoomOutsideDucks, kColorBrown,    kPeopleTrader},    // E: Trader (screen 50).
	{5, kColorYellow,       kRoomRobins,       kColorRed,      kPeopleAvalot},    // F: Avvy, tied up (scr.42)
	{1, kColorBlue,         kRoomAylesOffice,  kColorWhite,    kPeopleAyles},     // G: Ayles (screen 16).
	{1, kColorBrown,        kRoomMusicRoom,    kColorWhite,    kPeopleJacques},   // H: Jacques (screen 7).
	{1, kColorLightgreen,   kRoomNottsPub,     kColorGreen,    kPeopleSpurge},    // I: Spurge (screen 47).
	{2, kColorYellow,       kRoomNottsPub,     kColorRed,      kPeopleAvalot},    // J: Avalot (screen 47).
	{1, kColorLightgray,    kRoomLustiesRoom,  kColorBlack,    kPeopleDuLustie},  // K: du Lustie (screen 23).
	{1, kColorYellow,       kRoomOubliette,    kColorRed,      kPeopleAvalot},    // L: Avalot (screen 27).
	{2, kColorWhite,        kRoomOubliette,    kColorRed,      kPeopleInvisible}, // M: Avaroid (screen 27).
	{3, kColorLightgray,    kRoomArgentPub,    kColorDarkgray, kPeopleMalagauche},// N: Malagauche (screen 19).
	{4, kColorLightmagenta, kRoomNottsPub,     kColorRed,      kPeoplePort},      // O: Port (screen 47).
	{1, kColorLightgreen,   kRoomDucks,        kColorDarkgray, kPeopleDrDuck}     // P: Duck (screen 51).
};

Dialogs::Dialogs(AvalancheEngine *vm) {
	_vm = vm;
	_noError = true;

	_aboutBox = false;
	_talkX = 0;
	_talkY = 0;
	_maxLineNum = 0;
	_scReturn = false;
	_currentFont = kFontStyleRoman;
	_param = 0;
	_useIcon = 0;
	_scrollBells = 0;
	_underScroll = 0;
	_shadowBoxX = 0;
	_shadowBoxY = 0;
}

void Dialogs::init() {
	loadFont();
	resetScrollDriver();
}

/**
 * Determine the color of the ready light and draw it
 * @remarks	Originally called 'state'
 */
void Dialogs::setReadyLight(byte state) {
	if (_vm->_ledStatus == state)
		return; // Already like that!

	// TODO: Implement different patterns for green color.
	Color color = kColorBlack;
	switch (state) {
	case 0:
		color = kColorBlack;
		break; // Off
	case 1:
	case 2:
	case 3:
		color = kColorGreen;
		break; // Hit a key
	}

	_vm->_graphics->drawReadyLight(color);
	CursorMan.showMouse(true);
	_vm->_ledStatus = state;
}

void Dialogs::easterEgg() {
	warning("STUB: Scrolls::easterEgg()");
}

void Dialogs::say(int16 x, int16 y, Common::String z) {
	FontType itw;
	byte lz = z.size();

	bool offset = x % 8 == 4;
	x /= 8;
	y++;
	int16 i = 0;
	for (int xx = 0; xx < lz; xx++) {
		switch (z[xx]) {
		case kControlRoman:
			_currentFont = kFontStyleRoman;
			break;
		case kControlItalic:
			_currentFont = kFontStyleItalic;
			break;
		default: {
			for (int yy = 0; yy < 12; yy++)
				itw[(byte)z[xx]][yy] = _fonts[_currentFont][(byte)z[xx]][yy + 2];

			// We have to draw the characters one-by-one because of the accidental font changes.
			i++;
			Common::String chr(z[xx]);
			_vm->_graphics->drawScrollText(chr, itw, 12, (x - 1) * 8 + offset * 4 + i * 8, y, kColorBlack);
			}
		}
	}
}

/**
 * One of the 3 "Mode" functions passed as ScrollsFunctionType parameters.
 * @remarks	Originally called 'normscroll'
 */
void Dialogs::scrollModeNormal() {
	// Original code is:
	// egg : array[1..8] of char = ^P^L^U^G^H+'***';
	// this is not using kControl characters: it's the secret code to be entered to trigger the easter egg
	// TODO: To be fixed when the Easter egg code is implemented
	Common::String egg = Common::String::format("%c%c%c%c%c***", kControlParagraph, kControlLeftJustified, kControlNegative, kControlBell, kControlBackspace);
	Common::String e = "(c) 1994";

	setReadyLight(3);
	_vm->_animationsEnabled = false;
	_vm->_graphics->loadMouse(kCurFletch);

	_vm->_graphics->saveScreen();
	_vm->_graphics->showScroll();

	Common::Event event;
	bool escape = false;
	while (!_vm->shouldQuit() && !escape) {
		_vm->_graphics->refreshScreen();
		while (_vm->getEvent(event)) {
			if ((event.type == Common::EVENT_LBUTTONUP) ||
				((event.type == Common::EVENT_KEYDOWN) && ((event.kbd.keycode == Common::KEYCODE_ESCAPE) ||
				(event.kbd.keycode == Common::KEYCODE_RETURN) || (event.kbd.keycode == Common::KEYCODE_HASH) ||
				(event.kbd.keycode == Common::KEYCODE_PLUS)))) {
				escape = true;
				break;
			} else if (event.type == Common::EVENT_KEYDOWN)
				_vm->errorLed();
		}
	}

	_vm->_graphics->restoreScreen();
	_vm->_graphics->removeBackup();

	warning("STUB: scrollModeNormal() - Check Easter Egg trigger");
#if 0
	char r;
	bool oktoexit;
	do {
		do {
			_vm->check(); // was "checkclick;"

//#ifdef RECORD slowdown(); basher::count++; #endif

			if (_vm->_enhanced->keypressede())
				break;
		} while (!((mrelease > 0) || (buttona1()) || (buttonb1())));

		if (mrelease == 0) {
			inkey();
			if (aboutscroll) {
				move(e[2 - 1], e[1 - 1], 7);
				e[8 - 1] = inchar;
				if (egg == e)
					easteregg();
			}
			oktoexit = set::of('\15', '\33', '+', '#', eos).has(inchar);
			if (!oktoexit)  errorled();
		}

	} while (!((oktoexit) || (mrelease > 0)));

//#ifdef RECORD record_one(); #endif

	_vm->screturn = r == '#'; // "back door"
#endif

	setReadyLight(0);
	_vm->_animationsEnabled = true;
	_vm->_holdLeftMouse = false; // Used in Lucerna::checkclick().
}

/**
 * One of the 3 "Mode" functions passed as ScrollsFunctionType parameters.
 * The "asking" scroll. Used indirectly in diplayQuestion().
 * @remarks	Originally called 'dialogue'
 */
void Dialogs::scrollModeDialogue() {
	_vm->_graphics->loadMouse(kCurHand);

	_vm->_graphics->saveScreen();
	_vm->_graphics->showScroll();

	Common::Event event;
	while (!_vm->shouldQuit()) {
		_vm->_graphics->refreshScreen();

		_vm->getEvent(event);

		Common::Point cursorPos = _vm->getMousePos();
		cursorPos.y /= 2;

		char inChar = 0;
		if ((event.type == Common::EVENT_KEYDOWN) && (event.kbd.ascii <= 122) && (event.kbd.ascii >= 97)) {
			inChar = (char)event.kbd.ascii;
			Common::String temp(inChar);
			temp.toUppercase();
			inChar = temp[0];
		}

		if (_vm->shouldQuit() || (event.type == Common::EVENT_LBUTTONUP) || (event.type == Common::EVENT_KEYDOWN)) {
			if (((cursorPos.x >= _shadowBoxX - 65) && (cursorPos.y >= _shadowBoxY - 24) && (cursorPos.x <= _shadowBoxX - 5) && (cursorPos.y <= _shadowBoxY - 10))
				|| (inChar == 'Y') || (inChar == 'J') || (inChar == 'O')) { // Yes, Ja, Oui
				_scReturn = true;
				break;
			} else if (((cursorPos.x >= _shadowBoxX + 5) && (cursorPos.y >= _shadowBoxY - 24) && (cursorPos.x <= _shadowBoxX + 65) && (cursorPos.y <= _shadowBoxY - 10))
						|| (inChar == 'N')){ // No, Non, Nein
				_scReturn = false;
				break;
			}
		}
	}

	_vm->_graphics->restoreScreen();
	_vm->_graphics->removeBackup();
}

void Dialogs::store(byte what, TuneType &played) {
	memcpy(played, played + 1, sizeof(played) - 1);
	played[30] = what;
}

bool Dialogs::theyMatch(TuneType &played) {
	byte mistakes = 0;

	for (unsigned int i = 0; i < sizeof(played); i++) {
		if (played[i] != kTune[i])
			mistakes++;
	}

	return mistakes < 5;
}

/**
 * One of the 3 "Mode" functions passed as ScrollsFunctionType parameters.
 * Part of the harp mini-game.
 * @remarks	Originally called 'music_Scroll'
 */
void Dialogs::scrollModeMusic() {
	setReadyLight(3);
	_vm->_animationsEnabled = false;
	CursorMan.showMouse(false);
	_vm->_graphics->loadMouse(kCurFletch);

	TuneType played;
	for (unsigned int i = 0; i < sizeof(played); i++)
		played[i] = kPitchInvalid;
	int8 lastOne = -1, thisOne = -1; // Invalid values.

	_vm->_animationsEnabled = false;

	_vm->_graphics->saveScreen();
	_vm->_graphics->showScroll();

	Common::Event event;
	while (!_vm->shouldQuit()) {
		_vm->_graphics->refreshScreen();

		_vm->getEvent(event);

		// When we stop playing?
		if ((event.type == Common::EVENT_LBUTTONDOWN) ||
			((event.type == Common::EVENT_KEYDOWN) && ((event.kbd.keycode == Common::KEYCODE_RETURN) || (event.kbd.keycode == Common::KEYCODE_ESCAPE)))) {
				break;
		}

		// When we DO play:
		if ((event.type == Common::EVENT_KEYDOWN)
			&& ((event.kbd.keycode == Common::KEYCODE_q) || (event.kbd.keycode == Common::KEYCODE_w)
			|| (event.kbd.keycode == Common::KEYCODE_e) || (event.kbd.keycode == Common::KEYCODE_r)
			|| (event.kbd.keycode == Common::KEYCODE_t) || (event.kbd.keycode == Common::KEYCODE_y)
			|| (event.kbd.keycode == Common::KEYCODE_u) || (event.kbd.keycode == Common::KEYCODE_i)
			|| (event.kbd.keycode == Common::KEYCODE_o) || (event.kbd.keycode == Common::KEYCODE_p)
			|| (event.kbd.keycode == Common::KEYCODE_LEFTBRACKET) || (event.kbd.keycode == Common::KEYCODE_RIGHTBRACKET))) {
				byte value = 0;
				switch (event.kbd.keycode) {
				case Common::KEYCODE_q:
					value = 0;
					break;
				case Common::KEYCODE_w:
					value = 1;
					break;
				case Common::KEYCODE_e:
					value = 2;
					break;
				case Common::KEYCODE_r:
					value = 3;
					break;
				case Common::KEYCODE_t:
					value = 4;
					break;
				case Common::KEYCODE_y:
					value = 5;
					break;
				case Common::KEYCODE_u:
					value = 6;
					break;
				case Common::KEYCODE_i:
					value = 7;
					break;
				case Common::KEYCODE_o:
					value = 8;
					break;
				case Common::KEYCODE_p:
					value = 9;
					break;
				case Common::KEYCODE_LEFTBRACKET:
					value = 10;
					break;
				case Common::KEYCODE_RIGHTBRACKET:
					value = 11;
					break;
				default:
					error("cannot happen");
					break;
				}

				lastOne = thisOne;
				thisOne = value;

				_vm->_sound->playNote(_vm->kNotes[thisOne], 100);
				_vm->_system->delayMillis(200);

				if (!_vm->_bellsAreRinging) { // These handle playing the right tune.
					if (thisOne < lastOne)
						store(kPitchLower, played);
					else if (thisOne == lastOne)
						store(kPitchSame, played);
					else
						store(kPitchHigher, played);
				}

				if (theyMatch(played)) {
					setReadyLight(0);
					_vm->_timer->addTimer(8, Timer::kProcJacquesWakesUp, Timer::kReasonJacquesWakingUp);
					break;
				}
		}
	}

	_vm->_graphics->restoreScreen();
	_vm->_graphics->removeBackup();

	_vm->_animationsEnabled = true;
	CursorMan.showMouse(true);
}

void Dialogs::resetScrollDriver() {
	_scrollBells = 0;
	_currentFont = kFontStyleRoman;
	_useIcon = 0;
	_vm->_interrogation = 0; // Always reset after a scroll comes up.
}

/**
 * Rings the bell x times
 * @remarks	Originally called 'dingdongbell'
 */
void Dialogs::ringBell() {
	for (int i = 0; i < _scrollBells; i++)
		_vm->errorLed(); // Ring the bell "_scrollBells" times.
}

/**
 * This moves the mouse pointer off the scroll so that you can read it.
 * @remarks	Originally called 'dodgem'
 */
void Dialogs::dodgem() {
	_dodgeCoord = _vm->getMousePos();
	g_system->warpMouse(_dodgeCoord.x, _underScroll); // Move the pointer off the scroll.
}

/**
 * This is the opposite of Dodgem.
 * It moves the mouse pointer back, IF you haven't moved it in the meantime.
 * @remarks	Originally called 'undodgem'
 */
void Dialogs::unDodgem() {
	Common::Point actCoord = _vm->getMousePos();
	if ((actCoord.x == _dodgeCoord.x) && (actCoord.y == _underScroll))
		g_system->warpMouse(_dodgeCoord.x, _dodgeCoord.y); // No change, so restore the pointer's original position.
}

void Dialogs::drawScroll(DialogFunctionType modeFunc) {
	int16 lx = 0;
	int16 ly = (_maxLineNum + 1) * 6;
	int16 ex;
	for (int i = 0; i <= _maxLineNum; i++) {
		ex = _scroll[i].size() * 8;
		if (lx < ex)
			lx = ex;
	}
	int16 mx = 320;
	int16 my = 100; // Getmaxx & getmaxy div 2, both.
	lx /= 2;
	ly -= 2;

	if ((1 <= _useIcon) && (_useIcon <= 34))
		lx += kHalfIconWidth;

	CursorMan.showMouse(false);
	_vm->_graphics->drawScroll(mx, lx, my, ly);

	mx -= lx;
	my -= ly + 2;

	bool center = false;

	byte iconIndent = 0;
	switch (_useIcon) {
	case 0:
		iconIndent = 0;
		break; // No icon.
	case 34:
		_vm->_graphics->drawSign("about", 28, 76, 15);
		iconIndent = 0;
		break;
	case 35:
		_vm->_graphics->drawSign("gameover", 52, 59, 71);
		iconIndent = 0;
		break;
	}

	if ((1 <= _useIcon) && (_useIcon <= 33)) { // Standard icon.
		_vm->_graphics->drawIcon(mx, my + ly / 2, _useIcon);
		iconIndent = 53;
	}

	for (int i = 0; i <= _maxLineNum; i++) {
		if (!_scroll[i].empty())
			switch (_scroll[i][_scroll[i].size() - 1]) {
			case kControlCenter:
				center = true;
				_scroll[i].deleteLastChar();
				break;
			case kControlLeftJustified:
				center = false;
				_scroll[i].deleteLastChar();
				break;
			case kControlQuestion:
				_shadowBoxX = mx + lx;
				_shadowBoxY = my + ly;
				_scroll[i].setChar(' ', 0);
				_vm->_graphics->drawShadowBox(_shadowBoxX - 65, _shadowBoxY - 24, _shadowBoxX - 5, _shadowBoxY - 10, "Yes.");
				_vm->_graphics->drawShadowBox(_shadowBoxX + 5, _shadowBoxY - 24, _shadowBoxX + 65, _shadowBoxY - 10, "No.");
				break;
			}

		if (center)
			say(320 - _scroll[i].size() * 4 + iconIndent, my, _scroll[i]);
		else
			say(mx + iconIndent, my, _scroll[i]);

		my += 12;
	}

	_underScroll = (my + 3) * 2; // Multiplying because of the doubled screen height.
	ringBell();

	_vm->_dropsOk = false;
	dodgem();

	(this->*modeFunc)();

	unDodgem();
	_vm->_dropsOk = true;

	resetScrollDriver();
}

void Dialogs::drawBubble(DialogFunctionType modeFunc) {
	Common::Point points[3];

	CursorMan.showMouse(false);
	int16 xl = 0;
	int16 yl = (_maxLineNum + 1) * 5;
	for (int i = 0; i <= _maxLineNum; i++) {
		uint16 textWidth = _scroll[i].size() * 8;
		if (textWidth > xl)
			xl = textWidth;
	}
	xl /= 2;

	int16 xw = xl + 18;
	int16 yw = yl + 7;
	int16 my = yw * 2 - 2;
	int16 xc = 0;

	if (_talkX - xw < 0)
		xc = -(_talkX - xw);
	if (_talkX + xw > 639)
		xc = 639 - (_talkX + xw);

	// Compute triangle coords for the tail of the bubble
	points[0].x = _talkX - 10;
	points[0].y = yw;
	points[1].x = _talkX + 10;
	points[1].y = yw;
	points[2].x = _talkX;
	points[2].y = _talkY;

	_vm->_graphics->prepareBubble(xc, xw, my, points);

	// Draw the text of the bubble. The centering of the text was improved here compared to Pascal's settextjustify().
	// The font is not the same that outtextxy() uses in Pascal. I don't have that, so I used characters instead.
	// It's almost the same, only notable differences are '?', '!', etc.
	for (int i = 0; i <= _maxLineNum; i++) {
		int16 x = xc + _talkX - _scroll[i].size() / 2 * 8;
		bool offset = _scroll[i].size() % 2;
		_vm->_graphics->drawScrollText(_scroll[i], _vm->_font, 8, x - offset * 4, (i * 10) + 12, _vm->_graphics->_talkFontColor);
	}

	ringBell();
	CursorMan.showMouse(false);
	_vm->_dropsOk = false;

	// This does the actual drawing to the screen.
	(this->*modeFunc)();

	_vm->_dropsOk = true;
	CursorMan.showMouse(true); // sink;
	resetScrollDriver();
}

void Dialogs::reset() {
	_maxLineNum = 0;
	for (int i = 0; i < 15; i++) {
		if (!_scroll[i].empty())
			_scroll[i].clear();
	}
}

/**
 * Natural state of bubbles
 * @remarks	Originally called 'natural'
 */
void Dialogs::setBubbleStateNatural() {
	_talkX = 320;
	_talkY = 200;
	_vm->_graphics->setDialogColor(kColorDarkgray, kColorWhite);
}

Common::String Dialogs::displayMoney() {
	Common::String result;

	if (_vm->_money < 12) { // just pence
		result = Common::String::format("%dd", _vm->_money);
	} else if (_vm->_money < 240) { // shillings & pence
		if ((_vm->_money % 12) == 0)
			result = Common::String::format("%d/-", _vm->_money / 12);
		else
			result = Common::String::format("%d/%d", _vm->_money / 12, _vm->_money % 12);
	} else { // L, s & d
		result = Common::String::format("\x9C%d.%d.%d", _vm->_money / 240, (_vm->_money / 12) % 20,
		                _vm->_money % 12);
	}
	if (_vm->_money > 12) {
		Common::String extraStr = Common::String::format(" (that's %dd)", _vm->_money);
		result += extraStr;
	}

	return result;
}

/**
 * Strip trailing character in a string
 * @remarks	Originally called 'strip'
 */
void Dialogs::stripTrailingSpaces(Common::String &str) {
	while (str.lastChar() == ' ')
		str.deleteLastChar();
	// We don't use String::trim() here because we need the leading whitespaces.
}

/**
 * Does the word wrapping.
 */
void Dialogs::solidify(byte n) {
	if (!_scroll[n].contains(' '))
		return; // No spaces.

	// So there MUST be a space there, somewhere...
	do {
		_scroll[n + 1] = _scroll[n][_scroll[n].size() - 1] + _scroll[n + 1];
		_scroll[n].deleteLastChar();
	} while (_scroll[n][_scroll[n].size() - 1] != ' ');

	stripTrailingSpaces(_scroll[n]);
}

/**
 * @remarks	Originally called 'calldriver'
 * Display text by calling the dialog driver. It unifies the function of the original
 * 'calldriver' and 'display' by using Common::String instead of a private buffer.
 */
void Dialogs::displayText(Common::String text) {
	_vm->_sound->stopSound();

	setReadyLight(0);
	_scReturn = false;
	bool mouthnext = false;
	bool callSpriteRun = true; // Only call sprite_run the FIRST time.

	switch (text.lastChar()) {
	case kControlToBuffer:
		text.deleteLastChar();
		break; // ^D = (D)on't include pagebreak
	case kControlSpeechBubble:
	case kControlQuestion:
		break; // ^B = speech (B)ubble, ^Q = (Q)uestion in dialogue box
	default:
		text.insertChar(kControlParagraph, text.size());
	}

	for (uint16 i = 0; i < text.size(); i++) {
		if (mouthnext) {
			if (text[i] == kControlRegister)
				_param = 0;
			else if (('0' <= text[i]) && (text[i] <= '9'))
				_param = text[i] - 48;
			else if (('A' <= text[i]) && (text[i] <= 'Z'))
				_param = text[i] - 55;

			mouthnext = false;
		} else {
			switch (text[i]) {
			case kControlParagraph:
				if ((_maxLineNum == 0) && (_scroll[0].empty()))
					break;

				if (callSpriteRun)
					_vm->spriteRun();
				callSpriteRun = false;

				drawScroll(&Avalanche::Dialogs::scrollModeNormal);

				reset();

				if (_scReturn)
					return;
				break;
			case kControlBell:
				_scrollBells++;
				break;
			case kControlSpeechBubble:
				if ((_maxLineNum == 0) && (_scroll[0].empty()))
					break;

				if (callSpriteRun)
					_vm->spriteRun();
				callSpriteRun = false;

				if (_param == 0)
					setBubbleStateNatural();
				else if ((1 <= _param) && (_param <= 9)) {
					assert(_param - 1 < _vm->_animation->kSpriteNumbMax);
					AnimationType *spr = _vm->_animation->_sprites[_param - 1];
					if ((_param > _vm->_animation->kSpriteNumbMax) || (!spr->_quick)) { // Not valid.
						_vm->errorLed();
						setBubbleStateNatural();
					} else
						spr->chatter(); // Normal sprite talking routine.
				} else if ((10 <= _param) && (_param <= 36)) {
					// Quasi-peds. (This routine performs the same
					// thing with QPs as triptype.chatter does with the
					// sprites.)
					assert(_param - 10 < 16);
					PedType *quasiPed = &_vm->_peds[kQuasipeds[_param - 10]._whichPed];
					_talkX = quasiPed->_x;
					_talkY = quasiPed->_y; // Position.

					_vm->_graphics->setDialogColor(kQuasipeds[_param - 10]._backgroundColor, kQuasipeds[_param - 10]._textColor);
				} else {
					_vm->errorLed(); // Not valid.
					setBubbleStateNatural();
				}

				drawBubble(&Avalanche::Dialogs::scrollModeNormal);

				reset();

				if (_scReturn)
					return;
				break;

			// CHECME: The whole kControlNegative block seems completely unused, as the only use (the easter egg check) is a false positive
			case kControlNegative:
				switch (_param) {
				case 1:
					displayText(displayMoney() + kControlToBuffer); // Insert cash balance. (Recursion)
					break;
				case 2: {
					int pwdId = _vm->_parser->kFirstPassword + _vm->_passwordNum;
					displayText(_vm->_parser->_vocabulary[pwdId]._word + kControlToBuffer);
					}
					break;
				case 3:
					displayText(_vm->_favoriteDrink + kControlToBuffer);
					break;
				case 4:
					displayText(_vm->_favoriteSong + kControlToBuffer);
					break;
				case 5:
					displayText(_vm->_worstPlaceOnEarth + kControlToBuffer);
					break;
				case 6:
					displayText(_vm->_spareEvening + kControlToBuffer);
					break;
				case 9: {
					Common::String tmpStr = Common::String::format("%d,%d%c",_vm->_catacombX, _vm->_catacombY, kControlToBuffer);
					displayText(tmpStr);
					}
					break;
				case 10:
					switch (_vm->_boxContent) {
					case 0: // Sixpence.
						displayScrollChain('Q', 37); // You find the sixpence.
						_vm->_money += 6;
						_vm->_boxContent = _vm->_parser->kNothing;
						_vm->incScore(2);
						return;
					case Parser::kNothing:
						displayText("nothing at all. It's completely empty.");
						break;
					default:
						displayText(_vm->getItem(_vm->_boxContent) + '.');
					}
					break;
				case 11:
					for (int j = 0; j < kObjectNum; j++) {
						if (_vm->_objects[j])
							displayText(_vm->getItem(j) + ", " + kControlToBuffer);
					}
					break;
				}
				break;
			case kControlIcon:
				_useIcon = _param;
				break;
			case kControlNewLine:
				_maxLineNum++;
				break;
			case kControlQuestion:
				if (callSpriteRun)
					_vm->spriteRun();
				callSpriteRun = false;

				_maxLineNum++;
				_scroll[_maxLineNum] = kControlQuestion;

				drawScroll(&Avalanche::Dialogs::scrollModeDialogue);
				reset();
				break;
			case kControlRegister:
				mouthnext = true;
				break;
			case kControlInsertSpaces:
				for (int j = 0; j < 9; j++)
					_scroll[_maxLineNum] += ' ';
				break;
			default: // Add new char.
				if (_scroll[_maxLineNum].size() == 50) {
					solidify(_maxLineNum);
					_maxLineNum++;
				}
				_scroll[_maxLineNum] += text[i];
				break;
			}
		}
	}

	setReadyLight(2);
}

void Dialogs::setTalkPos(int16 x, int16 y) {
	_talkX = x;
	_talkY = y;
}

int16 Dialogs::getTalkPosX() {
	return _talkX;
}

bool Dialogs::displayQuestion(Common::String question) {
	displayText(question + kControlNewLine + kControlQuestion);

	if (_scReturn && (_vm->_rnd->getRandomNumber(1) == 0)) { // Half-and-half chance.
		Common::String tmpStr = Common::String::format("...Positive about that?%cI%c%c%c", kControlRegister, kControlIcon, kControlNewLine, kControlQuestion);
		displayText(tmpStr); // Be annoying!
		if (_scReturn && (_vm->_rnd->getRandomNumber(3) == 3)) { // Another 25% chance
			// \? are used to avoid that ??! is parsed as a trigraph
			tmpStr = Common::String::format("%c100%% certain\?\?!%c%c%c%c", kControlInsertSpaces, kControlInsertSpaces, kControlIcon, kControlNewLine, kControlQuestion);
			displayText(tmpStr); // Be very annoying!
		}
	}

	return _scReturn;
}

void Dialogs::loadFont() {
	Common::File file;

	if (!file.open("avalot.fnt"))
		error("AVALANCHE: Scrolls: File not found: avalot.fnt");

	for (int16 i = 0; i < 256; i++)
		file.read(_fonts[0][i], 16);
	file.close();

	if (!file.open("avitalic.fnt"))
		error("AVALANCHE: Scrolls: File not found: avitalic.fnt");

	for (int16 i = 0; i < 256; i++)
		file.read(_fonts[1][i], 16);
	file.close();

	if (!file.open("ttsmall.fnt"))
		error("AVALANCHE: Scrolls: File not found: ttsmall.fnt");

	for (int16 i = 0; i < 256; i++)
		file.read(_vm->_font[i],16);
	file.close();
}

/**
 * Practically this one is a mini-game which called when you play the harp in the monastery.
 * @remarks	Originally called 'musical_scroll'
 */
void Dialogs::displayMusicalScroll() {
	Common::String tmpStr = Common::String::format("To play the harp...%c%cUse these keys:%c%cQ W E R T Y U I O P [ ]%c%cOr press Enter to stop playing.%c",
		        kControlNewLine, kControlNewLine, kControlNewLine, kControlInsertSpaces, kControlNewLine, kControlNewLine, kControlToBuffer);
	displayText(tmpStr);

	_vm->spriteRun();
	CursorMan.showMouse(false);
	drawScroll(&Avalanche::Dialogs::scrollModeMusic);
	CursorMan.showMouse(true);
	reset();
}

void Dialogs::unSkrimble(Common::String &text) {
	for (uint16  i = 0; i < text.size(); i++)
		text.setChar((~(text[i] - (i + 1))) % 256, i);
}

void Dialogs::doTheBubble(Common::String &text) {
	text.insertChar(kControlSpeechBubble, text.size());
	assert(text.size() < 2000);
}

/**
 * Display a string in a scroll
 * @remarks	Originally called 'dixi'
 */
void Dialogs::displayScrollChain(char block, byte point, bool report, bool bubbling) {
	Common::File indexfile;
	if (!indexfile.open("avalot.idx"))
		error("AVALANCHE: Visa: File not found: avalot.idx");

	bool error = false;

	indexfile.seek((toupper(block) - 'A') * 2);
	uint16 idx_offset = indexfile.readUint16LE();
	if (idx_offset == 0)
		error = true;

	indexfile.seek(idx_offset + point * 2);
	uint16 sez_offset = indexfile.readUint16LE();
	if (sez_offset == 0)
		error = true;

	indexfile.close();

	_noError = !error;

	if (error) {
		if (report) {
			Common::String todisplay = Common::String::format("%cError accessing scroll %c%d", kControlBell, block, point);
			displayText(todisplay);
		}
		return;
	}

	Common::File sezfile;
	if (!sezfile.open("avalot.sez"))
		::error("AVALANCHE: Visa: File not found: avalot.sez");

	sezfile.seek(sez_offset);
	uint16 _bufSize = sezfile.readUint16LE();
	assert(_bufSize < 2000);
	char *_buffer = new char[_bufSize];
	sezfile.read(_buffer, _bufSize);
	sezfile.close();
	Common::String text(_buffer, _bufSize);
	delete[] _buffer;

	unSkrimble(text);
	if (bubbling)
		doTheBubble(text);
	displayText(text);
}

/**
 * Start speech
 * @remarks	Originally called 'speech'
 */
void Dialogs::speak(byte who, byte subject) {
	if (subject == 0) { // No subject.
		displayScrollChain('S', who, false, true);
		return;
	}

	// Subject given.
	_noError = false; // Assume that until we know otherwise.

	Common::File indexfile;
	if (!indexfile.open("converse.avd"))
		error("AVALANCHE: Visa: File not found: converse.avd");

	indexfile.seek(who * 2 - 2);
	uint16 idx_offset = indexfile.readUint16LE();
	uint16 next_idx_offset = indexfile.readUint16LE();

	if ((idx_offset == 0) || ((((next_idx_offset - idx_offset) / 2) - 1) < subject))
		return;

	indexfile.seek(idx_offset + subject * 2);
	uint16 sezOffset = indexfile.readUint16LE();
	if ((sezOffset == 0) || (indexfile.err()))
		return;
	indexfile.close();

	Common::File sezfile;
	if (!sezfile.open("avalot.sez"))
		error("AVALANCHE: Visa: File not found: avalot.sez");

	sezfile.seek(sezOffset);
	uint16 _bufSize = sezfile.readUint16LE();
	assert(_bufSize < 2000);
	char *_buffer = new char[_bufSize];
	sezfile.read(_buffer, _bufSize);
	sezfile.close();
	Common::String text(_buffer, _bufSize);
	delete[] _buffer;

	unSkrimble(text);
	doTheBubble(text);
	displayText(text);

	_noError = true;
}

void Dialogs::talkTo(byte whom) {
	if (_vm->_parser->_person == kPeoplePardon) {
		_vm->_parser->_person = (People)_vm->_subjectNum;
		_vm->_subjectNum = 0;
	}

	if (_vm->_subjectNum == 0) {
		switch (whom) {
		case kPeopleSpludwick:
			if ((_vm->_lustieIsAsleep) & (!_vm->_objects[kObjectPotion - 1])) {
				displayScrollChain('Q', 68);
				_vm->_objects[kObjectPotion - 1] = true;
				_vm->refreshObjectList();
				_vm->incScore(3);
				return;
			} else if (_vm->_talkedToCrapulus) {
				// Spludwick - what does he need?
				// 0 - let it through to use normal routine.
				switch (_vm->_givenToSpludwick) {
				case 1: // Fallthrough is intended.
				case 2: {
					Common::String objStr = _vm->getItem(AvalancheEngine::kSpludwicksOrder[_vm->_givenToSpludwick]);
					Common::String tmpStr = Common::String::format("Can you get me %s, please?%c2%c",
						objStr.c_str(), kControlRegister, kControlSpeechBubble);
					displayText(tmpStr);
					}
					return;
				case 3:
					displayScrollChain('Q', 30); // Need any help with the game?
					return;
				}
			} else {
				displayScrollChain('Q', 42); // Haven't talked to Crapulus. Go and talk to him.
				return;
			}
			break;
		case kPeopleIbythneth:
			if (_vm->_givenBadgeToIby) {
				displayScrollChain('Q', 33); // Thanks a lot!
				return; // And leave the proc.
			}
			break; // Or... just continue, 'cos he hasn't got it.
		case kPeopleDogfood:
			if (_vm->_wonNim) { // We've won the game.
				displayScrollChain('Q', 6); // "I'm Not Playing!"
				return; // Zap back.
			} else
				_vm->_askedDogfoodAboutNim = true;
			break;
		case kPeopleAyles:
			if (!_vm->_aylesIsAwake) {
				displayScrollChain('Q', 43); // He's fast asleep!
				return;
			} else if (!_vm->_givenPenToAyles) {
				displayScrollChain('Q', 44); // Can you get me a pen, Avvy?
				return;
			}
			break;

		case kPeopleJacques:
			displayScrollChain('Q', 43);
			return;

		case kPeopleGeida:
			if (_vm->_givenPotionToGeida)
				_vm->_geidaFollows = true;
			else {
				displayScrollChain('U', 17);
				return;
			}
			break;
		case kPeopleSpurge:
			if (!_vm->_sittingInPub) {
				displayScrollChain('Q', 71); // Try going over and sitting down.
				return;
			} else {
				if (_vm->_spurgeTalkCount < 5)
					_vm->_spurgeTalkCount++;
				if (_vm->_spurgeTalkCount > 1) { // no. 1 falls through
					displayScrollChain('Q', 70 + _vm->_spurgeTalkCount);
					return;
				}
			}
			break;
		}
	// On a subject. Is there any reason to block it?
	} else if ((whom == kPeopleAyles) && (!_vm->_aylesIsAwake)) {
		displayScrollChain('Q', 43); // He's fast asleep!
		return;
	}

	if (whom > 149)
		whom -= 149;

	bool noMatches = true;
	for (int i = 0; i < _vm->_animation->kSpriteNumbMax; i++) {
		if (_vm->_animation->_sprites[i]->_characterId == whom) {
			Common::String tmpStr = Common::String::format("%c%c%c", kControlRegister, i + 49, kControlToBuffer);
			displayText(tmpStr);
			noMatches = false;
			break;
		}
	}

	if (noMatches) {
		Common::String tmpStr = Common::String::format("%c%c%c", kControlRegister, kControlRegister, kControlToBuffer);
		displayText(tmpStr);
	}

	speak(whom, _vm->_subjectNum);

	if (!_noError)
		displayScrollChain('N', whom); // File not found!

	if ((_vm->_subjectNum == 0) && ((whom + 149) == kPeopleCrapulus)) { // Crapulus: get the badge - first time only
		_vm->_objects[kObjectBadge - 1] = true;
		_vm->refreshObjectList();
		displayScrollChain('Q', 1); // Circular from Cardiff.
		_vm->_talkedToCrapulus = true;
		_vm->setRoom(kPeopleCrapulus, kRoomDummy); // Crapulus walks off.

		AnimationType *spr = _vm->_animation->_sprites[1];
		spr->_vanishIfStill = true;
		spr->walkTo(2); // Walks away.

		_vm->incScore(2);
	}
}

/**
 * This makes Avalot say the response.
 * @remarks	Originally called 'sayit'
 */
void Dialogs::sayIt(Common::String str) {
	Common::String x = str;
	x.setChar(toupper(x[0]), 0);
	Common::String tmpStr = Common::String::format("%c1%s.%c%c2", kControlRegister, x.c_str(), kControlSpeechBubble, kControlRegister);
	displayText(tmpStr);
}

Common::String Dialogs::personSpeaks() {
	if ((_vm->_parser->_person == kPeoplePardon) || (_vm->_parser->_person == kPeopleNone)) {
		if ((_vm->_him == kPeoplePardon) || (_vm->getRoom(_vm->_him) != _vm->_room))
			_vm->_parser->_person = _vm->_her;
		else
			_vm->_parser->_person = _vm->_him;
	}

	if (_vm->getRoom(_vm->_parser->_person) != _vm->_room) {
		return Common::String::format("%c1", kControlRegister); // Avvy himself!
	}

	bool found = false; // The _person we're looking for's code is in _person.
	Common::String tmpStr;

	for (int i = 0; i < _vm->_animation->kSpriteNumbMax; i++) {
		AnimationType *curSpr = _vm->_animation->_sprites[i];
		if (curSpr->_quick && (curSpr->_characterId + 149 == _vm->_parser->_person)) {
			tmpStr += Common::String::format("%c%c", kControlRegister, '1' + i);
			found = true;
		}
	}

	if (found)
		return tmpStr;

	for (int i = 0; i < 16; i++) {
		if ((kQuasipeds[i]._who == _vm->_parser->_person) && (kQuasipeds[i]._room == _vm->_room))
			tmpStr += Common::String::format("%c%c", kControlRegister, 'A' + i);
	}

	return tmpStr;
}

/**
 * Display a message when (uselessly) giving an object away
 * @remarks	Originally called 'heythanks'
 */
void Dialogs::sayThanks(byte thing) {
	Common::String tmpStr = personSpeaks();
	tmpStr += Common::String::format("Hey, thanks!%c(But now, you've lost it!)", kControlSpeechBubble);
	displayText(tmpStr);

	if (thing < kObjectNum)
		_vm->_objects[thing] = false;
}

/**
 * Display a 'Hello' message
 */
void Dialogs::sayHello() {
	Common::String tmpStr = personSpeaks();
	tmpStr += Common::String::format("Hello.%c", kControlSpeechBubble);
	displayText(tmpStr);
}

/**
 * Display a 'OK' message
 */
void Dialogs::sayOK() {
	Common::String tmpStr = personSpeaks();
	tmpStr += Common::String::format("That's OK.%c", kControlSpeechBubble);
	displayText(tmpStr);
}

/**
 * Display a 'Silly' message
 * @remarks	Originally called 'silly'
 */
void Dialogs::saySilly() {
	displayText("Don't be silly!");
}

} // End of namespace Avalanche
