/* 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 "mohawk/cstime_game.h"
#include "mohawk/cstime_ui.h"
#include "mohawk/cstime_view.h"
#include "mohawk/resource.h"
#include "common/algorithm.h" // find
#include "common/events.h"
#include "common/system.h"
#include "common/textconsole.h"
#include "graphics/fontman.h"

namespace Mohawk {

// read a null-terminated string from a stream
static Common::String readString(Common::SeekableReadStream *stream) {
	Common::String ret;
	while (!stream->eos()) {
		byte in = stream->readByte();
		if (!in)
			break;
		ret += in;
	}
	return ret;
}

CSTimeInterface::CSTimeInterface(MohawkEngine_CSTime *vm) : _vm(vm) {
	_sceneRect = Common::Rect(0, 0, 640, 340);
	_uiRect = Common::Rect(0, 340, 640, 480);

	_bookRect = Common::Rect(_uiRect.right - 95, _uiRect.top + 32, _uiRect.right - 25, _uiRect.top + 122);
	_noteRect = Common::Rect(_uiRect.left + 27, _uiRect.top + 31, _uiRect.left + 103, _uiRect.top + 131);
	_dialogTextRect = Common::Rect(0 + 125, 340 + 40, 640 - 125, 480 - 20);

	_cursorActive = false;
	_cursorShapes[0] = 0xFFFF;
	_cursorShapes[1] = 0xFFFF;
	_cursorShapes[2] = 0xFFFF;
	_cursorNextTime = 0;

	_help = new CSTimeHelp(_vm);
	_inventoryDisplay = new CSTimeInventoryDisplay(_vm, _dialogTextRect);
	_book = new CSTimeBook(_vm);
	_note = new CSTimeCarmenNote(_vm);
	_options = new CSTimeOptions(_vm);

	// The demo uses hardcoded system fonts
	if (!(_vm->getFeatures() & GF_DEMO)) {
		if (!_normalFont.loadFromFON("EvP14.fon"))
			error("failed to load normal font");
		if (!_dialogFont.loadFromFON("Int1212.fon"))
			error("failed to load dialog font");
		if (!_rolloverFont.loadFromFON("Int1818.fon"))
			error("failed to load rollover font");
	}

	_uiFeature = NULL;
	_dialogTextFeature = NULL;
	_rolloverTextFeature = NULL;
	_bubbleTextFeature = NULL;

	_mouseWasInScene = false;
	_state = kCSTimeInterfaceStateNormal;

	_dialogLines.resize(5);
	_dialogLineColors.resize(5);
}

CSTimeInterface::~CSTimeInterface() {
	delete _help;
	delete _inventoryDisplay;
	delete _book;
	delete _note;
	delete _options;
}

const Graphics::Font &CSTimeInterface::getNormalFont() const {
	// HACK: Use a ScummVM GUI font in place of a system one for the demo
	if (_vm->getFeatures() & GF_DEMO)
		return *FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);

	return _normalFont;
}

const Graphics::Font &CSTimeInterface::getDialogFont() const {
	// HACK: Use a ScummVM GUI font in place of a system one for the demo
	if (_vm->getFeatures() & GF_DEMO)
		return *FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);

	return _dialogFont;
}

const Graphics::Font &CSTimeInterface::getRolloverFont() const {
	// HACK: Use a ScummVM GUI font in place of a system one for the demo
	if (_vm->getFeatures() & GF_DEMO)
		return *FontMan.getFontByUsage(Graphics::FontManager::kBigGUIFont);

	return _rolloverFont;
}

void CSTimeInterface::cursorInstall() {
	_vm->getView()->loadBitmapCursors(200);
}

void CSTimeInterface::cursorIdle() {
	if (!_cursorActive || _cursorShapes[1] == 0xFFFF)
		return;

	if (_vm->_system->getMillis() <= _cursorNextTime + 250)
		return;

	cursorSetShape(_cursorShapes[1], false);
	_cursorShapes[1] = _cursorShapes[2];
	_cursorShapes[2] = 0xFFFF;
}

void CSTimeInterface::cursorActivate(bool state) {
	_cursorActive = state;
}

void CSTimeInterface::cursorChangeShape(uint16 id) {
	if (_cursorShapes[1] == 0xFFFF)
		_cursorShapes[1] = id;
	else
		_cursorShapes[2] = id;
}

uint16 CSTimeInterface::cursorGetShape() {
	if (_cursorShapes[2] != 0xFFFF)
		return _cursorShapes[2];
	else if (_cursorShapes[1] != 0xFFFF)
		return _cursorShapes[1];
	else
		return _cursorShapes[0];
}

void CSTimeInterface::cursorSetShape(uint16 id, bool reset) {
	if (_cursorShapes[0] != id) {
		_cursorShapes[0] = id;
		_vm->getView()->setBitmapCursor(id);
		_cursorNextTime = _vm->_system->getMillis();
	}
}

void CSTimeInterface::cursorSetWaitCursor() {
	uint16 shape = cursorGetShape();
	switch (shape) {
	case 8:
		cursorChangeShape(9);
		break;
	case 9:
		break;
	case 11:
		cursorChangeShape(12);
		break;
	case 13:
		cursorChangeShape(15);
		break;
	default:
		cursorChangeShape(3);
		break;
	}
}

void CSTimeInterface::openResFile() {
	_vm->loadResourceFile("data/iface");
}

void CSTimeInterface::install() {
	uint16 resourceId = 100; // TODO
	_vm->getView()->installGroup(resourceId, 16, 0, true, 100);

	// TODO: store/reset these

	_dialogTextFeature = _vm->getView()->installViewFeature(0, 0, NULL);
	_dialogTextFeature->_data.bounds = _dialogTextRect;
	_dialogTextFeature->_data.bitmapIds[0] = 0;
	// We don't set a port.
	_dialogTextFeature->_moveProc = (Module::FeatureProc)&CSTimeModule::dialogTextMoveProc;
	_dialogTextFeature->_drawProc = (Module::FeatureProc)&CSTimeModule::dialogTextDrawProc;
	_dialogTextFeature->_timeProc = NULL;
	_dialogTextFeature->_flags = kFeatureOldSortForeground; // FIXME: not in original

	_rolloverTextFeature = _vm->getView()->installViewFeature(0, 0, NULL);
	_rolloverTextFeature->_data.bounds = Common::Rect(0 + 100, 340 + 5, 640 - 100, 480 - 25);
	_rolloverTextFeature->_data.bitmapIds[0] = 0;
	_rolloverTextFeature->_moveProc = (Module::FeatureProc)&CSTimeModule::rolloverTextMoveProc;
	_rolloverTextFeature->_drawProc = (Module::FeatureProc)&CSTimeModule::rolloverTextDrawProc;
	_rolloverTextFeature->_timeProc = NULL;
	_rolloverTextFeature->_flags = kFeatureOldSortForeground; // FIXME: not in original
}

void CSTimeInterface::draw() {
	// TODO
	if (!_uiFeature) {
		uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop;
		assert(flags == 0x4800000);
		uint16 resourceId = 100; // TODO
		_uiFeature = _vm->getView()->installViewFeature(resourceId, flags, NULL);
		// TODO: special-case for case 20
	} else {
		_uiFeature->resetFeatureScript(1, 0);
		// TODO: special-case for case 20
	}

	// TODO: special-case for cases 19 and 20

	_note->drawSmallNote();
	_book->drawSmallBook();
	_inventoryDisplay->draw();
}

void CSTimeInterface::idle() {
	// TODO: inv sound handling

	_vm->getCase()->getCurrScene()->idle();
	_inventoryDisplay->idle();
	cursorIdle();

	_vm->getView()->idleView();
}

void CSTimeInterface::mouseDown(Common::Point pos) {
	_vm->resetTimeout();

	if (_options->getState()) {
		// TODO: _options->mouseDown(pos);
		return;
	}

	if (!cursorGetState())
		return;
	if (_vm->getCase()->getCurrScene()->eventIsActive())
		return;

	switch (cursorGetShape()) {
	case 1:
		cursorChangeShape(4);
		break;
	case 2:
		cursorChangeShape(5);
		break;
	case 13:
		cursorChangeShape(14);
		break;
	}

	if (_book->getState() == 2) {
		// TODO: _book->mouseDown(pos);
		return;
	}

	// TODO: if in sailing puzzle, sailing puzzle mouse down, return

	if (_note->getState() > 0) {
		return;
	}

	if (_sceneRect.contains(pos)) {
		_vm->getCase()->getCurrScene()->mouseDown(pos);
		return;
	}

	// TODO: case 20 ui craziness is handled seperately..

	CSTimeConversation *conv = _vm->getCase()->getCurrConversation();
	if (_bookRect.contains(pos) || (_noteRect.contains(pos) && _note->havePiece(0xffff))) {
		if (conv->getState() != (uint)~0)
			conv->end(false);
		if (_help->getState() != (uint)~0)
			_help->end();
		return;
	}

	if (_help->getState() != (uint)~0) {
		_help->mouseDown(pos);
		return;
	}

	if (conv->getState() != (uint)~0) {
		conv->mouseDown(pos);
		return;
	}

	// TODO: handle symbols

	if (_inventoryDisplay->_invRect.contains(pos)) {
		// TODO: special handling for case 6

		_inventoryDisplay->mouseDown(pos);
	}
}

void CSTimeInterface::mouseMove(Common::Point pos) {
	if (_options->getState()) {
		// TODO: _options->mouseMove(pos);
		return;
	}

	if (!cursorGetState())
		return;

	if (_mouseWasInScene && _uiRect.contains(pos)) {
		clearTextLine();
		_mouseWasInScene = false;
	}

	if (_book->getState() == 2) {
		// TODO: _book->mouseMove(pos);
		return;
	}

	if (_note->getState() == 2)
		return;

	// TODO: case 20 ui craziness is handled seperately..

	if (_sceneRect.contains(pos) && !_vm->getCase()->getCurrScene()->eventIsActive()) {
		_vm->getCase()->getCurrScene()->mouseMove(pos);
		_mouseWasInScene = true;
		return;
	}

	if (cursorGetShape() == 13) {
		cursorSetShape(1);
		return;
	} else if (cursorGetShape() == 14) {
		cursorSetShape(4);
		return;
	}

	bool mouseIsDown = _vm->getEventManager()->getButtonState() & 1;

	if (_book->getState() == 1 && !_bookRect.contains(pos)) {
		if (_state != kCSTimeInterfaceStateDragging) {
			clearTextLine();
			cursorSetShape(mouseIsDown ? 4 : 1);
			_book->setState(0);
		}
		return;
	}

	// TODO: case 20 ui craziness again

	if (_note->getState() == 1 && !_noteRect.contains(pos)) {
		if (_state != kCSTimeInterfaceStateDragging) {
			clearTextLine();
			cursorSetShape(mouseIsDown ? 4 : 1);
			_note->setState(0);
		}
		return;
	}

	// TODO: handle symbols

	if (_bookRect.contains(pos)) {
		if (_state != kCSTimeInterfaceStateDragging) {
			displayTextLine("Open Chronopedia");
			cursorSetShape(mouseIsDown ? 5 : 2);
			_book->setState(1);
		}
		return;
	}

	if (_noteRect.contains(pos)) {
		if (_state != kCSTimeInterfaceStateDragging && _note->havePiece(0xffff) && !_note->getState()) {
			displayTextLine("Look at Note");
			cursorSetShape(mouseIsDown ? 5 : 2);
			_note->setState(1);
		}
		return;
	}

	if (_vm->getCase()->getCurrConversation()->getState() != (uint)~0) {
		_vm->getCase()->getCurrConversation()->mouseMove(pos);
		return;
	}

	if (_help->getState() != (uint)~0) {
		_help->mouseMove(pos);
		return;
	}

	if (_state == kCSTimeInterfaceStateDragging) {
		// FIXME: dragging
		return;
	}

	// FIXME: if case is 20, we're done, return

	if (_inventoryDisplay->_invRect.contains(pos)) {
		_inventoryDisplay->mouseMove(pos);
	}
}

void CSTimeInterface::mouseUp(Common::Point pos) {
	if (_options->getState()) {
		// TODO: options->mouseUp(pos);
		return;
	}

	if (!cursorGetState())
		return;

	if (_state == kCSTimeInterfaceStateDragging) {
		stopDragging();
		return;
	}

	if (_state == kCSTimeInterfaceStateDragStart)
		_state = kCSTimeInterfaceStateNormal;

	switch (cursorGetShape()) {
	case 4:
		cursorChangeShape(1);
		break;
	case 5:
		cursorChangeShape(2);
		break;
	case 14:
		cursorChangeShape(13);
		break;
	}

	if (_vm->getCase()->getCurrScene()->eventIsActive()) {
		if (_vm->getCurrentEventType() == kCSTimeEventWaitForClick)
			_vm->mouseClicked();
		return;
	}

	if (_book->getState() == 2) {
		// TODO: _book->mouseUp(pos);
		return;
	}

	if (_note->getState() == 2) {
		_note->closeNote();
		mouseMove(pos);
		return;
	}

	// TODO: if in sailing puzzle, sailing puzzle mouse up, return

	// TODO: case 20 ui craziness is handled seperately..

	if (_sceneRect.contains(pos)) {
		_vm->getCase()->getCurrScene()->mouseUp(pos);
		return;
	}

	if (_vm->getCase()->getCurrConversation()->getState() != (uint)~0) {
		_vm->getCase()->getCurrConversation()->mouseUp(pos);
		return;
	}

	if (_help->getState() != (uint)~0) {
		_help->mouseUp(pos);
		return;
	}

	// TODO: handle symbols

	if (_bookRect.contains(pos)) {
		// TODO: handle flashing cuffs
		// TODO: _book->open();
		return;
	}

	if (_noteRect.contains(pos)) {
		// TODO: special-casing for case 19
		if (_note->havePiece(0xffff))
			_note->drawBigNote();
	}

	if (_inventoryDisplay->_invRect.contains(pos)) {
		// TODO: special-casing for case 6
		_inventoryDisplay->mouseUp(pos);
	}
}

void CSTimeInterface::cursorOverHotspot() {
	if (!cursorGetState())
		return;
	if (_state == kCSTimeInterfaceStateDragStart || _state == kCSTimeInterfaceStateDragging)
		return;
	if (cursorGetShape() == 3 || cursorGetShape() == 9)
		return;

	bool mouseIsDown = _vm->getEventManager()->getButtonState() & 1;
	if (mouseIsDown)
		cursorSetShape(5);
	else if (cursorGetShape() == 1)
		cursorChangeShape(2);
}

void CSTimeInterface::setCursorForCurrentPoint() {
	uint16 shape = 1;

	Common::Point mousePos = _vm->getEventManager()->getMousePos();
	if (_bookRect.contains(mousePos)) {
		shape = 2;
	} else {
		uint convState = _vm->getCase()->getCurrConversation()->getState();
		uint helpState = _help->getState();
		// TODO: symbols too
		if (convState == (uint)~0 || convState == 0 || helpState == (uint)~0 || helpState == 0) {
			// FIXME: set cursor to 2 if inventory display occupied
		} else if (convState == 1 || helpState == 1) {
			// FIXME: set cursor to 2 if over conversation rect
		}
	}

	cursorSetShape(shape);
}

void CSTimeInterface::clearTextLine() {
	_rolloverText.clear();
}

void CSTimeInterface::displayTextLine(Common::String text) {
	_rolloverText = text;
}

void CSTimeInterface::clearDialogArea() {
	_dialogLines.clear();
	_dialogLines.resize(5);
	// TODO: dirty feature
}

void CSTimeInterface::clearDialogLine(uint line) {
	_dialogLines[line].clear();
	// TODO: dirty feature
}

void CSTimeInterface::displayDialogLine(uint16 id, uint line, byte color) {
	Common::SeekableReadStream *stream = _vm->getResource(ID_STRI, id);
	Common::String text = readString(stream);
	delete stream;

	_dialogLines[line] = text;
	_dialogLineColors[line] = color;
	// TODO: dirty feature
}

void CSTimeInterface::drawTextIdToBubble(uint16 id) {
	Common::SeekableReadStream *stream = _vm->getResource(ID_STRI, id);
	Common::String text = readString(stream);
	delete stream;

	drawTextToBubble(&text);
}

void CSTimeInterface::drawTextToBubble(Common::String *text) {
	if (_bubbleTextFeature)
		error("Attempt to display two text objects");
	if (!text)
		text = &_bubbleText;
	if (text->empty())
		return;

	_currentBubbleText = *text;

	uint bubbleId = _vm->getCase()->getCurrScene()->getBubbleType();
	Common::Rect bounds;
	switch (bubbleId) {
	case 0:
		bounds = Common::Rect(15, 7, 625, 80);
		break;
	case 1:
		bounds = Common::Rect(160, 260, 625, 333);
		break;
	case 2:
		bounds = Common::Rect(356, 3, 639, 90);
		break;
	case 3:
		bounds = Common::Rect(10, 7, 380, 80);
		break;
	case 4:
		bounds = Common::Rect(15, 270, 625, 328);
		break;
	case 5:
		bounds = Common::Rect(15, 7, 550, 70);
		break;
	case 6:
		bounds = Common::Rect(0, 0, 313, 76);
		break;
	case 7:
		bounds = Common::Rect(200, 25, 502, 470);
		break;
	default:
		error("unknown bubble type %d in drawTextToBubble", bubbleId);
	}

	_bubbleTextFeature = _vm->getView()->installViewFeature(0, 0, NULL);
	_bubbleTextFeature->_data.bounds = bounds;
	_bubbleTextFeature->_data.bitmapIds[0] = 0;
	_bubbleTextFeature->_moveProc = (Module::FeatureProc)&CSTimeModule::bubbleTextMoveProc;
	_bubbleTextFeature->_drawProc = (Module::FeatureProc)&CSTimeModule::bubbleTextDrawProc;
	_bubbleTextFeature->_timeProc = NULL;
	_bubbleTextFeature->_flags = kFeatureOldSortForeground; // FIXME: not in original
}

void CSTimeInterface::closeBubble() {
	if (_bubbleTextFeature)
		_vm->getView()->removeFeature(_bubbleTextFeature, true);
	_bubbleTextFeature = NULL;
}

void CSTimeInterface::startDragging(uint16 id) {
	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[id];

	cursorSetShape(11);
	_draggedItem = id;

	if (_draggedItem == TIME_CUFFS_ID) {
		if (_inventoryDisplay->getCuffsShape() == 11) {
			_inventoryDisplay->setCuffsFlashing();
			_vm->getView()->idleView();
		}
	}

	uint32 dragFlags = (grabbedFromInventory() ? 0x800 : 0x600);
	_vm->getView()->dragFeature((NewFeature *)invObj->feature, _grabPoint, 4, dragFlags, NULL);

	if (_vm->getCase()->getId() == 1 && id == 2) {
		// Hardcoded behavior for the torch in the first case.
		if (_vm->getCase()->getCurrScene()->getId() == 4) {
			// This is the dark tomb.
			// FIXME: apply torch hack
			_vm->_caseVariable[2]++;
		} else {
			// FIXME: unapply torch hack
		}
	}

	_state = kCSTimeInterfaceStateDragging;

	if (grabbedFromInventory())
		return;

	// Hide the associated scene feature, if there is one.
	if (invObj->featureId != 0xffff) {
		CSTimeEvent event;
		event.type = kCSTimeEventDisableFeature;
		event.param2 = invObj->featureId;
		_vm->addEvent(event);
	}

	_vm->addEventList(invObj->events);
}

void CSTimeInterface::stopDragging() {
	CSTimeScene *scene = _vm->getCase()->getCurrScene();
	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_draggedItem];

	Common::Point mousePos = _vm->_system->getEventManager()->getMousePos();
	_state = kCSTimeInterfaceStateNormal;

	if (_sceneRect.contains(mousePos))
		scene->setCursorForCurrentPoint();
	else
		setCursorForCurrentPoint();

	// Find the inventory object hotspot which is topmost for this drop, if any.
	uint16 foundInvObjHotspot = 0xffff;
	const Common::Array<CSTimeHotspot> &hotspots = scene->getHotspots();
	for (uint i = 0; i < hotspots.size(); i++) {
		if (hotspots[i].state != 1)
			continue;
		if (!hotspots[i].region.containsPoint(mousePos))
			continue;
		for (uint j = 0; j < invObj->hotspots.size(); j++) {
			if (invObj->hotspots[j].sceneId != scene->getId())
				continue;
			if (invObj->hotspots[j].hotspotId != i)
				continue;
			if (foundInvObjHotspot != 0xffff && invObj->hotspots[foundInvObjHotspot].hotspotId < invObj->hotspots[j].hotspotId)
				continue;
			foundInvObjHotspot = j;
		}
	}

	// Work out if we're going to consume (nom-nom) the object after the drop.
	bool consumeObj = false;
	bool runConsumeEvents = false;
	if (foundInvObjHotspot != 0xffff) {
		CSTimeInventoryHotspot &hotspot = invObj->hotspots[foundInvObjHotspot];

		clearTextLine();
		for (uint i = 0; i < invObj->locations.size(); i++) {
			if (invObj->locations[i].sceneId != hotspot.sceneId)
				continue;
			if (invObj->locations[i].hotspotId != hotspot.hotspotId)
				continue;
			consumeObj = true;
			break;
		}

		if (_draggedItem == TIME_CUFFS_ID && !_inventoryDisplay->getCuffsState()) {
			consumeObj = false;
			// Nuh-uh, they're not activated yet.
			_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, _vm->getCase()->getCurrScene()->getHelperId(), 9902));
		} else {
			// FIXME: ding();
			runConsumeEvents = true;
		}
	}

	// Deal with the actual drop.
	if (grabbedFromInventory()) {
		if (!consumeObj) {
			_vm->getView()->dragFeature((NewFeature *)invObj->feature, mousePos, 2, 0x800, NULL);
			// TODO: playSound(151);
		} else if (_draggedItem != TIME_CUFFS_ID) {
			_vm->getView()->dragFeature((NewFeature *)invObj->feature, mousePos, 2, 0x600, NULL);
			_vm->_haveInvItem[_draggedItem] = 0;
			invObj->feature = NULL;
			invObj->featureDisabled = true;
			_inventoryDisplay->removeItem(_draggedItem);
		} else if (!_inventoryDisplay->getCuffsState()) {
			// Inactive cuffs.
			// TODO: We never actually get here? Which would explain why it makes no sense.
			_vm->getView()->dragFeature((NewFeature *)invObj->feature, mousePos, 2, 0x800, NULL);
			invObj->feature = NULL;
		} else {
			// Active cuffs.
			_vm->getView()->dragFeature((NewFeature *)invObj->feature, mousePos, 2, 0x600, NULL);
			_vm->_haveInvItem[_draggedItem] = 0;
			invObj->feature = NULL;
			invObj->featureDisabled = true;
		}

		if (runConsumeEvents) {
			_vm->addEventList(invObj->hotspots[foundInvObjHotspot].events);
		}

		_inventoryDisplay->draw();
	} else {
		uint32 dragFlags = 0x600;
		_vm->getView()->dragFeature((NewFeature *)invObj->feature, mousePos, 2, dragFlags, NULL);

		if (_inventoryDisplay->_invRect.contains(mousePos)) {
			// Dropped into the inventory.
			invObj->feature = NULL;
			if (invObj->canTake) {
				dropItemInInventory(_draggedItem);
				if (invObj->hotspotId)
					_vm->addEvent(CSTimeEvent(kCSTimeEventDisableHotspot, 0xffff, invObj->hotspotId));
			} else {
				if (invObj->featureId)
					_vm->addEvent(CSTimeEvent(kCSTimeEventAddFeature, 0xffff, invObj->featureId));
			}

			for (uint i = 0; i < invObj->hotspots.size(); i++) {
				if (invObj->hotspots[i].sceneId != scene->getId())
					continue;
				if (invObj->hotspots[i].hotspotId != 0xffff)
					continue;
				_vm->addEventList(invObj->hotspots[i].events);
			}
		} else {
			// Dropped into the scene.

			CSTimeEvent event;
			event.param1 = 0xffff;
			if (consumeObj) {
				invObj->feature = NULL;
				invObj->featureDisabled = true;
				event.type = kCSTimeEventDisableHotspot;
				event.param2 = invObj->hotspotId;
			} else {
				invObj->feature = NULL;
				event.type = kCSTimeEventAddFeature;
				event.param2 = invObj->featureId;
			}
			_vm->addEvent(event);

			if (runConsumeEvents) {
				_vm->addEventList(invObj->hotspots[foundInvObjHotspot].events);
			} else {
				for (uint i = 0; i < invObj->hotspots.size(); i++) {
					if (invObj->hotspots[i].sceneId != scene->getId())
						continue;
					if (invObj->hotspots[i].hotspotId != 0xfffe)
						continue;
					_vm->addEventList(invObj->hotspots[i].events);
				}
			}
		}
	}

	if (_vm->getCase()->getId() == 1 && _vm->getCase()->getCurrScene()->getId() == 4) {
		// Hardcoded behavior for torches in the dark tomb, in the first case.
		if (_draggedItem == 1 && foundInvObjHotspot == 0xffff) {
			// Trying to drag an unlit torch around?
			_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, 0, 16352));
		} else if (_draggedItem == 2 && _vm->_caseVariable[2] == 1) {
			// This the first time we tried dragging the lit torch around.
			_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, 0, 16354));
		}
	}

	// TODO: Is this necessary?
	_draggedItem = 0xffff;
}

void CSTimeInterface::setGrabPoint() {
	_grabPoint = _vm->_system->getEventManager()->getMousePos();
}

bool CSTimeInterface::grabbedFromInventory() {
	return (_inventoryDisplay->_invRect.contains(_grabPoint));
}

void CSTimeInterface::dropItemInInventory(uint16 id) {
	if (_vm->_haveInvItem[id])
		return;

	_vm->_haveInvItem[id] = 1;
	_vm->getCase()->_inventoryObjs[id]->feature = NULL;

	_inventoryDisplay->insertItemInDisplay(id);

	// TODO: deal with symbols

	if (_vm->getCase()->getCurrConversation()->getState() == (uint)~0 || _vm->getCase()->getCurrConversation()->getState() == 0) {
		// FIXME: additional check here
		// FIXME: play sound 151?
		_inventoryDisplay->draw();
		return;
	}

	// FIXME: ding();
	clearDialogArea();
	_inventoryDisplay->show();
	_inventoryDisplay->draw();
	_inventoryDisplay->setState(kCSTimeInterfaceDroppedInventory);
}

CSTimeHelp::CSTimeHelp(MohawkEngine_CSTime *vm) : _vm(vm) {
	_state = (uint)~0;
	_currEntry = 0xffff;
	_currHover = 0xffff;
	_nextToProcess = 0xffff;
}

CSTimeHelp::~CSTimeHelp() {
}

void CSTimeHelp::addQaR(uint16 text, uint16 speech) {
	CSTimeHelpQaR qar;
	qar.text = text;
	qar.speech = speech;
	_qars.push_back(qar);
}

void CSTimeHelp::start() {
	if (_vm->getInterface()->getInventoryDisplay()->getState() == 4)
		return;

	_state = 2;

	uint16 speech = 5900 + _vm->_rnd->getRandomNumberRng(0, 2);
	_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, _vm->getCase()->getCurrScene()->getHelperId(), speech));

	if (noHelperChanges())
		return;

	// Play a NIS, making sure the Good Guide is disabled.
	_vm->addEvent(CSTimeEvent(kCSTimeEventCharSetState, _vm->getCase()->getCurrScene()->getHelperId(), 0));
	_vm->addEvent(CSTimeEvent(kCSTimeEventCharPlayNIS, _vm->getCase()->getCurrScene()->getHelperId(), 0));
	_vm->addEvent(CSTimeEvent(kCSTimeEventCharSetState, _vm->getCase()->getCurrScene()->getHelperId(), 0));
}

void CSTimeHelp::end(bool runEvents) {
	_state = (uint)~0;
	_currHover = 0xffff;

	_vm->getInterface()->clearDialogArea();
	_vm->getInterface()->getInventoryDisplay()->show();

	if (noHelperChanges())
		return;

	_vm->addEvent(CSTimeEvent(kCSTimeEventCharSetState, _vm->getCase()->getCurrScene()->getHelperId(), 1));
	_vm->addEvent(CSTimeEvent(kCSTimeEventCharSomeNIS55, _vm->getCase()->getCurrScene()->getHelperId(), 1));
}

void CSTimeHelp::cleanupAfterFlapping() {
	if (_state == 2) {
		// Startup.
		_vm->getInterface()->getInventoryDisplay()->hide();
		selectStrings();
		display();
		_state = 1;
		return;
	}

	if (_nextToProcess == 0xffff)
		return;

	unhighlightLine(_nextToProcess);
	_nextToProcess = 0xffff;

	// TODO: case 18 hard-coding
}

void CSTimeHelp::mouseDown(Common::Point &pos) {
	for (uint i = 0; i < _qars.size(); i++) {
		Common::Rect thisRect = _vm->getInterface()->_dialogTextRect;
		thisRect.top += 1 + i*15;
		thisRect.bottom = thisRect.top + 15;
		if (!thisRect.contains(pos))
			continue;

		_currEntry = i;
		highlightLine(i);
		_vm->getInterface()->cursorSetShape(5);
	}
}

void CSTimeHelp::mouseMove(Common::Point &pos) {
	bool mouseIsDown = _vm->getEventManager()->getButtonState() & 1;

	for (uint i = 0; i < _qars.size(); i++) {
		Common::Rect thisRect = _vm->getInterface()->_dialogTextRect;
		thisRect.top += 1 + i*15;
		thisRect.bottom = thisRect.top + 15;
		if (!thisRect.contains(pos))
			continue;

		if (mouseIsDown) {
			if (i != _currEntry)
				break;
			highlightLine(i);
		}

		_vm->getInterface()->cursorOverHotspot();
		_currHover = i;
		return;
	}

	if (_currHover != 0xffff) {
		if (_vm->getInterface()->cursorGetShape() != 3) {
			unhighlightLine(_currHover);
			_vm->getInterface()->cursorSetShape(1);
		}
		_currHover = 0xffff;
	}
}

void CSTimeHelp::mouseUp(Common::Point &pos) {
	if (_currEntry == 0xffff || _qars[_currEntry].speech == 0) {
		_vm->getInterface()->cursorSetShape(1);
		end();
		return;
	}

	Common::Rect thisRect = _vm->getInterface()->_dialogTextRect;
	thisRect.top += 1 + _currEntry*15;
	thisRect.bottom = thisRect.top + 15;
	if (!thisRect.contains(pos))
		return;

	_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, _vm->getCase()->getCurrScene()->getHelperId(), 5900 + _qars[_currEntry].speech));
	_nextToProcess = _currEntry;
	_askedAlready.push_back(_qars[_currEntry].text);
}

void CSTimeHelp::display() {
	_vm->getInterface()->clearDialogArea();

	for (uint i = 0; i < _qars.size(); i++) {
		uint16 text = _qars[i].text;
		bool askedAlready = Common::find(_askedAlready.begin(), _askedAlready.end(), text) != _askedAlready.end();

		_vm->getInterface()->displayDialogLine(5900 + text, i, askedAlready ? 13 : 32);
	}
}

void CSTimeHelp::highlightLine(uint line) {
	uint16 text = _qars[line].text;
	_vm->getInterface()->displayDialogLine(5900 + text, line, 244);
}

void CSTimeHelp::unhighlightLine(uint line) {
	uint16 text = _qars[line].text;
	bool askedAlready = Common::find(_askedAlready.begin(), _askedAlready.end(), text) != _askedAlready.end();
	_vm->getInterface()->displayDialogLine(5900 + text, line, askedAlready ? 13 : 32);
}


void CSTimeHelp::selectStrings() {
	_qars.clear();
	_vm->getCase()->selectHelpStrings();
}

bool CSTimeHelp::noHelperChanges() {
	// These are hardcoded.
	if (_vm->getCase()->getId() == 4 && _vm->getCase()->getCurrScene()->getId() == 5)
		return true;
	if (_vm->getCase()->getId() == 5)
		return true;
	if (_vm->getCase()->getId() == 14 && _vm->getCase()->getCurrScene()->getId() == 4)
		return true;
	if (_vm->getCase()->getId() == 17 && _vm->getCase()->getCurrScene()->getId() == 2)
		return true;

	return false;
}

CSTimeInventoryDisplay::CSTimeInventoryDisplay(MohawkEngine_CSTime *vm, Common::Rect baseRect) : _vm(vm) {
	_state = 0;
	_cuffsState = false;
	_cuffsShape = 10;

	_invRect = baseRect;

	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		_itemRect[i].left = baseRect.left + 15 + i * 92;
		_itemRect[i].top = baseRect.top + 5;
		_itemRect[i].right = _itemRect[i].left + 90;
		_itemRect[i].bottom = _itemRect[i].top + 70;
	}
}

CSTimeInventoryDisplay::~CSTimeInventoryDisplay() {
}

void CSTimeInventoryDisplay::install() {
	uint count = _vm->getCase()->_inventoryObjs.size() - 1;

	// FIXME: some cases have hard-coded counts

	_vm->getView()->installGroup(9000, count, 0, true, 9000);
}

void CSTimeInventoryDisplay::draw() {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_displayedItems[i]];

		if (invObj->featureDisabled)
			continue;

		if (invObj->feature) {
			invObj->feature->resetFeatureScript(1, 0);
			continue;
		}

		// TODO: 0x2000 is set! help?
		uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop | 0x2000;
		if (i == TIME_CUFFS_ID) {
			// Time Cuffs are handled differently.
			// TODO: Can we not use _cuffsShape here?
			uint16 id = 100 + 10;
			if (_cuffsState) {
				id = 100 + 12;
				flags &= ~kFeatureNewNoLoop;
			}
			invObj->feature = _vm->getView()->installViewFeature(id, flags, NULL);
		} else {
			Common::Point pos((_itemRect[i].left + _itemRect[i].right) / 2, (_itemRect[i].top + _itemRect[i].bottom) / 2);
			uint16 id = 9000 + (invObj->id - 1);
			invObj->feature = _vm->getView()->installViewFeature(id, flags, &pos);
		}
	}
}

void CSTimeInventoryDisplay::show() {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_displayedItems[i]];
		if (!invObj->feature)
			continue;
		invObj->feature->show();
	}
}

void CSTimeInventoryDisplay::hide() {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_displayedItems[i]];
		if (!invObj->feature)
			continue;
		invObj->feature->hide(true);
	}
}

void CSTimeInventoryDisplay::idle() {
	if (_vm->getInterface()->getCarmenNote()->getState() ||
		_vm->getCase()->getCurrConversation()->getState() != 0xffff ||
		_vm->getInterface()->getHelp()->getState() != 0xffff) {
		if (_state == 4) {
			// FIXME: check timeout!
			hide();
			_vm->getCase()->getCurrConversation()->display();
			_state = 0;
		}
		return;
	}

	if (!_state)
		return;

	// FIXME: deal with actual inventory stuff
}

void CSTimeInventoryDisplay::clearDisplay() {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++)
		_displayedItems[i] = 0xffff;

	// We always start out with the Time Cuffs.
	_vm->_haveInvItem[TIME_CUFFS_ID] = 1;
	insertItemInDisplay(TIME_CUFFS_ID);

	_cuffsState = false;
}

void CSTimeInventoryDisplay::insertItemInDisplay(uint16 id) {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++)
		if (_displayedItems[i] == id)
			return;

	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++)
		if (_displayedItems[i] == 0xffff) {
			_displayedItems[i] = id;
			return;
		}

	error("couldn't insert item into display");
}

void CSTimeInventoryDisplay::removeItem(uint16 id) {
	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[id];
	if (invObj->feature) {
		_vm->getView()->removeFeature(invObj->feature, true);
		invObj->feature = NULL;
	}
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++)
		if (_displayedItems[i] == id)
			_displayedItems[i] = 0xffff;
}

void CSTimeInventoryDisplay::mouseDown(Common::Point &pos) {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		if (!_itemRect[i].contains(pos))
			continue;
		_draggedItem = i;
		_vm->getInterface()->cursorSetShape(8);
		_vm->getInterface()->setGrabPoint();
		_vm->getInterface()->setState(kCSTimeInterfaceStateDragStart);
	}
}

void CSTimeInventoryDisplay::mouseMove(Common::Point &pos) {
	bool mouseIsDown = _vm->getEventManager()->getButtonState() & 1;
	if (mouseIsDown && _vm->getInterface()->cursorGetShape() == 8) {
		Common::Point grabPoint = _vm->getInterface()->getGrabPoint();
		if (mouseIsDown && (abs(grabPoint.x - pos.x) > 2 || abs(grabPoint.y - pos.y) > 2)) {
			if (_vm->getInterface()->grabbedFromInventory()) {
				_vm->getInterface()->startDragging(getLastDisplayedClicked());
			} else {
				// TODO: CSTimeScene::mouseMove does quite a lot more, why not here too?
				_vm->getInterface()->startDragging(_vm->getCase()->getCurrScene()->getInvObjForCurrentHotspot());
			}
		}
	}

	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		if (!_itemRect[i].contains(pos))
			continue;
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_displayedItems[i]];
		Common::String text = "Pick up ";
		// TODO: special-case for case 11, scene 3, inv obj 1 (just use "Read " instead)
		text += _vm->getCase()->getRolloverText(invObj->stringId);
		_vm->getInterface()->displayTextLine(text);
		_vm->getInterface()->cursorOverHotspot();
		// FIXME: there's some trickery here to store the id for the below
		return;
	}

	if (false /* FIXME: if we get here and the stored id mentioned above is set.. */) {
		_vm->getInterface()->clearTextLine();
		if (_vm->getInterface()->getState() != kCSTimeInterfaceStateDragging) {
			if (_vm->getInterface()->cursorGetShape() != 3 && _vm->getInterface()->cursorGetShape() != 9)
				_vm->getInterface()->cursorSetShape(1);
		}
		// FIXME: reset that stored id
	}
}

