/* 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 "mohawk/sound.h"
#include "common/events.h"
#include "common/system.h"
#include "common/textconsole.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;
}

// read a rect from a stream
Common::Rect readRect(Common::SeekableReadStream *stream) {
	Common::Rect rect;

	rect.left = stream->readSint16BE();
	rect.top = stream->readSint16BE();
	rect.right = stream->readSint16BE();
	rect.bottom = stream->readSint16BE();

	return rect;
}

void Region::loadFrom(Common::SeekableReadStream *stream) {
	uint16 rectCount = stream->readUint16BE();
	if (!rectCount) {
		// TODO: why this?
		stream->skip(2);
		rectCount = stream->readUint16BE();
	}
	for (uint i = 0; i < rectCount; i++) {
		Common::Rect rect = readRect(stream);
		if (!rect.isValidRect())
			continue;
		_rects.push_back(rect);
	}
}

bool Region::containsPoint(Common::Point &pos) const {
	for (uint i = 0; i < _rects.size(); i++)
		if (_rects[i].contains(pos))
			return true;

	return false;
}

CSTimeChar::CSTimeChar(MohawkEngine_CSTime *vm, CSTimeScene *scene, uint id) : _vm(vm), _scene(scene), _id(id) {
	_resting = true;
	_flappingState = 0xffff;
	_surfingState = 0;

	_NIS = NULL;
	_restFeature = NULL;
	_talkFeature = NULL;

	_talkFeature1 = NULL;
	_talkFeature2 = NULL;
	_talkFeature3 = NULL;
	_lastTime1 = 0;
	_lastTime2 = 0;
	_lastTime3 = 0;

	_unknown1 = _unknown2 = _unknown3 = 0;
	_enabled = false;
	_nextCue = 0;
	_waveStatus = 0;

	_playingWaveId = 0;
}

CSTimeChar::~CSTimeChar() {
}

void CSTimeChar::idle() {
	if (!_unknown2)
		return;

	if (_flappingState == 1) {
		idleTalk();
		return;
	}

	if (!_NIS)
		idleAmbients();
}

void CSTimeChar::setupAmbientAnims(bool onetime) {
	CSTimeConversation *conv = _vm->getCase()->getCurrConversation();
	if (_unknown1 == 0xffff || !_unknown2 || !_ambients.size() || !_resting || !_enabled ||
		(conv->getState() != (uint)~0 && conv->getSourceChar() == _id)) {
		setupRestPos();
		_resting = true;
		return;
	}

	removeChr();
	for (uint i = 0; i < _ambients.size(); i++) {
		// FIXME: check ambient condition
		uint32 flags = kFeatureSortStatic;
		if (_ambients[i].delay != 0xffff) {
			flags |= kFeatureNewNoLoop;
			if (onetime)
				flags |= kFeatureNewDisableOnReset;
		}
		installAmbientAnim(i, flags);
	}
}

void CSTimeChar::idleAmbients() {
	if (_flappingState != 0xffff)
		return;

	for (uint i = 0; i < _ambients.size(); i++) {
		if (!_ambients[i].feature)
			continue;
		uint16 delay = _ambients[i].delay;
		if (delay == 0xffff)
			continue;
		uint32 now = _vm->_system->getMillis();
		if (now < _ambients[i].nextTime)
			continue;
		_ambients[i].feature->resetFeatureScript(1, 0);
		_ambients[i].nextTime = now + delay;
	}
}

void CSTimeChar::stopAmbients(bool restpos) {
	for (uint i = 0; i < _ambients.size(); i++) {
		if (!_ambients[i].feature)
			continue;
		_vm->getView()->removeFeature(_ambients[i].feature, true);
		_ambients[i].feature = NULL;
	}

	if (restpos)
		setupRestPos();
}

void CSTimeChar::setupRestPos() {
	if (_unknown1 == 0xffff || !_unknown1 || !_unknown2)
		return;

	if (!_restFeature) {
		uint id = _enabled ? 0 : 13;
		uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop | kFeatureNewDisableOnReset;
		Feature *feature = _vm->getView()->installViewFeature(getChrBaseId() + id, flags, NULL);
		// FIXME: fix priorities
		_restFeature = feature;
	} else {
		_restFeature->resetFeatureScript(1, 0);
	}

	// FIXME: fix more priorities
}

void CSTimeChar::removeChr() {
	if (_unknown1 == 0xffff || !_unknown1)
		return;

	if (_talkFeature) {
		_vm->getView()->removeFeature(_talkFeature, true);
		_vm->getView()->removeFeature(_talkFeature3, true);
		if (_talkFeature1)
			_vm->getView()->removeFeature(_talkFeature1, true);
		if (_talkFeature2) // original checks unknown1 > 1, but this is silly when e.g. _enabled is false
			_vm->getView()->removeFeature(_talkFeature2, true);
	}

	if (_restFeature)
		_vm->getView()->removeFeature(_restFeature, true);

	_talkFeature1 = NULL;
	_talkFeature2 = NULL;
	_talkFeature3 = NULL;

	_talkFeature = NULL;
	_restFeature = NULL;
}

uint16 CSTimeChar::getChrBaseId() {
	return _scene->getSceneId() + (_id + 1) * 200;
}

uint CSTimeChar::getScriptCount() {
	static uint bases[4] = { 0, 10, 13, 21 };
	assert(_unknown1 < 4);
	return bases[_unknown1] + _ambients.size() + _unknown3;
}

void CSTimeChar::playNIS(uint16 id) {
	if (_NIS)
		removeNIS();
	stopAmbients(false);
	removeChr();
	uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop;
	_NIS = _vm->getView()->installViewFeature(getChrTypeScriptBase() + id + _ambients.size(), flags, NULL);
	// FIXME: fix priorities
}

bool CSTimeChar::NISIsDone() {
	return (_NIS->_data.paused || !_NIS->_data.enabled);
}

void CSTimeChar::removeNIS() {
	if (!_NIS)
		return;
	_vm->getView()->removeFeature(_NIS, true);
	_NIS = NULL;
}

void CSTimeChar::startFlapping(uint16 id) {
	if (!_unknown2)
		return;

	_scene->_activeChar = this;
	if (_restFeature) {
		_vm->getView()->removeFeature(_restFeature, true);
		_restFeature = NULL;
	}
	stopAmbients(false);
	setupTalk();
	_flappingState = 1;
	playFlapWave(id);
}

void CSTimeChar::interruptFlapping() {
	if (_playingWaveId)
		_vm->_sound->stopSound(_playingWaveId);
	// TODO: kill any other (preload) sound
	_waveStatus = 'q';
}

void CSTimeChar::installAmbientAnim(uint id, uint32 flags) {
	Feature *feature = _vm->getView()->installViewFeature(getChrTypeScriptBase() + id, flags, NULL);
	// FIXME: fix priorities

	_ambients[id].feature = feature;
	_ambients[id].nextTime = _vm->_system->getMillis() + _ambients[id].delay;
}

uint16 CSTimeChar::getChrTypeScriptBase() {
	static uint bases[4] = { 0, 10, 13, 21 };
	assert(_unknown1 < 4);
	return bases[_unknown1] + getChrBaseId();
}

void CSTimeChar::playFlapWave(uint16 id) {
	_playingWaveId = id;
	_vm->_sound->playSound(id, Audio::Mixer::kMaxChannelVolume, false, &_cueList);
	_nextCue = 0;
	_waveStatus = 'b';
}

void CSTimeChar::updateWaveStatus() {
	// This is a callback in the original, handling audio events.
	assert(_playingWaveId);

	// FIXME: state 's' for .. something? stopped?
	if (!_vm->_sound->isPlaying(_playingWaveId)) {
		_waveStatus = 'q';
		return;
	}

	uint samples = _vm->_sound->getNumSamplesPlayed(_playingWaveId);
	for (uint i = _nextCue; i < _cueList.pointCount; i++) {
		if (_cueList.points[i].sampleFrame > samples)
			return;
		if (_cueList.points[i].name.empty())
			warning("cue %d reached but was empty", i);
		else
			_waveStatus = _cueList.points[i].name[0];
		_nextCue = i + 1;
	}
}

void CSTimeChar::setupTalk() {
	if (_unknown1 == 0xffff || !_unknown1 || !_unknown2 || _talkFeature)
		return;

	uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop | kFeatureNewDisableOnReset;
	_talkFeature = _vm->getView()->installViewFeature(getChrBaseId() + (_enabled ? 1 : 14), flags, NULL);

	_talkFeature3 = _vm->getView()->installViewFeature(getChrBaseId() + (_enabled ? 4 : 15), flags, NULL);
	if (_enabled) {
		_talkFeature1 = _vm->getView()->installViewFeature(getChrBaseId() + 2, flags, NULL);
		if (_unknown1 > 1) {
			_talkFeature2 = _vm->getView()->installViewFeature(getChrBaseId() + 10, flags, NULL);
		}
	}
	// FIXME: fix priorities
}

void CSTimeChar::idleTalk() {
	updateWaveStatus();

	if (_waveStatus == 'q') {
		if (_surfingState) {
			// FIXME
			_surfingState = 0;
		} else {
			// FIXME
			_playingWaveId = 0;
		}
		stopFlapping();
		return;
	}

	if (_waveStatus == 's' && _surfingState) {
		// FIXME
		_waveStatus = 'q';
		return;
	}

	CSTimeView *view = _vm->getView();

	if (_enabled && view->getTime() > _lastTime1) {
		_lastTime1 = view->getTime() + 2000 + _vm->_rnd->getRandomNumberRng(0, 1000);
		if (_talkFeature1)
			_talkFeature1->resetFeatureScript(1, getChrBaseId() + 2 + _vm->_rnd->getRandomNumberRng(0, 1));
	}

	if (_enabled && view->getTime() > _lastTime2) {
		_lastTime2 = view->getTime() + 3000 + _vm->_rnd->getRandomNumberRng(0, 1000);
		if (_talkFeature2)
			_talkFeature2->resetFeatureScript(1, getChrBaseId() + 10 + _vm->_rnd->getRandomNumberRng(0, 2));
	}

	if (_waveStatus == 'c') {
		if (_talkFeature3)
			_talkFeature3->resetFeatureScript(1, getChrBaseId() + (_enabled ? 4 : 15));
	} else if (view->getTime() > _lastTime3) {
		_lastTime3 = view->getTime() + 100;
		if (_talkFeature3)
			_talkFeature3->resetFeatureScript(1, getChrBaseId() + (_enabled ? 4 : 15) + _vm->_rnd->getRandomNumberRng(1, 5));
	}

	// FIXME: more animations
}

void CSTimeChar::stopFlapping() {
	_flappingState = 0;
	removeChr();
	// FIXME: stupid hardcoded hack for case 5
	setupAmbientAnims(true);
}

CSTimeConversation::CSTimeConversation(MohawkEngine_CSTime *vm, uint id) : _vm(vm), _id(id) {
	clear();

	Common::SeekableReadStream *convStream = _vm->getResource(ID_CONV, id * 10 + 500);

	_nameId = convStream->readUint16BE();
	_greeting = convStream->readUint16BE();
	_greeting2 = convStream->readUint16BE();

	uint16 qarIds[8];
	for (uint i = 0; i < 8; i++)
		qarIds[i] = convStream->readUint16BE();

	delete convStream;

	for (uint i = 0; i < 8; i++) {
		// FIXME: are they always in order?
		if (qarIds[i] == 0xffff)
			continue;
		_qars.push_back(CSTimeQaR());
		CSTimeQaR &qar = _qars.back();
		loadQaR(qar, qarIds[i]);
	}
}

void CSTimeConversation::start() {
	uint16 greeting = _greeting;

	if (_talkCount > 1)
		greeting = _greeting2;

	_state = 2;

	if (greeting == 0xffff) {
		finishProcessingQaR();
		return;
	}

	CSTimeEvent event;
	event.type = kCSTimeEventCharStartFlapping;
	event.param1 = _sourceChar;
	event.param2 = greeting;
	_vm->addEvent(event);
}

void CSTimeConversation::finishProcessingQaR() {
	if (_state == 2) {
		_vm->getInterface()->getInventoryDisplay()->hide();
		_vm->getInterface()->clearTextLine();
		selectItemsToDisplay();
		display();
		return;
	}

	if (_nextToProcess == 0xffff)
		return;

	uint qarIndex = _itemsToDisplay[_nextToProcess];
	CSTimeQaR &qar = _qars[qarIndex];

	if (!qar.nextQaRsId) {
		end(true);
		_nextToProcess = 0xffff;
		return;
	}

	if (qar.responseStringId != 0xffff) {
		_vm->addEventList(qar.events);
	}

	if (qar.nextQaRsId == 0xffff) {
		_qars.remove_at(qarIndex);
		_vm->getInterface()->clearDialogLine(_nextToProcess);
		_nextToProcess = 0xffff;
		return;
	}

	loadQaR(qar, qar.nextQaRsId);
	if (qar.unknown2)
		qar.finished = true;
	_vm->getInterface()->displayDialogLine(qar.questionStringId, _nextToProcess, qar.finished ? 13 : 32);

	_nextToProcess = 0xffff;
}

void CSTimeConversation::end(bool useLastClicked, bool runEvents) {
	if (runEvents) {
		uint entry = _currEntry;
		if (!useLastClicked)
			entry = _itemsToDisplay.size() - 1;
		CSTimeQaR &qar = _qars[_itemsToDisplay[entry]];
		_vm->addEventList(qar.events);
		if (_sourceChar != 0xffff)
			_vm->getCase()->getCurrScene()->getChar(_sourceChar)->setupAmbientAnims(true);
	}

	CSTimeInterface *iface = _vm->getInterface();
	CSTimeInventoryDisplay *invDisplay = iface->getInventoryDisplay();
	if (invDisplay->getState() == 4) {
		invDisplay->hide();
		invDisplay->setState(0);
	}

	setState((uint)~0);
	_currHover = 0xffff;

	iface->clearTextLine();
	iface->clearDialogArea();
	invDisplay->show();

	// TODO: stupid case 20 stuff
}

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

	for (uint i = 0; i < _itemsToDisplay.size(); i++) {
		// FIXME: some rect stuff?

		CSTimeQaR &qar = _qars[_itemsToDisplay[i]];
		_vm->getInterface()->displayDialogLine(qar.questionStringId, i, qar.finished ? 13 : 32);
	}

	_state = 1;
}

void CSTimeConversation::selectItemsToDisplay() {
	_itemsToDisplay.clear();

	for (uint i = 0; i < _qars.size(); i++) {
		if (_qars[i].unknown1 != 0xffff && !_vm->getCase()->checkConvCondition(_qars[i].unknown1))
			continue;
		if (_itemsToDisplay.size() == 5)
			error("Too many conversation paths");
		_itemsToDisplay.push_back(i);
	}
}

void CSTimeConversation::mouseDown(Common::Point &pos) {
	if (_vm->getInterface()->getInventoryDisplay()->getState() == 4)
		return;

	// TODO: case 20 rect check

	for (uint i = 0; i < _itemsToDisplay.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, true);
		break;
	}
}

void CSTimeConversation::mouseMove(Common::Point &pos) {
	// TODO: case 20 rect check

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

	for (uint i = 0; i < _itemsToDisplay.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) {
			_vm->getInterface()->cursorSetShape(1, true);
			if (_vm->getInterface()->getInventoryDisplay()->getState() != 4)
				unhighlightLine(_currHover);
		}
		_currHover = 0xffff;
	}
}

void CSTimeConversation::mouseUp(Common::Point &pos) {
	if (_vm->getInterface()->getInventoryDisplay()->getState() == 4)
		return;

	if (_currEntry == 0xffff)
		return;

	// TODO: case 20 rect check

	CSTimeQaR &qar = _qars[_itemsToDisplay[_currEntry]];
	Common::Rect thisRect = _vm->getInterface()->_dialogTextRect;
	thisRect.top += 1 + _currEntry*15;
	thisRect.bottom = thisRect.top + 15;
	if (!thisRect.contains(pos))
		return;

	if (qar.responseStringId != 0xffff) {
		CSTimeEvent newEvent;
		newEvent.type = kCSTimeEventCharStartFlapping;
		newEvent.param1 = _sourceChar;
		newEvent.param2 = qar.responseStringId;
		_vm->addEvent(newEvent);
		_nextToProcess = _currEntry;
		return;
	}

	if (!qar.nextQaRsId) {
		_vm->getInterface()->cursorChangeShape(1);
		end(true);
		return;
	}

	_vm->addEventList(qar.events);
	_nextToProcess = _currEntry;
}

void CSTimeConversation::setAsked(uint qar, uint entry) {
	assert(qar < 8 && entry < 5);
	_asked[qar][entry] = true;
}

void CSTimeConversation::clear() {
	_state = (uint)~0;
	_talkCount = 0;
	_sourceChar = 0xffff;
	_currHover = 0xffff;
	_currEntry = 0xffff;
	_nextToProcess = 0xffff;
	for (uint i = 0; i < 8; i++)
		for (uint j = 0; j < 5; j++)
			_asked[i][j] = false;
}

void CSTimeConversation::loadQaR(CSTimeQaR &qar, uint16 id) {
	Common::SeekableReadStream *qarsStream = _vm->getResource(ID_QARS, id);
	qar.finished = false;
	qar.unknown1 = qarsStream->readUint16BE();
	qar.questionStringId = qarsStream->readUint16BE();
	qar.responseStringId = qarsStream->readUint16BE();
	qar.unknown2 = qarsStream->readUint16BE();
	qar.nextQaRsId = qarsStream->readUint16BE();
	uint16 numEvents = qarsStream->readUint16BE();
	for (uint j = 0; j < numEvents; j++) {
		CSTimeEvent event;
		event.type = qarsStream->readUint16BE();
		event.param1 = qarsStream->readUint16BE();
		event.param2 = qarsStream->readUint16BE();
		qar.events.push_back(event);
	}
}

void CSTimeConversation::highlightLine(uint line) {
	CSTimeQaR &qar = _qars[_itemsToDisplay[line]];
	_vm->getInterface()->displayDialogLine(qar.questionStringId, line, 244);
}

void CSTimeConversation::unhighlightLine(uint line) {
	CSTimeQaR &qar = _qars[_itemsToDisplay[line]];
	_vm->getInterface()->displayDialogLine(qar.questionStringId, line, qar.finished ? 13 : 32);
}

CSTimeCase::CSTimeCase(MohawkEngine_CSTime *vm, uint id) : _vm(vm), _id(id) {
	_vm->loadResourceFile(Common::String::format("Cases/C%dText", id));
	// We load this early, so we can use the text for debugging.
	loadRolloverText();

	_vm->loadResourceFile(Common::String::format("Cases/C%dInfo", id));
	Common::SeekableReadStream *caseInfoStream = _vm->getResource(ID_CINF, 1);
	uint16 numScenes = caseInfoStream->readUint16BE();
	uint16 numInvObjs = caseInfoStream->readUint16BE();
	uint16 numConversations = caseInfoStream->readUint16BE();
	for (uint i = 0; i < 3; i++)
		_noteFeatureId[i] = caseInfoStream->readUint16BE();
	delete caseInfoStream;

	debug("Loading %d inventory objects...", numInvObjs);
	for (uint i = 0; i < numInvObjs; i++)
		_inventoryObjs.push_back(loadInventoryObject(i));

	_vm->loadResourceFile(Common::String::format("Cases/C%dArt", id));
	_vm->loadResourceFile(Common::String::format("Cases/C%dDlog", id));

	debug("Loading %d scenes...", numScenes);
	for (uint i = 0; i < numScenes; i++)
		_scenes.push_back(new CSTimeScene(_vm, this, i + 1));

	debug("Loading %d conversations...", numConversations);
	for (uint i = 0; i < numConversations; i++)
		_conversations.push_back(new CSTimeConversation(_vm, i));

	assert(!_conversations.empty());
	_currConv = _conversations[0];

	_currScene = 0xffff;
}

CSTimeCase::~CSTimeCase() {
}

void CSTimeCase::loadRolloverText() {
	Common::SeekableReadStream *stringStream = _vm->getResource(ID_STRL, 9100);
	while (stringStream->pos() < stringStream->size())
		_rolloverText.push_back(readString(stringStream));
	for (uint i = 0; i < _rolloverText.size(); i++)
		debug("string %d: '%s'", i, _rolloverText[i].c_str());
	delete stringStream;
}

CSTimeInventoryObject *CSTimeCase::loadInventoryObject(uint id) {
	CSTimeInventoryObject *invObj = new CSTimeInventoryObject;
	invObj->feature = NULL;
	invObj->id = id;

	Common::SeekableReadStream *invObjStream = _vm->getResource(ID_INVO, id + 1);
	uint16 numHotspots = invObjStream->readUint16BE();
	invObj->stringId = invObjStream->readUint16BE();
	invObj->hotspotId = invObjStream->readUint16BE();
	invObj->featureId = invObjStream->readUint16BE();
	invObj->canTake = invObjStream->readUint16BE();
	invObj->featureDisabled = false;
	debug(" invobj '%s', hotspot id %d, feature id %d, can take %d", _rolloverText[invObj->stringId].c_str(), invObj->hotspotId, invObj->featureId, invObj->canTake);
	uint16 numConsumableLocations = invObjStream->readUint16BE();
	debug(" Loading %d consumable locations...", numConsumableLocations);
	for (uint i = 0; i < numConsumableLocations; i++) {
		CSTimeLocation location;
		location.sceneId = invObjStream->readUint16BE();
		location.hotspotId = invObjStream->readUint16BE();
		invObj->locations.push_back(location);
	}
	uint16 numEvents = invObjStream->readUint16BE();
	debug(" Loading %d events...", numEvents);
	for (uint i = 0; i < numEvents; i++) {
		CSTimeEvent event;
		event.type = invObjStream->readUint16BE();
		event.param1 = invObjStream->readUint16BE();
		event.param2 = invObjStream->readUint16BE();
		invObj->events.push_back(event);
	}
	debug(" Loading %d hotspots...", numHotspots);
	for (uint i = 0; i < numHotspots; i++) {
		CSTimeInventoryHotspot hotspot;
		hotspot.sceneId = invObjStream->readUint16BE();
		hotspot.hotspotId = invObjStream->readUint16BE();
		hotspot.stringId = invObjStream->readUint16BE();
		uint16 numHotspotEvents = invObjStream->readUint16BE();
		debug("  scene %d, hotspot %d, string id %d, with %d hotspot events", hotspot.sceneId, hotspot.hotspotId, hotspot.stringId, numHotspotEvents);
		for (uint j = 0; j < numHotspotEvents; j++) {
			CSTimeEvent event;
			event.type = invObjStream->readUint16BE();
			event.param1 = invObjStream->readUint16BE();
			event.param2 = invObjStream->readUint16BE();
			hotspot.events.push_back(event);
		}
		invObj->hotspots.push_back(hotspot);
	}
	delete invObjStream;

	return invObj;
}

CSTimeScene *CSTimeCase::getCurrScene() {
	return _scenes[_currScene - 1];
}

CSTimeScene::CSTimeScene(MohawkEngine_CSTime *vm, CSTimeCase *case_, uint id) : _vm(vm), _case(case_), _id(id) {
	_visitCount = 0;
	_activeChar = NULL;
	_currHotspot = 0xffff;
	_hoverHotspot = 0xffff;
	load();
}

void CSTimeScene::load() {
	Common::SeekableReadStream *sceneStream = _vm->getResource(ID_SCEN, _id + 1000);
	_unknown1 = sceneStream->readUint16BE();
	_unknown2 = sceneStream->readUint16BE();
	_helperId = sceneStream->readUint16BE();
	_bubbleType = sceneStream->readUint16BE();
	uint16 numHotspots = sceneStream->readUint16BE();
	_numObjects = sceneStream->readUint16BE();
	debug("Scene: unknowns %d, %d, %d, bubble type %d, %d objects", _unknown1, _unknown2, _helperId, _bubbleType, _numObjects);

	uint16 numChars = sceneStream->readUint16BE();
	uint16 numEvents = sceneStream->readUint16BE();
	debug(" Loading %d events...", numEvents);
	for (uint i = 0; i < numEvents; i++) {
		CSTimeEvent event;
		event.type = sceneStream->readUint16BE();
		event.param1 = sceneStream->readUint16BE();
		event.param2 = sceneStream->readUint16BE();
		_events.push_back(event);
	}
	uint16 numEvents2 = sceneStream->readUint16BE();
	debug(" Loading %d events2...", numEvents2);
	for (uint i = 0; i < numEvents2; i++) {
		CSTimeEvent event;
		event.type = sceneStream->readUint16BE();
		event.param1 = sceneStream->readUint16BE();
		event.param2 = sceneStream->readUint16BE();
		_events2.push_back(event);
	}
	debug(" Loading %d chars...", numChars);
	for (uint i = 0; i < numChars; i++) {
		CSTimeChar *chr = new CSTimeChar(_vm, this, i);
		if (!_activeChar)
			_activeChar = chr;
		chr->_enabled = true;
		chr->_unknown1 = sceneStream->readUint16BE();
		chr->_unknown2 = sceneStream->readUint16BE();
		uint16 numAmbients = sceneStream->readUint16BE();
		chr->_unknown3 = sceneStream->readUint16BE();
		debug("  unknowns %d, %d, %d, with %d ambients", chr->_unknown1, chr->_unknown2, chr->_unknown3, numAmbients);
		for (uint j = 0; j < numAmbients; j++) {
			CSTimeAmbient ambient;
			ambient.delay = sceneStream->readUint16BE();
			ambient.feature = NULL;
			chr->_ambients.push_back(ambient);
		}
		_chars.push_back(chr);
	}
	debug(" Loading %d hotspots...", numHotspots);
	for (uint i = 0; i < numHotspots; i++) {
		CSTimeHotspot hotspot;
		hotspot.stringId = sceneStream->readUint16BE();
		hotspot.invObjId = sceneStream->readUint16BE();
		hotspot.cursor = sceneStream->readUint16BE();
		hotspot.state = sceneStream->readUint16BE();
		uint16 numHotspotEvents = sceneStream->readUint16BE();
		debug("  hotspot '%s', inv obj %d, cursor %d, state %d, with %d hotspot events", _case->getRolloverText(hotspot.stringId).c_str(), hotspot.invObjId, hotspot.cursor, hotspot.state, numHotspotEvents);
		for (uint j = 0; j < numHotspotEvents; j++) {
			CSTimeEvent event;
			event.type = sceneStream->readUint16BE();
			event.param1 = sceneStream->readUint16BE();
			event.param2 = sceneStream->readUint16BE();
			hotspot.events.push_back(event);
		}
		_hotspots.push_back(hotspot);
	}
	delete sceneStream;

	Common::SeekableReadStream *hotspotStream = _vm->getResource(ID_HOTS, _id + 1100);
	for (uint i = 0; i < _hotspots.size(); i++) {
		_hotspots[i].region.loadFrom(hotspotStream);
	}
	delete hotspotStream;
}

void CSTimeScene::installGroup() {
	uint16 resourceId = getSceneId();
	_vm->getView()->installGroup(resourceId, _numObjects, 0, true, resourceId);

	for (uint i = 0; i < _chars.size(); i++) {
		uint count = _chars[i]->getScriptCount();
		if (!count)
			continue;
		_vm->getView()->installGroup(resourceId, count, 0, true, _chars[i]->getChrBaseId());
	}
}

void CSTimeScene::buildScene() {
	uint16 resourceId = getSceneId();

	_vm->getView()->installBG(resourceId);

	for (uint i = 0; i < _numObjects; i++) {
		if (!_case->checkObjectCondition(i)) {
			_objectFeatures.push_back(NULL);
			continue;
		}

		// FIXME: reset object if it already exists
		// FIXME: deal with NULL
		uint32 flags = kFeatureSortStatic | kFeatureNewNoLoop | kFeatureNewDisableOnReset;
		assert(flags == 0x4C00000);
		Feature *feature = _vm->getView()->installViewFeature(resourceId + i, flags, NULL);
		_objectFeatures.push_back(feature);
	}
}

void CSTimeScene::leave() {
	for (uint i = 0; i < _objectFeatures.size(); i++) {
		if (_objectFeatures[i] == NULL)
			continue;
		_vm->getView()->removeFeature(_objectFeatures[i], true);
		_objectFeatures[i] = NULL;
	}

	for (uint i = 0; i < _chars.size(); i++) {
		_chars[i]->stopAmbients(false);
		_chars[i]->removeChr();
		_chars[i]->removeNIS();
	}

	_vm->getView()->removeGroup(getSceneId());
}

uint16 CSTimeScene::getSceneId() {
	// FIXME: there are a lot of special cases
	uint16 resourceId = 10000 + 2000 * (_id - 1);
	return resourceId;
}

void CSTimeScene::mouseDown(Common::Point &pos) {
	CSTimeConversation *conv = _vm->getCase()->getCurrConversation();
	bool convActive = (conv->getState() != (uint)~0);
	bool helpActive = (_vm->getInterface()->getHelp()->getState() != (uint)~0);
	if (convActive || helpActive) {
		bool foundPoint = false;
		for (uint i = 0; i < _hotspots.size(); i++) {
			CSTimeHotspot &hotspot = _hotspots[i];
			if (!hotspot.region.containsPoint(pos))
				continue;
			foundPoint = true;

			if (!convActive) {
				// In help mode, we ignore clicks on any help hotspot.
				if (!hotspotContainsEvent(i, kCSTimeEventStartHelp))
					break;
				_currHotspot = 0xffff;
				return;
			}

			// In conversation mode, we ignore clicks which would restart the current conversation.
			for (uint j = 0; j < hotspot.events.size(); j++) {
				if (hotspot.events[j].type != kCSTimeEventStartConversation)
					continue;
				// FIXME: check that the conversation *is* the current one
				_currHotspot = 0xffff;
				return;
			}

			break;
		}

		if (foundPoint) {
			// We found a valid hotspot and we didn't ignore it, stop the conversation/help.
			if (convActive) {
				conv->end(false);
			} else {
				_vm->getInterface()->getHelp()->end();
			}
		}
	} else {
		// FIXME: check symbols
	}

	// FIXME: return if sailing puzzle

	_currHotspot = 0xffff;
	for (uint i = 0; i < _hotspots.size(); i++) {
		CSTimeHotspot &hotspot = _hotspots[i];
		if (!hotspot.region.containsPoint(pos))
			continue;
		if (hotspot.state != 1)
			continue;
		mouseDownOnHotspot(i);
		break;
	}

	if (_currHotspot == 0xffff)
		_vm->getInterface()->cursorSetShape(4, false);
}

void CSTimeScene::mouseMove(Common::Point &pos) {
	// TODO: if we're in sailing puzzle, return

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

	if (_vm->getInterface()->getState() == kCSTimeInterfaceStateDragStart) {
		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(_vm->getInterface()->getInventoryDisplay()->getLastDisplayedClicked());
			} else {
				CSTimeHotspot &hotspot = _hotspots[_currHotspot];
				if (_vm->_haveInvItem[hotspot.invObjId]) {
					_vm->getInterface()->setState(kCSTimeInterfaceStateNormal);
				} else {
					// FIXME: special-casing for cases 9, 13, 15, 18 19

					CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[hotspot.invObjId];
					if (invObj->feature)
						error("Inventory object already had feature when dragging from scene");
					uint16 id = 9000 + (invObj->id - 1);
					// FIXME: 0x2000 is set! help?
					uint32 flags = kFeatureNewNoLoop | 0x2000;
					invObj->feature = _vm->getView()->installViewFeature(id, flags, &grabPoint);
					invObj->featureDisabled = false;
					_vm->getInterface()->startDragging(hotspot.invObjId);
				}
			}
		}
	}

	for (uint i = 0; i < _hotspots.size(); i++) {
		CSTimeHotspot &hotspot = _hotspots[i];
		if (hotspot.state != 1)
			continue;
		if (!hotspot.region.containsPoint(pos))
			continue;

		if (_vm->getInterface()->getState() == kCSTimeInterfaceStateDragging) {
			// Only show a hotspot here if the dragged object can be dropped onto it.
			CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_vm->getInterface()->getDraggedNum()];
			bool showText = false;
			for (uint j = 0; j < invObj->hotspots.size(); j++) {
				if (invObj->hotspots[j].sceneId != _id)
					continue;
				if (invObj->hotspots[j].hotspotId != i)
					continue;
				showText = true;
			}
			if (!showText)
				continue;
		} else {
			// If we're not dragging but the mouse is down, ignore all hotspots
			// except the one which was clicked on.
			if (mouseIsDown && (i != _currHotspot))
				continue;
		}

		if (i != _hoverHotspot)
			_vm->getInterface()->clearTextLine();
		cursorOverHotspot(i);
		_hoverHotspot = i;
		return;
	}

	if (_vm->getInterface()->getState() != kCSTimeInterfaceStateDragging) {
		switch (_vm->getInterface()->cursorGetShape()) {
			case 2:
			case 13:
				_vm->getInterface()->cursorSetShape(1);
				break;
			case 5:
			case 14:
				_vm->getInterface()->cursorSetShape(4);
				break;
			case 11:
				_vm->getInterface()->cursorSetShape(10);
				break;
		}
	}

	if (_hoverHotspot == 0xffff)
		return;

	CSTimeConversation *conv = _case->getCurrConversation();
	CSTimeHelp *help = _vm->getInterface()->getHelp();
	if (conv->getState() != (uint)~0 && conv->getState() != 0) {
		Common::String text = "Talking to " + _case->getRolloverText(conv->getNameId());
		_vm->getInterface()->displayTextLine(text);
	} else if (help->getState() != (uint)~0 && help->getState() != 0) {
		Common::String text = "Talking to " + _case->getRolloverText(0);
		_vm->getInterface()->displayTextLine(text);
	} else if (_vm->getInterface()->getState() == kCSTimeInterfaceStateDragging) {
		// TODO: check symbols before this
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[_vm->getInterface()->getDraggedNum()];
		Common::String text = "Holding " + _case->getRolloverText(invObj->stringId);
		_vm->getInterface()->displayTextLine(text);
	} else {
		_vm->getInterface()->clearTextLine();
	}

	_hoverHotspot = 0xffff;
}

void CSTimeScene::mouseUp(Common::Point &pos) {
	// TODO: if sailing puzzle is active, return

	if (_currHotspot == 0xffff) {
		if (_vm->getInterface()->cursorGetShape() == 4)
			_vm->getInterface()->cursorChangeShape(1);
		return;
	}

	if (_vm->getInterface()->getState() == kCSTimeInterfaceStateDragStart)
		_vm->getInterface()->setState(kCSTimeInterfaceStateNormal);

	CSTimeHotspot &hotspot = _hotspots[_currHotspot];
	if (hotspot.region.containsPoint(pos) && hotspot.state == 1) {
		mouseUpOnHotspot(_currHotspot);
	} else {
		if (_vm->getInterface()->cursorGetShape() == 4 || _vm->getInterface()->cursorGetShape() == 14)
			_vm->getInterface()->cursorChangeShape(1);
	}
}

void CSTimeScene::idle() {
	// TODO: if sailing puzzle is active, idle it instead

	for (uint i = 0; i < _chars.size(); i++)
		_chars[i]->idle();
}

void CSTimeScene::setupAmbientAnims() {
	for (uint i = 0; i < _chars.size(); i++)
		_chars[i]->setupAmbientAnims(false);
}

void CSTimeScene::idleAmbientAnims() {
	if (_vm->NISIsRunning())
		return;

	for (uint i = 0; i < _chars.size(); i++)
		_chars[i]->idleAmbients();
}

bool CSTimeScene::eventIsActive() {
	return _vm->NISIsRunning() /* TODO || _vm->soundIsPlaying()*/ || _vm->getCurrentEventType() == kCSTimeEventWaitForClick
		|| _activeChar->_flappingState != 0xffff || _vm->getInterface()->getState() == kCSTimeInterfaceDroppedInventory;
}

