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

// 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...");
}

// 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.
void 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->front();

		// 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 (eventi->size() < 2) {
				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;
				eventi->pop_front();
				event_p = &eventi->front();
				event_p->time += delta_time;
				--eventi;
			}
		} else if (result == kEvStBreak) {
			break;
		}
	}
}

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;

			_vm->_scene->getBGMaskInfo(w, h, maskBuffer);
			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:
		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) {
						PalEntry portraitBgColor = _vm->_interface->_portraitBgColor;
						byte portraitColor = (_vm->getLanguage() == Common::ES_ESP) ? 253 : 254;

						// Set the portrait bg color, in case a saved state is restored from the
						// launcher. In this case, sfSetPortraitBgColor is not called, thus the
						// portrait color will always be 0 (black).
						if (portraitBgColor.red == 0 && portraitBgColor.green == 0 && portraitBgColor.blue == 0)
							portraitBgColor.green = 255;

						if (_vm->_spiritualBarometer > 255)
							_vm->_gfx->setPaletteColor(portraitColor, 0xff, 0xff, 0xff);
						else
							_vm->_gfx->setPaletteColor(portraitColor,
								_vm->_spiritualBarometer * portraitBgColor.red / 256,
								_vm->_spiritualBarometer * portraitBgColor.green / 256,
								_vm->_spiritualBarometer * 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);

		ByteArray resourceData;

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

		ByteArray image;
		int width;
		int height;

		_vm->decodeBGImage(resourceData, image, &width, &height);

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

		const Rect profileRect(width, height);

		_vm->_render->getBackGroundSurface()->blit(profileRect, image.getBuffer());
		_vm->_render->addDirtyRect(profileRect);
		_vm->_frameCount++;

		_vm->_gfx->setPalette(palette);

		// 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->isIHNMDemo())
				_vm->_gfx->setCursor(kCursorNormal);
			break;
		case kEventSetBusyCursor:
			// in ITE and IHNM demo there is just one cursor
			// ITE never makes this call
			if (!_vm->isIHNMDemo())
				_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;
		}
		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;
		}
		break;
#endif
	case kActorEvent:
		switch (event->op) {
		case kEventMove:
			// TODO (check Actor::direct)
			break;
		default:
			break;
		}
		break;
	default:
		break;
	}

	return kEvStDelete;
}

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

EventColumns *Events::chain(EventColumns *eventColumns, const Event &event) {

	if (eventColumns == NULL) {
		EventColumns tmp;

		_eventList.push_back(tmp);
		eventColumns = &_eventList.back();
	}

	eventColumns->push_back(event);
	initializeEvent(eventColumns->back());

	return eventColumns;
}

EventColumns *Events::chainMusic(EventColumns *eventColumns, long musicId, bool loop, long time) {
	Event event;

	event.type = kEvTOneshot;
	event.code = kMusicEvent;
	event.param = musicId;
	event.param2 = loop ? MUSIC_NORMAL : MUSIC_LOOP;
	event.op = kEventPlay;
	event.time = time;
	return chain(eventColumns, event);
}

void Events::initializeEvent(Event &event) {
	switch (event.type) {
	case kEvTOneshot:
		break;
	case kEvTContinuous:
	case kEvTImmediate:
		event.time += event.duration;
		break;
	case kEvTInterval:
		break;
	}
}

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

		// Only remove events not marked kEvFNoDestory (engine events)
		if (!(eventi->front().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->front().code & EVENT_MASK) == kMusicEvent)) {
				_vm->_music->stop();
				if (eventi->front().op == kEventPlay)
					_vm->_music->play(eventi->front().param, (MusicFlags)eventi->front().param2);
			}

			eventi = _eventList.reverse_erase(eventi);
		}
	}
}

// Removes all events from the list (even kEvFNoDestory)
void Events::freeList() {
	_eventList.clear();
}

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

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

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

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

} // End of namespace Saga