void CSTimeInventoryDisplay::mouseUp(Common::Point &pos) {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == 0xffff)
			continue;
		if (!_itemRect[i].contains(pos))
			continue;
		// TODO: instead, stupid hack for case 11, scene 3, inv obj 1 (kCSTimeEventUnknown39, 0xffff, 0xffff)
		// TODO: instead, stupid hack for case 18, scene not 3, inv obj 4 (kCSTimeEventCondition, 1, 29)
		CSTimeEvent event;
		event.param1 = _vm->getCase()->getCurrScene()->getHelperId();
		if (event.param1 == 0xffff)
			event.type = kCSTimeEventSpeech;
		else
			event.type = kCSTimeEventCharStartFlapping;
		if (i == TIME_CUFFS_ID) {
			if (_cuffsState)
				event.param2 = 9903;
			else
				event.param2 = 9902;
		} else {
			event.param2 = 9905 + _displayedItems[i];
		}
		_vm->addEvent(event);
	}
}

void CSTimeInventoryDisplay::activateCuffs(bool active) {
	_cuffsState = active;
	if (!_cuffsState)
		return;

	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[TIME_CUFFS_ID];
	_cuffsShape = 11;
	if (invObj->feature)
		_vm->getView()->removeFeature(invObj->feature, true);
	uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop;
	invObj->feature = _vm->getView()->installViewFeature(100 + _cuffsShape, flags, NULL);
	invObj->featureDisabled = false;
}