void CSTimeScene::cursorOverHotspot(uint id) {
	CSTimeHotspot &hotspot = _hotspots[id];

	if (!_vm->getInterface()->cursorGetState())
		return;

	if (_vm->getInterface()->getState() == kCSTimeInterfaceStateDragging) {
		uint16 stringId = 0xffff;

		uint16 draggedId = _vm->getInterface()->getDraggedNum();
		CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[draggedId];
		for (uint j = 0; j < invObj->hotspots.size(); j++) {
			if (invObj->hotspots[j].sceneId != _id)
				continue;
			if (invObj->hotspots[j].hotspotId != id)
				continue;
			stringId = invObj->hotspots[j].stringId;
			break;
		}

		if (hotspot.stringId != 0xFFFF) {
			Common::String textLine;
			if (false) {
				// FIXME: special-case time cuffs
			} else {
				bool isChar = (hotspot.cursor == 1) && (draggedId != TIME_CUFFS_ID);
				textLine = (isChar ? "Give " : "Use ");
				textLine += _case->getRolloverText(invObj->stringId);
				textLine += (isChar ? " to " : " on ");
				textLine += _case->getRolloverText(stringId);
			}
			_vm->getInterface()->displayTextLine(textLine);
		}
	} else {
		if (hotspot.stringId != 0xFFFF)
			_vm->getInterface()->displayTextLine(_case->getRolloverText(hotspot.stringId));
	}

	if (_vm->getEventManager()->getButtonState() & 1) {
		if (_vm->getInterface()->getState() != kCSTimeInterfaceStateDragStart && _vm->getInterface()->getState() != kCSTimeInterfaceStateDragging) {
			CSTimeHotspot &currHotspot = _hotspots[_currHotspot];
			if (currHotspot.cursor == 2)
				_vm->getInterface()->cursorSetShape(14);
			else
				_vm->getInterface()->cursorSetShape(5);
		}
	} else {
		if (hotspot.cursor == 2)
			_vm->getInterface()->cursorSetShape(13);
		else if (_vm->getInterface()->cursorGetShape() != 8 && _vm->getInterface()->cursorGetShape() != 11)
			_vm->getInterface()->cursorSetShape(2);
	}
}

