/* 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.
 *
 * $URL: https://scummvm.svn.sourceforge.net/svnroot/scummvm/scummvm/tags/release-1-1-1/engines/saga/events.cpp $
 * $Id: events.cpp 48226 2010-03-10 20:38:39Z lordhoto $
 *
 */

// Event management module

#include "saga/saga.h"
#include "saga/gfx.h"

#include "saga/animation.h"
#include "saga/console.h"
#include "saga/scene.h"
#include "saga/interface.h"
#include "saga/palanim.h"
#include "saga/render.h"
#include "saga/sndres.h"
#include "saga/resource.h"
#include "saga/music.h"
#include "saga/actor.h"

#include "saga/events.h"

namespace Saga {

Events::Events(SagaEngine *vm) : _vm(vm) {
	debug(8, "Initializing event subsystem...");
}

Events::~Events() {
	debug(8, "Shutting down event subsystem...");
	freeList();
}

// Function to process event list once per frame.
// First advances event times, then processes each event with the appropriate
// handler depending on the type of event.
int Events::handleEvents(long msec) {
	long delta_time;
	int result;

	// Advance event times
	processEventTime(msec);

	// Process each event in list
	for (EventList::iterator eventi = _eventList.begin(); eventi != _eventList.end(); ++eventi) {
		Event *event_p = &*eventi;

		// Call the appropriate event handler for the specific event type
		switch (event_p->type) {

		case kEvTOneshot:
			result = handleOneShot(event_p);
			break;

		case kEvTContinuous:
			result = handleContinuous(event_p);
			break;

		case kEvTInterval:
			result = handleInterval(event_p);
			break;

		case kEvTImmediate:
			result = handleImmediate(event_p);
			break;

		default:
			result = kEvStInvalidCode;
			warning("Invalid event code encountered");
			break;
		}

		// Process the event appropriately based on result code from
		// handler
		if ((result == kEvStDelete) || (result == kEvStInvalidCode)) {
			// If there is no event chain, delete the base event.
			if (event_p->chain == NULL) {
				eventi = _eventList.reverse_erase(eventi);
			} else {
				// If there is an event chain present, move the next event
				// in the chain up, adjust it by the previous delta time,
				// and reprocess the event
				delta_time = event_p->time;
				Event *from_chain = event_p->chain;
				memcpy(event_p, from_chain, sizeof(*event_p));
				free(from_chain);

				event_p->time += delta_time;
				--eventi;
			}
		} else if (result == kEvStBreak) {
			break;
		}
	}

	return SUCCESS;
}

int Events::handleContinuous(Event *event) {
	double event_pc = 0.0; // Event completion percentage
	int event_done = 0;

	BGInfo bgInfo;
	Rect rect;
	if (event->duration != 0) {
		event_pc = ((double)event->duration - event->time) / event->duration;
	} else {
		event_pc = 1.0;
	}

	if (event_pc >= 1.0) {
		// Cap percentage to 100
		event_pc = 1.0;
		event_done = 1;
	}

	if (event_pc < 0.0) {
		// Event not signaled, skip it
		return kEvStContinue;
	} else if (!(event->code & kEvFSignaled)) {
		// Signal event
		event->code |= kEvFSignaled;
		event_pc = 0.0;
	}

	switch (event->code & EVENT_MASK) {
	case kPalEvent:
		switch (event->op) {
		case kEventBlackToPal:
			_vm->_gfx->blackToPal((PalEntry *)event->data, event_pc);
			break;
		case kEventPalToBlack:
			_vm->_gfx->palToBlack((PalEntry *)event->data, event_pc);
			break;
#ifdef ENABLE_IHNM
		case kEventPalFade:
			_vm->_gfx->palFade((PalEntry *)event->data, event->param, event->param2, event->param3, event->param4, event_pc);
			break;
#endif
		default:
			break;
		}
		break;
	case kTransitionEvent:
		switch (event->op) {
		case kEventDissolve:
			_vm->_scene->getBGInfo(bgInfo);
			rect.left = rect.top = 0;
			rect.right = bgInfo.bounds.width();
			rect.bottom = bgInfo.bounds.height();
			_vm->_render->getBackGroundSurface()->transitionDissolve(bgInfo.buffer, rect, 0, event_pc);
			_vm->_render->setFullRefresh(true);
			break;
		case kEventDissolveBGMask:
			// we dissolve it centered.
			// set flag of Dissolve to 1. It is a hack to simulate zero masking.
			int w, h;
			byte *maskBuffer;
			size_t len;

			_vm->_scene->getBGMaskInfo(w, h, maskBuffer, len);
			rect.left = (_vm->getDisplayInfo().width - w) / 2;
			rect.top = (_vm->getDisplayInfo().height - h) / 2;
			rect.setWidth(w);
			rect.setHeight(h);

			_vm->_render->getBackGroundSurface()->transitionDissolve( maskBuffer, rect, 1, event_pc);
			_vm->_render->setFullRefresh(true);
			break;
		default:
			break;
		}
		break;
	default:
		break;

	}

	if (event_done) {
		return kEvStDelete;
	}

	return kEvStContinue;
}

int Events::handleImmediate(Event *event) {
	double event_pc = 0.0; // Event completion percentage
	bool event_done = false;

	// Duration might be 0 so dont do division then
	if (event->duration != 0) {
		event_pc = ((double)event->duration - event->time) / event->duration;
	} else {
		// Just make sure that event_pc is 1.0 so event_done is true
		event_pc = 1.0;
	}

	if (event_pc >= 1.0) {
		// Cap percentage to 100
		event_pc = 1.0;
		event_done = true;
	}

	if (event_pc < 0.0) {
		// Event not signaled, skip it
		return kEvStBreak;
	} else if (!(event->code & kEvFSignaled)) {
		// Signal event
		event->code |= kEvFSignaled;
		event_pc = 0.0;
	}

	switch (event->code & EVENT_MASK) {
	case kPalEvent:
		switch (event->op) {
		case kEventBlackToPal:
			_vm->_gfx->blackToPal((PalEntry *)event->data, event_pc);
			break;
		case kEventPalToBlack:
			_vm->_gfx->palToBlack((PalEntry *)event->data, event_pc);
			break;
#ifdef ENABLE_IHNM
		case kEventPalFade:
			_vm->_gfx->palFade((PalEntry *)event->data, event->param, event->param2, event->param3, event->param4, event_pc);
			break;
#endif
		default:
			break;
		}
		break;
	case kScriptEvent:
	case kBgEvent:
	case kInterfaceEvent:
	case kSceneEvent:
	case kAnimEvent:
	case kCutawayEvent:
	case kActorEvent:
		handleOneShot(event);
		event_done = true;
		break;
	default:
		warning("Unhandled Immediate event type (%d)", event->code & EVENT_MASK);
		break;

	}

	if (event_done) {
		return kEvStDelete;
	}

	return kEvStBreak;
}

int Events::handleOneShot(Event *event) {
	Rect rect;


	if (event->time > 0) {
		return kEvStContinue;
	}

	// Event has been signaled

	switch (event->code & EVENT_MASK) {
	case kTextEvent:
		switch (event->op) {
		case kEventDisplay:
			((TextListEntry *)event->data)->display = true;
			break;
		case kEventRemove: {
			TextListEntry entry = *((TextListEntry *)event->data);
			_vm->_scene->_textList.remove(entry);
			} break;
		default:
			break;
		}

		break;
	case kSoundEvent:
		_vm->_sound->stopSound();
		if (event->op == kEventPlay)
			_vm->_sndRes->playSound(event->param, event->param2, event->param3 != 0);
		break;
	case kVoiceEvent:
		_vm->_sndRes->playVoice(event->param);
		break;
	case kMusicEvent:
		_vm->_music->stop();
		if (event->op == kEventPlay)
			_vm->_music->play(event->param, (MusicFlags)event->param2);
		break;
	case kBgEvent:
		{
			Surface *backGroundSurface = _vm->_render->getBackGroundSurface();
			BGInfo bgInfo;

			if (!(_vm->_scene->getFlags() & kSceneFlagISO)) {
				_vm->_scene->getBGInfo(bgInfo);

				backGroundSurface->blit(bgInfo.bounds, bgInfo.buffer);

				// If it is inset scene then draw black border
				if (bgInfo.bounds.width() < _vm->getDisplayInfo().width || bgInfo.bounds.height() < _vm->_scene->getHeight()) {
					Common::Rect rect1(2, bgInfo.bounds.height() + 4);
					Common::Rect rect2(bgInfo.bounds.width() + 4, 2);
					Common::Rect rect3(2, bgInfo.bounds.height() + 4);
					Common::Rect rect4(bgInfo.bounds.width() + 4, 2);
					rect1.moveTo(bgInfo.bounds.left - 2, bgInfo.bounds.top - 2);
					rect2.moveTo(bgInfo.bounds.left - 2, bgInfo.bounds.top - 2);
					rect3.moveTo(bgInfo.bounds.right, bgInfo.bounds.top - 2);
					rect4.moveTo(bgInfo.bounds.left - 2, bgInfo.bounds.bottom);

					backGroundSurface->drawRect(rect1, kITEColorBlack);
					backGroundSurface->drawRect(rect2, kITEColorBlack);
					backGroundSurface->drawRect(rect3, kITEColorBlack);
					backGroundSurface->drawRect(rect4, kITEColorBlack);
				}

				if (event->param == kEvPSetPalette) {
					PalEntry *palPointer;

#ifdef ENABLE_IHNM
					if (_vm->getGameId() == GID_IHNM) {
						if (_vm->_spiritualBarometer > 255)
							_vm->_gfx->setPaletteColor(kIHNMColorPortrait, 0xff, 0xff, 0xff);
						else
							_vm->_gfx->setPaletteColor(kIHNMColorPortrait,
								_vm->_spiritualBarometer * _vm->_interface->_portraitBgColor.red / 256,
								_vm->_spiritualBarometer * _vm->_interface->_portraitBgColor.green / 256,
								_vm->_spiritualBarometer * _vm->_interface->_portraitBgColor.blue / 256);
					}
#endif

					_vm->_scene->getBGPal(palPointer);
					_vm->_gfx->setPalette(palPointer);
				}
			}
			_vm->_render->clearFlag(RF_DISABLE_ACTORS);
		}
		break;
	case kPsychicProfileBgEvent:
		{
		ResourceContext *context = _vm->_resource->getContext(GAME_RESOURCEFILE);

		byte *resourceData;
		size_t resourceDataLength;

		_vm->_resource->loadResource(context, _vm->getResourceDescription()->psychicProfileResourceId, resourceData, resourceDataLength);

		byte *buf;
		size_t buflen;
		int width;
		int height;

		_vm->decodeBGImage(resourceData, resourceDataLength, &buf, &buflen, &width, &height);

		const PalEntry *palette = (const PalEntry *)_vm->getImagePal(resourceData, resourceDataLength);

		const Rect profileRect(width, height);

		_vm->_render->getBackGroundSurface()->blit(profileRect, buf);
		_vm->_render->addDirtyRect(profileRect);
		_vm->_frameCount++;

		_vm->_gfx->setPalette(palette);

		free(buf);
		free(resourceData);

		// Draw the scene. It won't be drawn by Render::drawScene(), as a placard is up
		_vm->_scene->draw();
		}
		break;
	case kAnimEvent:
		switch (event->op) {
		case kEventPlay:
			_vm->_anim->play(event->param, event->time, true);
			break;
		case kEventStop:
			_vm->_anim->stop(event->param);
			break;
		case kEventFrame:
			_vm->_anim->play(event->param, event->time, false);
			break;
		case kEventSetFlag:
			_vm->_anim->setFlag(event->param, event->param2);
			break;
		case kEventClearFlag:
			_vm->_anim->clearFlag(event->param, event->param2);
			break;
		case kEventResumeAll:
			_vm->_anim->resumeAll();
			break;
		default:
			break;
		}
		break;
	case kSceneEvent:
		switch (event->op) {
		case kEventDraw:
			{
				BGInfo bgInfo;
				_vm->_scene->getBGInfo(bgInfo);
				_vm->_render->getBackGroundSurface()->blit(bgInfo.bounds, bgInfo.buffer);
				_vm->_render->addDirtyRect(bgInfo.bounds);
				_vm->_scene->draw();
			}
			break;
		case kEventEnd:
			_vm->_scene->nextScene();
			return kEvStBreak;
		default:
			break;
		}
		break;
	case kPalAnimEvent:
		switch (event->op) {
		case kEventCycleStart:
			_vm->_palanim->cycleStart();
			break;
		case kEventCycleStep:
			_vm->_palanim->cycleStep(event->time);
			break;
		default:
			break;
		}
		break;
	case kInterfaceEvent:
		switch (event->op) {
		case kEventActivate:
			_vm->_interface->activate();
			break;
		case kEventDeactivate:
			_vm->_interface->deactivate();
			break;
		case kEventSetStatus:
			_vm->_interface->setStatusText((const char*)event->data);
			_vm->_interface->drawStatusBar();
			break;
		case kEventClearStatus:
			_vm->_interface->setStatusText("");
			_vm->_interface->drawStatusBar();
			break;
		case kEventSetFadeMode:
			_vm->_interface->setFadeMode(event->param);
			break;
		case kEventRestoreMode:
			_vm->_interface->restoreMode();
			break;
		case kEventSetMode:
			_vm->_interface->setMode(event->param);
			break;
		default:
			break;
		}
		break;
	case kScriptEvent:
		switch (event->op) {
		case kEventExecBlocking:
		case kEventExecNonBlocking: {
			debug(6, "Exec module number %ld script entry number %ld", event->param, event->param2);

			ScriptThread &sthread = _vm->_script->createThread(event->param, event->param2);
			sthread._threadVars[kThreadVarAction] = event->param3;
			sthread._threadVars[kThreadVarObject] = event->param4;
			sthread._threadVars[kThreadVarWithObject] = event->param5;
			sthread._threadVars[kThreadVarActor] = event->param6;

			if (event->op == kEventExecBlocking)
				_vm->_script->completeThread();

			break;
			}
		case kEventThreadWake:
			_vm->_script->wakeUpThreads(event->param);
			break;
		}
		break;
	case kCursorEvent:
		switch (event->op) {
		case kEventShow:
			_vm->_gfx->showCursor(true);
			break;
		case kEventHide:
			_vm->_gfx->showCursor(false);
			break;
		case kEventSetNormalCursor:
			// in ITE and IHNM demo there is just one cursor
			// ITE never makes this call
			if (!(_vm->getFeatures() & GF_IHNM_DEMO))
				_vm->_gfx->setCursor(kCursorNormal);
			break;
		case kEventSetBusyCursor:
			// in ITE and IHNM demo there is just one cursor
			// ITE never makes this call
			if (!(_vm->getFeatures() & GF_IHNM_DEMO))
				_vm->_gfx->setCursor(kCursorBusy);
			break;
		default:
			break;
		}
		break;
	case kGraphicsEvent:
		switch (event->op) {
		case kEventFillRect:
			rect.top = event->param2;
			rect.bottom = event->param3;
			rect.left = event->param4;
			rect.right = event->param5;
			_vm->_gfx->drawRect(rect, event->param);
			break;
		case kEventSetFlag:
			_vm->_render->setFlag(event->param);
			break;
		case kEventClearFlag:
			_vm->_render->clearFlag(event->param);
			break;
		default:
			break;
		}
#ifdef ENABLE_IHNM
	case kCutawayEvent:
		switch (event->op) {
		case kEventClear:
			_vm->_anim->clearCutaway();
			break;
		case kEventShowCutawayBg:
			_vm->_anim->showCutawayBg(event->param);
			break;
		default:
			break;
		}
#endif
	case kActorEvent:
		switch (event->op) {
		case kEventMove:
			// TODO (check Actor::direct)
			break;
		default:
			break;
		}
	default:
		break;
	}

	return kEvStDelete;
}

int Events::handleInterval(Event *event) {
	return kEvStDelete;
}

// Schedules an event in the event list; returns a pointer to the scheduled
// event suitable for chaining if desired.
Event *Events::queue(Event *event) {
	Event *queuedEvent;

	_eventList.push_back(*event);
	queuedEvent = &*--_eventList.end();
	initializeEvent(queuedEvent);

	return queuedEvent;
}

// Places a 'add_event' on the end of an event chain given by 'head_event'
// (head_event may be in any position in the event chain)
Event *Events::chain(Event *headEvent, Event *addEvent) {
	if (headEvent == NULL) {
		return queue(addEvent);
	}

	Event *walkEvent;
	for (walkEvent = headEvent; walkEvent->chain != NULL; walkEvent = walkEvent->chain) {
		continue;
	}

	walkEvent->chain = (Event *)malloc(sizeof(*walkEvent->chain));
	*walkEvent->chain = *addEvent;
	initializeEvent(walkEvent->chain);

	return walkEvent->chain;
}

int Events::initializeEvent(Event *event) {
	event->chain = NULL;
	switch (event->type) {
	case kEvTOneshot:
		break;
	case kEvTContinuous:
	case kEvTImmediate:
		event->time += event->duration;
		break;
	case kEvTInterval:
		break;
	default:
		return FAILURE;
	}

	return SUCCESS;
}

int Events::clearList(bool playQueuedMusic) {
	Event *chain_walk;
	Event *next_chain;

	// Walk down event list
	for (EventList::iterator eventi = _eventList.begin(); eventi != _eventList.end(); ++eventi) {

		// Only remove events not marked kEvFNoDestory (engine events)
		if (!(eventi->code & kEvFNoDestory)) {
			// Handle queued music change events before deleting them
			// This can happen in IHNM by music events set by sfQueueMusic()
			// Fixes bug #2057987 - "IHNM: Music stops in Ellen's chapter"
			if (playQueuedMusic && ((eventi->code & EVENT_MASK) == kMusicEvent)) {
				_vm->_music->stop();
				if (eventi->op == kEventPlay)
					_vm->_music->play(eventi->param, (MusicFlags)eventi->param2);
			}

			// Remove any events chained off this one
			for (chain_walk = eventi->chain; chain_walk != NULL; chain_walk = next_chain) {
				next_chain = chain_walk->chain;
				free(chain_walk);
			}
			eventi = _eventList.reverse_erase(eventi);
		}
	}

	return SUCCESS;
}

// Removes all events from the list (even kEvFNoDestory)
int Events::freeList() {
	Event *chain_walk;
	Event *next_chain;

	// Walk down event list
	EventList::iterator eventi = _eventList.begin();
	while (eventi != _eventList.end()) {

		// Remove any events chained off this one */
		for (chain_walk = eventi->chain; chain_walk != NULL; chain_walk = next_chain) {
			next_chain = chain_walk->chain;
			free(chain_walk);
		}
		eventi = _eventList.erase(eventi);
	}

	return SUCCESS;
}

// Walks down the event list, updating event times by 'msec'.
int Events::processEventTime(long msec) {
	uint16 event_count = 0;

	for (EventList::iterator eventi = _eventList.begin(); eventi != _eventList.end(); ++eventi) {
		eventi->time -= msec;
		event_count++;

		if (eventi->type == kEvTImmediate)
			break;

		if (event_count > EVENT_WARNINGCOUNT) {
			warning("Event list exceeds %u", EVENT_WARNINGCOUNT);
		}
	}

	return SUCCESS;
}

} // End of namespace Saga