void CSTimeInventoryDisplay::setCuffsFlashing() {
	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[TIME_CUFFS_ID];
	_cuffsShape = 12;
	if (invObj->feature)
		_vm->getView()->removeFeature(invObj->feature, true);
	uint32 flags = kFeatureSortStatic | 0x2000;
	invObj->feature = _vm->getView()->installViewFeature(100 + _cuffsShape, flags, NULL);
	invObj->featureDisabled = false;
}

bool CSTimeInventoryDisplay::isItemDisplayed(uint16 id) {
	for (uint i = 0; i < MAX_DISPLAYED_ITEMS; i++) {
		if (_displayedItems[i] == id)
			return true;
	}

	return false;
}

CSTimeBook::CSTimeBook(MohawkEngine_CSTime *vm) : _vm(vm) {
	_state = 0;
	_smallBookFeature = NULL;
}

CSTimeBook::~CSTimeBook() {
}

void CSTimeBook::drawSmallBook() {
	if (!_smallBookFeature) {
		_smallBookFeature = _vm->getView()->installViewFeature(101, kFeatureSortStatic | kFeatureNewNoLoop, NULL);
	} else {
		_smallBookFeature->resetFeature(false, NULL, 0);
	}
}

CSTimeCarmenNote::CSTimeCarmenNote(MohawkEngine_CSTime *vm) : _vm(vm) {
	_state = 0;
	_feature = NULL;
	clearPieces();
}