void CSTimeScene::mouseDownOnHotspot(uint id) {
	CSTimeHotspot &hotspot = _hotspots[id];

	_currHotspot = id;

	if (hotspot.invObjId == 0xffff || _vm->_haveInvItem[hotspot.invObjId]) {
		if (hotspot.cursor == 2)
			_vm->getInterface()->cursorChangeShape(14);
		else
			_vm->getInterface()->cursorChangeShape(5);
		return;
	}

	_vm->getInterface()->cursorSetShape(8, true);
	_vm->getInterface()->setGrabPoint();
	_vm->getInterface()->setState(kCSTimeInterfaceStateDragStart);

	CSTimeInventoryObject *invObj = _vm->getCase()->_inventoryObjs[hotspot.invObjId];
	_vm->getInterface()->displayTextLine("Pick up " + _case->getRolloverText(invObj->stringId));
}

void CSTimeScene::mouseUpOnHotspot(uint id) {
	CSTimeHotspot &hotspot = _hotspots[id];

	_vm->addEventList(hotspot.events);
	if (_vm->getInterface()->cursorGetShape() == 8 || (!hotspot.events.empty() && _vm->getInterface()->cursorGetShape() == 11))
		return;
	if (hotspot.cursor == 2)
		_vm->getInterface()->cursorChangeShape(13);
	else
		_vm->getInterface()->cursorChangeShape(2);
}

