/* 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-0-12-0/engines/parallaction/parallaction.cpp $
 * $Id: parallaction.cpp 33033 2008-07-13 13:04:36Z peres001 $
 *
 */

#include "common/config-manager.h"
#include "common/events.h"
#include "common/file.h"
#include "common/util.h"

#include "sound/mididrv.h"
#include "sound/mixer.h"


#include "parallaction/input.h"
#include "parallaction/parallaction.h"
#include "parallaction/debug.h"
#include "parallaction/sound.h"



namespace Parallaction {

// FIXME: remove this
Parallaction *_vm = NULL;

// public stuff

char		_saveData1[30] = { '\0' };
uint16		_language = 0;
uint32		_engineFlags = 0;

uint16		_score = 1;
char		_password[8];

uint32		_commandFlags = 0;

// private stuff


Parallaction::Parallaction(OSystem *syst, const PARALLACTIONGameDescription *gameDesc) :
	Engine(syst), _gameDescription(gameDesc), _char(this) {

	// FIXME: Fingolfin asks: why is there a FIXME here? Please either clarify what
	// needs fixing, or remove it!
	// FIXME
	_vm = this;

	Common::File::addDefaultDirectory( _gameDataPath );

	Common::addSpecialDebugLevel(kDebugDialogue, "dialogue", "Dialogues debug level");
	Common::addSpecialDebugLevel(kDebugParser, "parser", "Parser debug level");
	Common::addSpecialDebugLevel(kDebugDisk, "disk", "Disk debug level");
	Common::addSpecialDebugLevel(kDebugWalk, "walk", "Walk debug level");
	Common::addSpecialDebugLevel(kDebugGraphics, "gfx", "Gfx debug level");
	Common::addSpecialDebugLevel(kDebugExec, "exec", "Execution debug level");
	Common::addSpecialDebugLevel(kDebugInput, "input", "Input debug level");
	Common::addSpecialDebugLevel(kDebugAudio, "audio", "Audio debug level");
	Common::addSpecialDebugLevel(kDebugMenu, "menu", "Menu debug level");
	Common::addSpecialDebugLevel(kDebugInventory, "inventory", "Inventory debug level");

	syst->getEventManager()->registerRandomSource(_rnd, "parallaction");
}


Parallaction::~Parallaction() {
	delete _debugger;
	delete _globalTable;
	delete _callableNames;
	delete _cmdExec;
	delete _programExec;

	_gfx->clearGfxObjects(kGfxObjCharacter | kGfxObjNormal);
	hideDialogueStuff();
	delete _balloonMan;
	freeLocation();

	freeCharacter();
	destroyInventory();

	delete _localFlagNames;
	delete _gfx;
	delete _soundMan;
	delete _disk;
	delete _input;
}


int Parallaction::init() {

	_engineFlags = 0;
	_objectsNames = NULL;
	_globalTable = NULL;
	_location._hasSound = false;
	_baseTime = 0;
	_numLocations = 0;
	_location._startPosition.x = -1000;
	_location._startPosition.y = -1000;
	_location._startFrame = 0;
	_location._comment = NULL;
	_location._endComment = NULL;

	_pathBuffer = 0;

	_screenSize = _screenWidth * _screenHeight;

	strcpy(_characterName1, "null");

	memset(_locationNames, 0, NUM_LOCATIONS * 32);

	initInventory();	// needs to be pushed into subclass

	_input = new Input(this);

	_gfx = new Gfx(this);

	_debugger = new Debugger(this);

	setupBalloonManager();

	return 0;
}


void Parallaction::clearSet(OpcodeSet &opcodes) {
	for (Common::Array<const Opcode*>::iterator i = opcodes.begin(); i != opcodes.end(); ++i)
		delete *i;
	opcodes.clear();
}


void Parallaction::updateView() {

	if ((_engineFlags & kEnginePauseJobs) && (_engineFlags & kEngineInventory) == 0) {
		return;
	}

	_gfx->animatePalette();
	_gfx->updateScreen();
	g_system->delayMillis(30);
}



void Parallaction::freeCharacter() {
	debugC(1, kDebugExec, "freeCharacter()");

	delete _objectsNames;
	_objectsNames = 0;

	_gfx->clearGfxObjects(kGfxObjCharacter);

	_char.free();

	return;
}


void Parallaction::pauseJobs() {
	debugC(9, kDebugExec, "pausing jobs execution");

	_engineFlags |= kEnginePauseJobs;
	return;
}

void Parallaction::resumeJobs() {
	debugC(9, kDebugExec, "resuming jobs execution");

	_engineFlags &= ~kEnginePauseJobs;
	return;
}

AnimationPtr Parallaction::findAnimation(const char *name) {

	for (AnimationList::iterator it = _location._animations.begin(); it != _location._animations.end(); it++)
		if (!scumm_stricmp((*it)->_name, name)) return *it;

	return nullAnimationPtr;
}

void Parallaction::freeAnimations() {
	for (AnimationList::iterator it = _location._animations.begin(); it != _location._animations.end(); it++) {
		(*it)->_commands.clear();	// See comment for freeZones(), about circular references.
	}
	_location._animations.clear();
	return;
}



void Parallaction::allocateLocationSlot(const char *name) {
	// WORKAROUND: the original code erroneously incremented
	// _currentLocationIndex, thus producing inconsistent
	// savegames. This workaround modified the following loop
	// and if-statement, so the code exactly matches the one
	// in Big Red Adventure.
	_currentLocationIndex = -1;
	uint16 _di = 0;
	while (_locationNames[_di][0] != '\0') {
		if (!scumm_stricmp(_locationNames[_di], name)) {
			_currentLocationIndex = _di;
		}
		_di++;
	}

	if (_di == 120)
		error("No more location slots available. Please report this immediately to ScummVM team.");

	if (_currentLocationIndex  == -1) {
		strcpy(_locationNames[_numLocations], name);
		_currentLocationIndex = _numLocations;

		_numLocations++;
		_locationNames[_numLocations][0] = '\0';
		_localFlags[_numLocations] = 0;
	} else {
		setLocationFlags(kFlagsVisited);	// 'visited'
	}
}



void Parallaction::freeLocation() {
	debugC(2, kDebugExec, "freeLocation");

	_soundMan->stopSfx(0);
	_soundMan->stopSfx(1);
	_soundMan->stopSfx(2);
	_soundMan->stopSfx(3);

	_localFlagNames->clear();

	_location._walkNodes.clear();

	_gfx->clearGfxObjects(kGfxObjNormal);
	freeBackground();

	_location._programs.clear();
	freeZones();
	freeAnimations();

	free(_location._comment);
	_location._comment = 0;

	_location._commands.clear();
	_location._aCommands.clear();

	return;
}




void Parallaction::freeBackground() {

	_gfx->freeBackground();
	_pathBuffer = 0;

}

void Parallaction::setBackground(const char* name, const char* mask, const char* path) {

	_gfx->setBackground(kBackgroundLocation, name, mask, path);
	_pathBuffer = &_gfx->_backgroundInfo.path;

	return;
}

void Parallaction::showLocationComment(const char *text, bool end) {
	_balloonMan->setLocationBalloon(const_cast<char*>(text), end);
}


void Parallaction::processInput(InputData *data) {

	switch (data->_event) {
	case kEvEnterZone:
		debugC(2, kDebugInput, "processInput: kEvEnterZone");
		_gfx->showFloatingLabel(data->_label);
		break;

	case kEvExitZone:
		debugC(2, kDebugInput, "processInput: kEvExitZone");
		_gfx->hideFloatingLabel();
		break;

	case kEvAction:
		debugC(2, kDebugInput, "processInput: kEvAction");
		_input->stopHovering();
		pauseJobs();
		runZone(data->_zone);
		resumeJobs();
		break;

	case kEvOpenInventory:
		_input->stopHovering();
		_gfx->hideFloatingLabel();
		if (hitZone(kZoneYou, data->_mousePos.x, data->_mousePos.y) == 0) {
			setArrowCursor();
		}
		pauseJobs();
		openInventory();
		break;

	case kEvCloseInventory: // closes inventory and possibly select item
		closeInventory();
		setInventoryCursor(data->_inventoryIndex);
		resumeJobs();
		break;

	case kEvHoverInventory:
		highlightInventoryItem(data->_inventoryIndex);						// enable
		break;

	case kEvWalk:
		debugC(2, kDebugInput, "processInput: kEvWalk");
		_input->stopHovering();
		setArrowCursor();
		_char.scheduleWalk(data->_mousePos.x, data->_mousePos.y);
		break;

	case kEvQuitGame:
		_engineFlags |= kEngineQuit;
		break;

	case kEvSaveGame:
		_input->stopHovering();
		saveGame();
		setArrowCursor();
		break;

	case kEvLoadGame:
		_input->stopHovering();
		loadGame();
		setArrowCursor();
		break;

	}

	return;
}

void Parallaction::runGame() {

	InputData *data = _input->updateInput();
	if (data->_event != kEvNone) {
		processInput(data);
	}

	if (_engineFlags & kEngineQuit)
		return;

	runPendingZones();

	if (_engineFlags & kEngineQuit)
		return;

	if (_engineFlags & kEngineChangeLocation) {
		changeLocation(_location._name);
	}

	if (_engineFlags & kEngineQuit)
		return;

	_gfx->beginFrame();

	if (_input->_inputMode == Input::kInputModeGame) {
		_programExec->runScripts(_location._programs.begin(), _location._programs.end());
		_char._ani->_z = _char._ani->height() + _char._ani->_top;
		if (_char._ani->gfxobj) {
			_char._ani->gfxobj->z = _char._ani->_z;
		}
		walk(_char);
		drawAnimations();
	}

	// change this to endFrame?
	updateView();

}




//	displays transition before a new location
//
//	clears screen (in white??)
//	shows location comment (if any)
//	waits for mouse click
//	fades towards game palette
//
void Parallaction::doLocationEnterTransition() {
	debugC(2, kDebugExec, "doLocationEnterTransition");

	if (!_location._comment) {
		return;
	}

	if (getLocationFlags() & kFlagsVisited) {
		debugC(2, kDebugExec, "skipping location transition");
		return; // visited
	}

	Palette pal(_gfx->_palette);
	pal.makeGrayscale();
	_gfx->setPalette(pal);

	_programExec->runScripts(_location._programs.begin(), _location._programs.end());
	drawAnimations();

	_gfx->updateScreen();

	showLocationComment(_location._comment, false);
	_input->waitUntilLeftClick();
	_balloonMan->freeBalloons();

	// fades maximum intensity palette towards approximation of main palette
	for (uint16 _si = 0; _si<6; _si++) {
		pal.fadeTo(_gfx->_palette, 4);
		_gfx->setPalette(pal);
		_gfx->updateScreen();
		g_system->delayMillis(20);
	}

	_gfx->setPalette(_gfx->_palette);

	debugC(2, kDebugExec, "doLocationEnterTransition completed");

	return;
}

void Parallaction::setLocationFlags(uint32 flags) {
	_localFlags[_currentLocationIndex] |= flags;
}

void Parallaction::clearLocationFlags(uint32 flags) {
	_localFlags[_currentLocationIndex] &= ~flags;
}

void Parallaction::toggleLocationFlags(uint32 flags) {
	_localFlags[_currentLocationIndex] ^= flags;
}

uint32 Parallaction::getLocationFlags() {
	return _localFlags[_currentLocationIndex];
}




ZonePtr Parallaction::findZone(const char *name) {

	for (ZoneList::iterator it = _location._zones.begin(); it != _location._zones.end(); it++) {
		if (!scumm_stricmp((*it)->_name, name)) return *it;
	}

	return findAnimation(name);
}


void Parallaction::freeZones() {
	debugC(2, kDebugExec, "freeZones: kEngineQuit = %i", _engineFlags & kEngineQuit);

	ZoneList::iterator it = _location._zones.begin();

	while ( it != _location._zones.end() ) {

		// NOTE : this condition has been relaxed compared to the original, to allow the engine
		// to retain special - needed - zones that were lost across location switches.
		ZonePtr z = *it;
		if (((z->_top == -1) || (z->_left == -2)) && ((_engineFlags & kEngineQuit) == 0)) {
			debugC(2, kDebugExec, "freeZones preserving zone '%s'", z->_name);
			it++;
		} else {
			(*it)->_commands.clear();	// Since commands may reference zones, and both commands and zones are kept stored into
										// SharedPtr's, we need to kill commands explicitly to destroy any potential circular
										// reference.
			it = _location._zones.erase(it);
		}
	}

	return;
}


enum {
	WALK_LEFT = 0,
	WALK_RIGHT = 1,
	WALK_DOWN = 2,
	WALK_UP = 3
};

struct WalkFrames {
	int16 stillFrame[4];
	int16 firstWalkFrame[4];
	int16 numWalkFrames[4];
	int16 frameRepeat[4];
};

WalkFrames _char20WalkFrames = {
	{  0,  7, 14, 17 },
	{  1,  8, 15, 18 },
	{  6,  6,  2,  2 },
	{  2,  2,  4,  4 }
};

WalkFrames _char24WalkFrames = {
	{  0,  9, 18, 21 },
	{  1, 10, 19, 22 },
	{  8,  8,  2,  2 },
	{  2,  2,  4,  4 }
};

const char Character::_prefixMini[] = "mini";
const char Character::_suffixTras[] = "tras";
const char Character::_empty[] = "\0";


Character::Character(Parallaction *vm) : _vm(vm), _ani(new Animation), _builder(_ani) {
	_talk = NULL;
	_head = NULL;
	_objs = NULL;

	_direction = WALK_DOWN;
	_step = 0;

	_dummy = false;

	_ani->_left = 150;
	_ani->_top = 100;
	_ani->_z = 10;
	_ani->_oldPos.x = -1000;
	_ani->_oldPos.y = -1000;
	_ani->_frame = 0;
	_ani->_flags = kFlagsActive | kFlagsNoName;
	_ani->_type = kZoneYou;
	strncpy(_ani->_name, "yourself", ZONENAME_LENGTH);
}

void Character::getFoot(Common::Point &foot) {
	foot.x = _ani->_left + _ani->width() / 2;
	foot.y = _ani->_top + _ani->height();
}

void Character::setFoot(const Common::Point &foot) {
	_ani->_left = foot.x - _ani->width() / 2;
	_ani->_top = foot.y - _ani->height();
}

void Character::scheduleWalk(int16 x, int16 y) {
	if ((_ani->_flags & kFlagsRemove) || (_ani->_flags & kFlagsActive) == 0) {
		return;
	}

	_walkPath = _builder.buildPath(x, y);
	_engineFlags |= kEngineWalking;
}

void Character::free() {

	delete _talk;
	delete _head;
	delete _objs;
	delete _ani->gfxobj;

	_talk = NULL;
	_head = NULL;
	_objs = NULL;
	_ani->gfxobj = NULL;

	return;
}


// Various ways of detecting character modes used to exist
// inside the engine, so they have been unified in the two
// following macros.
// Mini characters are those used in far away shots, like
// the highway scenery, while Dummy characters are a mere
// workaround to keep the engine happy when showing slides.
// As a sidenote, standard sized characters' names start
// with a lowercase 'd'.
#define IS_MINI_CHARACTER(s) (((s)[0] == 'm'))
#define IS_DUMMY_CHARACTER(s) (((s)[0] == 'D'))

void Character::setName(const char *name) {
	const char *begin = name;
	const char *end = begin + strlen(name);

	_prefix = _empty;
	_suffix = _empty;

	_dummy = IS_DUMMY_CHARACTER(name);

	if (!_dummy) {
		if (!strstr(name, "donna")) {
			_engineFlags &= ~kEngineTransformedDonna;
		} else
		if (_engineFlags & kEngineTransformedDonna) {
			_suffix = _suffixTras;
		} else {
			const char *s = strstr(name, "tras");
			if (s) {
				_engineFlags |= kEngineTransformedDonna;
				_suffix = _suffixTras;
				end = s;
			}
		}
		if (IS_MINI_CHARACTER(name)) {
			_prefix = _prefixMini;
			begin = name+4;
		}
	}

	memset(_baseName, 0, 30);
	strncpy(_baseName, begin, end - begin);
	sprintf(_name, "%s%s", _prefix, _baseName);
	sprintf(_fullName, "%s%s%s", _prefix, _baseName, _suffix);
}

const char *Character::getName() const {
	return _name;
}

const char *Character::getBaseName() const {
	return _baseName;
}

const char *Character::getFullName() const {
	return _fullName;
}

bool Character::dummy() const {
	return _dummy;
}

void Parallaction::beep() {
	_soundMan->playSfx("beep", 3, false);
}

void Parallaction::scheduleLocationSwitch(const char *location) {
	strcpy(_location._name, location);
	_engineFlags |= kEngineChangeLocation;
}





void Character::updateDirection(const Common::Point& pos, const Common::Point& to) {

	Common::Point dist(to.x - pos.x, to.y - pos.y);
	WalkFrames *frames = (_ani->getFrameNum() == 20) ? &_char20WalkFrames : &_char24WalkFrames;

	_step++;

	if (dist.x == 0 && dist.y == 0) {
		_ani->_frame = frames->stillFrame[_direction];
		return;
	}

	if (dist.x < 0)
		dist.x = -dist.x;
	if (dist.y < 0)
		dist.y = -dist.y;

	_direction = (dist.x > dist.y) ? ((to.x > pos.x) ? WALK_LEFT : WALK_RIGHT) : ((to.y > pos.y) ? WALK_DOWN : WALK_UP);
	_ani->_frame = frames->firstWalkFrame[_direction] + (_step / frames->frameRepeat[_direction]) % frames->numWalkFrames[_direction];
}


} // namespace Parallaction