CSTimeCarmenNote::~CSTimeCarmenNote() {
}

void CSTimeCarmenNote::clearPieces() {
	for (uint i = 0; i < NUM_NOTE_PIECES; i++)
		_pieces[i] = 0xffff;
}

bool CSTimeCarmenNote::havePiece(uint16 piece) {
	for (uint i = 0; i < NUM_NOTE_PIECES; i++) {
		if (piece == 0xffff) {
			if (_pieces[i] != 0xffff)
				return true;
		} else if (_pieces[i] == piece)
			return true;
	}

	return false;
}

void CSTimeCarmenNote::addPiece(uint16 piece, uint16 speech) {
	uint i;
	for (i = 0; i < NUM_NOTE_PIECES; i++) {
		if (_pieces[i] == 0xffff) {
			_pieces[i] = piece;
			break;
		}
	}
	if (i == NUM_NOTE_PIECES)
		error("addPiece couldn't add piece to carmen note");

	// Get the Good Guide to say something.
	if (i == 2)
		speech = 9900; // Found the third piece.
	if (speech != 0xffff)
		_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, _vm->getCase()->getCurrScene()->getHelperId(), speech));

	// Remove the note feature, if any.
	uint16 noteFeatureId = _vm->getCase()->getNoteFeatureId(piece);
	if (noteFeatureId != 0xffff)
		_vm->addEvent(CSTimeEvent(kCSTimeEventDisableFeature, 0xffff, noteFeatureId));

	_vm->addEvent(CSTimeEvent(kCSTimeEventShowBigNote, 0xffff, 0xffff));

	if (i != 2)
		return;

	// TODO: special-casing for case 5

	_vm->addEvent(CSTimeEvent(kCSTimeEventCharPlayNIS, _vm->getCase()->getCurrScene()->getHelperId(), 3));

	// TODO: special-casing for case 5

	_vm->addEvent(CSTimeEvent(kCSTimeEventCharStartFlapping, _vm->getCase()->getCurrScene()->getHelperId(), 9901));
	_vm->addEvent(CSTimeEvent(kCSTimeEventActivateCuffs, 0xffff, 0xffff));
}

