/* 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/scummsys.h"
#include "common/error.h"
#include "common/system.h"
#include "common/events.h"
#include "common/archive.h"
#include "common/config-manager.h"
#include "common/fs.h"
#include "common/str.h"
#include "backends/keymapper/keymapper.h"
#include "trecision/animmanager.h"
#include "trecision/animtype.h"
#include "trecision/actor.h"
#include "trecision/console.h"
#include "trecision/defines.h"
#include "trecision/dialog.h"
#include "trecision/graphics.h"
#include "trecision/pathfinding3d.h"
#include "trecision/renderer3d.h"
#include "trecision/logic.h"
#include "trecision/scheduler.h"
#include "trecision/sound.h"
#include "trecision/trecision.h"
#include "trecision/text.h"
#include "trecision/video.h"

namespace Common {
class File;
}

namespace Trecision {

TrecisionEngine::TrecisionEngine(OSystem *syst, const ADGameDescription *desc) : Engine(syst), _gameDescription(desc) {
	_gameId = !strcmp(_gameDescription->gameId, "nl") ? GID_NightLong : GID_ArkOfTime;

	const Common::FSNode gameDataDir(ConfMan.getPath("path"));
	SearchMan.addSubDirectoryMatching(gameDataDir, "AUTORUN");
	SearchMan.addSubDirectoryMatching(gameDataDir, "DATA");
	SearchMan.addSubDirectoryMatching(gameDataDir, "FMV");
	// Amiga version loads data files from directories
	if (isAmiga()) {
		SearchMan.addSubDirectoryMatching(gameDataDir, "NLDATA.CD0");
		SearchMan.addSubDirectoryMatching(gameDataDir, "NLSPEECH.CD0");
		SearchMan.addSubDirectoryMatching(gameDataDir, "NLANIM.CDX");
	}

	_curRoom = 0;
	_oldRoom = 0;

	_curInventory = 0;

	for (int i = 0; i < 10; ++i)
		_curScriptFrame[i] = 0;

	_iconBase = 0;
	_inventoryRefreshStartIcon = 0;
	_curObj = 1;
	_inventoryRefreshStartLine = INVENTORY_HIDE;
	_lightIcon = 0xFF;
	_inventoryStatus = INV_OFF;
	_inventoryCounter = INVENTORY_HIDE;	
	_flagInventoryLocked = false;
	_inventorySpeedIndex = 0;
	_inventoryScrollTime = 0;

	_fastWalk = false;

	// Use With
	_useWith[0] = _useWith[1] = 0;
	_useWithInv[0] = _useWithInv[1] = false;

	// Messages
	for (int i = 0; i < MAXOBJNAME; ++i)
		_objName[i] = nullptr;

	for (int i = 0; i < MAXSENTENCE; ++i)
		_sentence[i] = nullptr;

	for (int i = 0; i < MAXSYSTEXT; ++i)
		_sysText[i] = nullptr;

	_curMessage = nullptr;
	_animMgr = nullptr;
	_dialogMgr = nullptr;
	_graphicsMgr = nullptr;
	_logicMgr = nullptr;
	_soundMgr = nullptr;
	_renderer = nullptr;
	_pathFind = nullptr;
	_textMgr = nullptr;
	_animTypeMgr = nullptr;
	_nextRefresh = 0;

	_curKey = Common::KEYCODE_INVALID;
	_curAction = kActionNone;
	_curAscii = 0;
	_mousePos = Common::Point(0, 0);
	_mouseMoved = _mouseLeftBtn = _mouseRightBtn = false;
	_keybInput = false;

	_gamePaused = false;

	_textPtr = nullptr;
	_lastInv = 0;
	_lastObj = 0;

	_curStack = 0;

	_flagScriptActive = false;

	_actor = nullptr;

	_flagDialogActive = false;
	_flagDialogMenuActive = false;
	_flagSkipTalk = false;
	_flagPaintCharacter = false;
	_flagShowCharacter = true;
	_flagSomeoneSpeaks = false;
	_flagCharacterSpeak = false;
	_flagUseWithStarted = false;
	_flagNoPaintScreen = false;
	_flagWaitRegen = false;

	for (int i = 0; i < MAXOBJINROOM; ++i) {
		_objectGraphics[i].buf = nullptr;
		_objectGraphics[i].mask = nullptr;
	}

	_curTime = 0;
	_characterSpeakTime = 0;
	_pauseStartTime = 0;
	_textStatus = TEXT_OFF;

	_cx = _cy = 0;

	_textArea = nullptr;
	Message msg = { MC_IDLE, 0, MP_DEFAULT, 0, 0, 0, 0 };
	_snake52 = msg;
	for (int i = 0; i < 50; ++i)
		_scriptFrame[i].clear();

	_scheduler = nullptr;
}

TrecisionEngine::~TrecisionEngine() {
	if (_animMgr)
		_animMgr->stopAllSmkAnims();

	_dataFile.close();
	_thumbnail.free();

	delete _animMgr;
	delete _dialogMgr;
	delete _graphicsMgr;
	delete _logicMgr;
	delete _soundMgr;
	delete _renderer;
	delete _pathFind;
	delete _textMgr;
	delete _scheduler;
	delete _animTypeMgr;

	delete _actor;
	delete[] _textArea;

	for (int i = 0; i < MAXOBJINROOM; ++i) {
		delete[] _objectGraphics[i].buf;
		delete[] _objectGraphics[i].mask;
	}
}

Common::Error TrecisionEngine::run() {
	syncSoundSettings();

	if (!_dataFile.open(this, "nldata.cd0"))
		error("Error opening nldata.cd0");

	_graphicsMgr = new GraphicsManager(this);
	if (!_graphicsMgr->init())
		return Common::kUnsupportedColorMode;
	_animMgr = new AnimManager(this);
	_dialogMgr = new DialogManager(this);
	_logicMgr = new LogicManager(this);
	_soundMgr = new SoundManager(this);
	_pathFind = new PathFinding3D(this);
	_renderer = new Renderer3D(this);
	_textMgr = new TextManager(this);
	_scheduler = new Scheduler(this);
	_animTypeMgr = new AnimTypeManager(this);
	_actor = new Actor(this);

	setDebugger(new Console(this));

	initMain();

	while (!shouldQuit()) {
		eventLoop();
		if (!_flagNoPaintScreen)
			processTime();

		processMouse();
		_scheduler->process();

		_animTypeMgr->handler(kAnimTypeBackground);

		processCurrentMessage();

		if (_flagScriptActive)
			evalScript();
	}

	if (isDemo())
		_graphicsMgr->showDemoPic();

	return Common::kNoError;
}

void TrecisionEngine::eventLoop() {
	Common::Event event;
	while (g_system->getEventManager()->pollEvent(event)) {
		Common::Keymapper *keymapper = _eventMan->getKeymapper();
		switch (event.type) {
		case Common::EVENT_MOUSEMOVE:
			_mouseMoved = true;
			_mousePos = event.mouse;
			break;

		case Common::EVENT_LBUTTONUP:
			_mouseLeftBtn = true;
			break;

		case Common::EVENT_RBUTTONUP:
			_mouseRightBtn = true;
			break;

		case Common::EVENT_CUSTOM_ENGINE_ACTION_END:
			_curAction = event.customType;
			switch (event.customType) {
			case kActionFastWalk:
				_fastWalk ^= true;
				break;
			case kActionPause:
				if (!_gamePaused && !_keybInput) {
					_curKey = Common::KEYCODE_INVALID;
					_curAction = kActionNone;
					keymapper->getKeymap("game-shortcuts")->setEnabled(false);

					_gamePaused = true;
					waitKey();
				}

				keymapper->getKeymap("game-shortcuts")->setEnabled(true);
				_gamePaused = false;
				break;

			default:
				break;
			}
			return;

		case Common::EVENT_KEYUP:
			_curKey = event.kbd.keycode;
			_curAscii = event.kbd.ascii;
			break;

		case Common::EVENT_JOYBUTTON_UP:
			_joyButtonUp = true;
			break;

		default:
			break;
		}
	}
	g_system->delayMillis(10);
	g_system->updateScreen();
}

void TrecisionEngine::initMain() {
	for (int c = 0; c < MAXOBJ; c++)
		_obj[c]._position = -1;

	_curRoom = kRoomIntro;
	_scheduler->init();

	loadAll();
	processTime();

	// Check if a saved game is to be loaded from the launcher
	if (ConfMan.hasKey("save_slot"))
		loadGameState(ConfMan.getInt("save_slot"));
	else
		changeRoom(_curRoom);
}

void TrecisionEngine::checkSystem() {
	eventLoop();
}

void TrecisionEngine::startCharacterAction(uint16 action, uint16 newRoom, uint8 newPos, uint16 textId) {
	_scheduler->initCharacterQueue();

	_flagInventoryLocked = false;
	if (action > hLAST) {
		_animMgr->startSmkAnim(action);
		_animTypeMgr->init(action, _curObj);
		_graphicsMgr->hideCursor();
		_flagShowCharacter = false;
		_scheduler->doEvent(MC_CHARACTER, ME_CHARACTERCONTINUEACTION, MP_DEFAULT, action, newRoom, newPos, _curObj);
	} else {
		if ((action == aWALKIN) || (action == aWALKOUT))
			_curObj = 0;
		_graphicsMgr->hideCursor();
		_actor->actorDoAction(action);
		_pathFind->nextStep();
	}

	if (textId)
		_textMgr->characterSayInAction(textId);
	else
		_textMgr->clearLastText();
}

bool TrecisionEngine::canPlayerInteract() {
	return (!_flagSomeoneSpeaks &&
			!_flagScriptActive &&
			!_flagDialogActive &&
			!_flagDialogMenuActive &&
			(_actor->_curAction < hWALKIN) &&
			!_flagUseWithStarted &&
			_flagShowCharacter &&
			!_animMgr->isActionActive());
}

void TrecisionEngine::setObjectVisible(uint16 objectId, bool visible) {
	_obj[objectId].setModeStatus(visible);
	refreshObject(objectId);
}

void TrecisionEngine::refreshObject(uint16 objectId) {
	for (int i = 0; i < MAXOBJINROOM; ++i) {
		if (!_room[_curRoom]._object[i])
			return;	// reached the end of the list, object not found

		if (objectId == _room[_curRoom]._object[i]) {
			break;	// object found in room objects, continue
		}
	}

	if (_obj[objectId].isModeMask() || _obj[objectId].isModeFull()) {
		SSortTable entry;
		entry._objectId = objectId;
		entry._remove = !isObjectVisible(objectId);
		_sortTable.push_back(entry);

		// Remove previous instances
		for (Common::List<SSortTable>::iterator it = _sortTableReplay.begin(); it != _sortTableReplay.end(); ++it) {
			if (it->_objectId == objectId) {
				_sortTableReplay.erase(it);
				break;
			}
		}

		_sortTableReplay.push_back(entry);
	}
}

bool TrecisionEngine::isObjectVisible(uint16 objectId) {
	return _obj[objectId].isModeStatus();
}

void TrecisionEngine::setObjectAnim(uint16 objectId, uint16 animId) {
	_obj[objectId]._anim = animId;
}

void TrecisionEngine::reEvent() {
	_scheduler->doEvent(_curMessage->_class, _curMessage->_event, _curMessage->_priority, _curMessage->_u16Param1, _curMessage->_u16Param2, _curMessage->_u8Param, _curMessage->_u32Param);
}

Common::SeekableReadStreamEndian *TrecisionEngine::getLocStream() {
	Common::Path filename(_room[_curRoom]._baseName);

	if (isAmiga()) {
		filename.appendInPlace(".bm");
		return readEndian(_dataFile.createReadStreamForMember(filename));
	} else {
		filename.appendInPlace(".cr");
		return readEndian(_dataFile.createReadStreamForCompressedMember(filename));
	}
}

void TrecisionEngine::readLoc() {
	_soundMgr->stopAllExceptMusic();

	_graphicsMgr->clearScreenBufferTop();
	_sortTable.clear();
	_sortTableReplay.clear();

	Common::SeekableReadStreamEndian *picFile = getLocStream();
	_graphicsMgr->loadBackground(picFile);
	readObj(picFile);

	_soundMgr->stopAll();

	if (_room[_curRoom]._sounds[0] != 0)
		_soundMgr->loadRoomSounds();

	Common::Path fname(Common::String::format("%s.3d", _room[_curRoom]._baseName));
	read3D(fname);

	if (_room[_curRoom]._bkgAnim) {
		_animMgr->startSmkAnim(_room[_curRoom]._bkgAnim);
	} else
		_animMgr->smkStop(kSmackerBackground);

	_animTypeMgr->init(_room[_curRoom]._bkgAnim, 0);
}

void TrecisionEngine::redrawRoom() {
	const uint16 curDialog = _dialogMgr->getCurDialog();
	const uint16 curChoice = _dialogMgr->getCurChoice();
	const uint16 bgAnim = _room[_curRoom]._bkgAnim;
	static const ElevatorAction elevatorActions[6] = {
		{dASCENSORE12, 3, a129PARLACOMPUTERESCENDE, kRoom13},
		{dASCENSORE12, 4, a129PARLACOMPUTERESCENDE, kRoom16},
		{dASCENSORE13, 17, a139CHIUDONOPORTESU, kRoom12},
		{dASCENSORE13, 18, a1316CHIUDONOPORTEGIU, kRoom16},
		{dASCENSORE16, 32, a1616SALECONASCENSORE, kRoom12},
		{dASCENSORE16, 33, a1616SALECONASCENSORE, kRoom13},
	};

	_flagShowCharacter = _dialogMgr->showCharacterAfterDialog();
	_flagPaintCharacter = true;
	_textStatus = TEXT_OFF;

	for (int i = 0; i < 6; ++i) {
		if (curDialog == elevatorActions[i].dialog && curChoice == elevatorActions[i].choice) {
			startCharacterAction(elevatorActions[i].action, elevatorActions[i].newRoom, 20, 0);
			break;
		}
	}

	Common::SeekableReadStreamEndian *picFile = getLocStream();
	_graphicsMgr->loadBackground(picFile);
	_sortTable.clear();
	_sortTable = _sortTableReplay;

	if (bgAnim)
		_animMgr->startSmkAnim(bgAnim);

	if (_curRoom == kRoom4P && curDialog == dF4PI)
		_animMgr->smkGoto(kSmackerBackground, 21);

	_graphicsMgr->paintScreen(true);
}

void TrecisionEngine::tendIn() {
	_textStatus = TEXT_OFF;

	_flagPaintCharacter = true;
	_graphicsMgr->paintScreen(true);

	_graphicsMgr->copyToScreen(0, 0, MAXX, MAXY);
}

} // End of namespace Trecision