bool CSTimeScene::hotspotContainsEvent(uint id, uint16 eventType) {
	CSTimeHotspot &hotspot = _hotspots[id];

	for (uint i = 0; i < hotspot.events.size(); i++)
		if (hotspot.events[i].type == eventType)
			return true;

	return false;
}

void CSTimeScene::setCursorForCurrentPoint() {
	Common::Point mousePos = _vm->getEventManager()->getMousePos();

	for (uint i = 0; i < _hotspots.size(); i++) {
		if (!_hotspots[i].region.containsPoint(mousePos))
			continue;
		if (_hotspots[i].state != 1)
			continue;
		if (_hotspots[i].cursor == 2) {
			_vm->getInterface()->cursorSetShape(13);
		} else switch (_vm->getInterface()->cursorGetShape()) {
		case 8:
			break;
		case 12:
			_vm->getInterface()->cursorSetShape(11);
			break;
		default:
			_vm->getInterface()->cursorSetShape(2);
			break;
		}
		return;
	}

	_vm->getInterface()->cursorSetShape(1);
}

void CSTimeScene::drawHotspots() {
	for (uint i = 0; i < _hotspots.size(); i++) {
		for (uint j = 0; j < _hotspots[i].region._rects.size(); j++) {
			_vm->_gfx->drawRect(_hotspots[i].region._rects[j], 10 + 5*i);
		}
	}
}

const Common::Array<CSTimeEvent> &CSTimeScene::getEvents(bool second) {
	if (second)
		return _events2;
	else
		return _events;
}

} // End of namespace Mohawk