void CSTimeCarmenNote::drawSmallNote() {
	if (!havePiece(0xffff))
		return;

	uint16 id = 100;
	if (_pieces[2] != 0xffff)
		id += 5;
	else if (_pieces[1] != 0xffff)
		id += 4;
	else
		id += 2;

	if (_feature)
		_vm->getView()->removeFeature(_feature, true);
	_feature = _vm->getView()->installViewFeature(id, kFeatureSortStatic | kFeatureNewNoLoop, NULL);
}

void CSTimeCarmenNote::drawBigNote() {
	if (_vm->getCase()->getCurrConversation()->getState() != (uint)~0) {
		_vm->getCase()->getCurrConversation()->end(false);
	} else if (_vm->getInterface()->getHelp()->getState() != (uint)~0) {
		_vm->getInterface()->getHelp()->end();
	}
	// TODO: kill symbols too

	uint16 id = 100;
	if (_pieces[2] != 0xffff)
		id += 9;
	else if (_pieces[1] != 0xffff)
		id += 8;
	else
		id += 6;

	if (_feature)
		_vm->getView()->removeFeature(_feature, true);
	_feature = _vm->getView()->installViewFeature(id, kFeatureSortStatic | kFeatureNewNoLoop, NULL);
	// FIXME: attach note drawing proc
	_state = 2;
}

void CSTimeCarmenNote::closeNote() {
	_state = 0;
	drawSmallNote();
}

CSTimeOptions::CSTimeOptions(MohawkEngine_CSTime *vm) : _vm(vm) {
	_state = 0;
}

CSTimeOptions::~CSTimeOptions() {
}

} // End of namespace Mohawk
