/* 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 "backends/modular-backend.h"
#include "backends/graphics/graphics.h"

#include "common/config-manager.h"
#include "common/debug-channels.h"
#include "common/tokenizer.h"

#include "graphics/macgui/macwindowmanager.h"
#include "graphics/wincursor.h"

#include "director/director.h"
#include "director/debugger.h"
#include "director/archive.h"
#include "director/cast.h"
#include "director/movie.h"
#include "director/picture.h"
#include "director/score.h"
#include "director/sound.h"
#include "director/window.h"
#include "director/debugger/debugtools.h"

/**
 * When detection is compiled dynamically, directory globs end up in detection plugin and
 * engine cannot link to them so duplicate them in the engine in this case
 */
#ifndef DETECTION_STATIC
#include "director/detection_paths.h"
#endif

namespace Director {

DirectorEngine *g_director;

DirectorEngine::DirectorEngine(OSystem *syst, const DirectorGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
	g_director = this;
	g_debugger = new Debugger();
	setDebugger(g_debugger);

	// parseOptions depends on the _dirSeparator
	_version = getDescriptionVersion();
	if (getPlatform() == Common::kPlatformWindows && _version >= 400) {
		_dirSeparator = '\\';
	} else {
		_dirSeparator = ':';
	}

	parseOptions();

	// Setup mixer
	syncSoundSettings();
	_defaultVolume = _mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType);

	// Load Palettes
	loadDefaultPalettes();

	// Load Patterns
	loadPatterns();

	// Load key codes
	loadKeyCodes();

	memset(_currentPalette, 0, 768);
	_currentPaletteLength = 0;
	_stage = nullptr;
	_mainArchive = nullptr;
	_currentWindow = nullptr;
	_cursorWindow = nullptr;
	_lingo = nullptr;
	_clipBoard = nullptr;
	_fixStageSize = false;
	_fixStageRect = Common::Rect();
	_wmMode = 0;
	_primitives = nullptr;

	_wmWidth = 1024;
	_wmHeight = 768;

	_fpsLimit = 0;
	_forceDate.tm_sec = -1;
	_forceDate.tm_min = -1;
	_forceDate.tm_hour = -1;
	_forceDate.tm_mday = -1;
	_forceDate.tm_mon = -1;
	_forceDate.tm_year = -1;
	_forceDate.tm_wday = -1;
	_loadSlowdownFactor = 0;
	_loadSlowdownCooldownTime = 0;
	_fileIOType = 0;
	_vfwPaletteHack = false;

	_wm = nullptr;

	_gameDataDir = Common::FSNode(ConfMan.getPath("path"));

	SearchMan.addDirectory(_gameDataDir, 0, 5);

	for (uint i = 0; Director::directoryGlobs[i]; i++) {
		Common::String directoryGlob = directoryGlobs[i];
		SearchMan.addSubDirectoryMatching(_gameDataDir, directoryGlob, 0, 5);
	}

	if (ConfMan.getBool("true_color") || (getGameFlags() & GF_32BPP) || debugChannelSet(-1, kDebug32bpp)) {
#ifdef USE_RGB_COLOR
		_colorDepth = 32;
#else
		warning("32-bpp color dept is not supported, forcing 8-bit");
		_colorDepth = 8;
#endif
	} else {
		_colorDepth = 8;	// 256-color
	}
	// Enable Macintosh gamma correction. This resolves the issue of Mac games appearing too dark.
	// Enabled by default for Macintosh and Pippin games in the detection code.
	// Right now only used in 8-bit mode to adjust the palette.
	// FIXME: How do we add this to true color rendering without a heap of workarounds?
	if ((getPlatform() == Common::kPlatformMacintosh) || (getPlatform() == Common::kPlatformPippin)) {
		_gammaCorrection = ConfMan.getBool("gamma_correction");
	} else {
		// FIXME: It would be good if we could have this option for non-Mac, except not
		// enabled by default.
		_gammaCorrection = false;
	}

	switch (getPlatform()) {
	case Common::kPlatformMacintoshII:
		_machineType = 4;
		break;
	case Common::kPlatformPippin:
		_machineType = 71;
		break;
	case Common::kPlatformWindows:
		_machineType = 256;
		break;
	case Common::kPlatformMacintosh:
	default:
		_machineType = 9;	// Macintosh IIci
	}

	_playbackPaused = false;
	_centerStage = true;

	_surface = nullptr;
	_tickBaseline = 0;

	_emulateMultiButtonMouse = false;
}

DirectorEngine::~DirectorEngine() {
	delete _lingo;

	clearPalettes();

	for (auto &it : _windowList) {
		it->decRefCount();
	}
	_stage->decRefCount();
	_stage = nullptr;
	if (_currentWindow) {
		_currentWindow->decRefCount();
		_currentWindow = nullptr;
	}
	delete _wm;
	for (auto &it : _allSeenResFiles) {
		delete it._value;
	}
	for (uint i = 0; i < _winCursor.size(); i++)
		delete _winCursor[i];

	delete _surface;
	delete _primitives;
}

Movie *DirectorEngine::getCurrentMovie() const { return _currentWindow->getCurrentMovie(); }
Common::String DirectorEngine::getCurrentPath() const { return _currentWindow->getCurrentPath(); }
Common::String DirectorEngine::getCurrentAbsolutePath() {
	Common::String currentPath = getCurrentPath();
	Common::String result;
	result += (getPlatform() == Common::kPlatformWindows && _version >= 400) ? "C:\\" : "@:";
	result += convertPath(currentPath);
	return result;
}

static bool buildbotErrorHandler(const char *msg) { return true; }

Window *DirectorEngine::getOrCreateWindow(Common::String &name) {
	for (auto &it : _windowList) {
		if (it->getName().equalsIgnoreCase(name)) {
			return it;
		}
	}
	Window *window = new Window(_wm->getNextId(), false, false, false, _wm, g_director, false);
	window->setName(name);
	window->setTitle(name);
	window->resizeInner(1, 1);
	window->setVisible(false, true);
	window->move(0, 0);
	window->incRefCount();
	_wm->addWindowInitialized(window->getMacWindow());
	_windowList.push_back(window);
	return window;
}

void DirectorEngine::forgetWindow(Window *window) {
	for (auto &it : _windowsToForget) {
		if (it == window)
			return;
	}
	window->setVisible(false, true);
	_windowsToForget.push_back(window);
}

void DirectorEngine::setCurrentWindow(Window *window) {
	if (_currentWindow == window)
		return;
	if (_currentWindow)
		_currentWindow->decRefCount();
	_currentWindow = window;
	_currentWindow->incRefCount();
}

void DirectorEngine::setVersion(uint16 version) {
	if (version == _version)
		return;

	debug("Switching to Director v%d", version);
	_version = version;
	_lingo->reloadBuiltIns();
}

Common::Error DirectorEngine::run() {
	debug("Starting v%d Director game", getVersion());

	// We want to avoid GUI errors for buildbot, because they hang it
	if (debugChannelSet(-1, kDebugFewFramesOnly))
		Common::setErrorHandler(buildbotErrorHandler);

	if (!_mixer->isReady()) {
		return Common::kAudioDeviceInitFailed;
	}

	memset(_currentPalette, 0, 768);

	//        we run mac-style menus     |   and we will redraw all widgets
	_wmMode = Graphics::kWMModalMenuMode | Graphics::kWMModeManualDrawWidgets;

	if (!debugChannelSet(-1, kDebugDesktop))
		_wmMode |= Graphics::kWMModeFullscreen | Graphics::kWMModeNoDesktop;

#ifdef USE_RGB_COLOR
	if (ConfMan.getBool("true_color") || (getGameFlags() & GF_32BPP) || debugChannelSet(-1, kDebug32bpp))
		_wmMode |= Graphics::kWMMode32bpp;
#endif

	if (getGameFlags() & GF_DESKTOP)
		_wmMode &= ~Graphics::kWMModeNoDesktop;

	if (getGameFlags() & GF_640x480) {
		_wmWidth = 640;
		_wmHeight = 480;
	}

	_wm = new Graphics::MacWindowManager(_wmMode, &_director3QuickDrawPatterns, getLanguage());
	_wm->setEngine(this);

	gameQuirks(_gameDescription->desc.gameId, _gameDescription->desc.platform);
	// Mix in all the saved files for the current target
	// Assign higher priority to save games to load them before original game files
	SearchMan.add(kSavedFilesArchive, new SavedArchive(_targetName), 1);

	_wm->setDesktopMode(_wmMode);

	_wm->printWMMode();

	_pixelformat = _wm->_pixelformat;

	debugC(1, kDebugImages, "Director pixelformat is: %s", _pixelformat.toString().c_str());

	_stage = new Window(_wm->getNextId(), false, false, false, _wm, this, true);
	_stage->incRefCount();

	// Set this as background so it doesn't come to foreground when multiple windows present
	_wm->setBackgroundWindow(_stage->getMacWindow());

	if (!desktopEnabled())
		_stage->disableBorder();

	_surface = new Graphics::ManagedSurface(1, 1);
	_wm->setScreen(_surface);
	_wm->addWindowInitialized(_stage->getMacWindow());
	_wm->setActiveWindow(_stage->getId());
	setPalette(CastMemberID(kClutSystemMac, -1));

	setCurrentWindow(_stage);

	_lingo = new Lingo(this);
	_lingo->switchStateFromWindow();

	if (getGameGID() == GID_TEST) {
		_currentWindow->runTests();
		return Common::kNoError;
	} else if (getGameGID() == GID_TESTALL) {
		_currentWindow->enqueueAllMovies();
	}

	if (getPlatform() == Common::kPlatformWindows)
		_machineType = 256; // IBM PC-type machine

	Common::Error err = _currentWindow->loadInitialMovie();

	// Exit gracefully when run with buildbot
	if (debugChannelSet(-1, kDebugFewFramesOnly) && err.getCode() == Common::kNoGameDataFoundError)
		return Common::kNoError;

	if (err.getCode() != Common::kNoError)
		return err;

	if (debugChannelSet(-1, kDebugConsole)) {
		g_debugger->attach();
		g_system->updateScreen();
	}

#ifdef USE_IMGUI
	ImGuiCallbacks callbacks;
	bool drawImGui = debugChannelSet(-1, kDebugImGui);
	callbacks.init = DT::onImGuiInit;
	callbacks.render = drawImGui ? DT::onImGuiRender : nullptr;
	callbacks.cleanup = DT::onImGuiCleanup;
	_system->setImGuiCallbacks(callbacks);
#endif

	bool loop = true;

	while (loop) {
		if (_stage->getCurrentMovie())
			processEvents();

		setCurrentWindow(_stage);
		g_lingo->switchStateFromWindow();
		loop = _currentWindow->step();

		if (loop) {
			for (auto &it : _windowList) {
				setCurrentWindow(it);
				g_lingo->switchStateFromWindow();
				_currentWindow->step();
			}
		}

		draw();
		while (!_windowsToForget.empty()) {
			Window *window = _windowsToForget.back();
			_windowsToForget.pop_back();
			for (size_t i = 0; i < _windowList.size(); i++) {
				if (_windowList[i] == window) {
					_windowList.remove_at(i);
					// FIXME: force window to be removed from WM
					window->decRefCount();
					break;
				}
			}
		}

		g_director->delayMillis(10);
#ifdef USE_IMGUI
		// For performance reasons, disable the renderer callback if the ImGui debug flag isn't set
		if (debugChannelSet(-1, kDebugImGui) != drawImGui) {
			drawImGui = !drawImGui;
			callbacks.render = drawImGui ? DT::onImGuiRender : nullptr;
			_system->setImGuiCallbacks(callbacks);
		}
#endif
	}

#ifdef USE_IMGUI
	_system->setImGuiCallbacks(ImGuiCallbacks());
#endif

	if (debugChannelSet(10, kDebugSaving)) {
		//_mainArchive->writeToFile(Common::String(""), getCurrentMovie());
	}

	return Common::kNoError;
}

Common::CodePage DirectorEngine::getPlatformEncoding() {
	// Returns the default encoding for the platform we're pretending to be.
	// (English Mac OS, Japanese Mac OS, English Windows, etc.)
	return getEncoding(getPlatform(), getLanguage());
}

Common::String DirectorEngine::getRawEXEName() const {
	if (!_gameDescription->desc.filesDescriptions[0].fileName)
		return Common::String();

	// Returns raw executable name (without getting overloaded from --start-movie option)
	return Common::Path(_gameDescription->desc.filesDescriptions[0].fileName).toString(g_director->_dirSeparator);
}

Common::String DirectorEngine::getEXEName() const {
	StartMovie startMovie = getStartMovie();
	if (startMovie.startMovie.size() > 0)
		return startMovie.startMovie;

	return getRawEXEName();
}

void DirectorEngine::parseOptions() {
	_options.startMovie.startFrame = -1;

	if (!ConfMan.hasKey("start_movie"))
		return;

	Common::StringTokenizer tok(ConfMan.get("start_movie"), ",");

	while (!tok.empty()) {
		Common::String part = tok.nextToken();

		int eqPos = part.findLastOf("=");
		Common::String key;
		Common::String value;

		if ((uint)eqPos != Common::String::npos) {
			key = part.substr(0, eqPos);
			value = part.substr(eqPos + 1, part.size());
		} else {
			value = part;
		}

		if (key == "movie" || key.empty()) { // Format is movie[@startFrame]
			if (!_options.startMovie.startMovie.empty()) {
				warning("parseOptions(): Duplicate startup movie: %s", value.c_str());
			}

			int atPos = value.findLastOf("@");

			if ((uint)atPos == Common::String::npos) {
				_options.startMovie.startMovie = value;
			} else {
				_options.startMovie.startMovie = value.substr(0, atPos);
				Common::String tail = value.substr(atPos + 1, value.size());
				if (tail.size() > 0)
					_options.startMovie.startFrame = atoi(tail.c_str());
			}

			_options.startMovie.startMovie = Common::Path(_options.startMovie.startMovie).punycodeDecode().toString(_dirSeparator);

			debug(2, "parseOptions(): Movie is: %s, frame is: %d", _options.startMovie.startMovie.c_str(), _options.startMovie.startFrame);
		} else if (key == "startup") {
			_options.startupPath = value;

			debug(2, "parseOptions(): Startup is: %s", value.c_str());
		} else {
			warning("parseOptions(): unknown option %s", part.c_str());
		}

	}
}

StartMovie DirectorEngine::getStartMovie() const {
	return _options.startMovie;
}

Common::Path DirectorEngine::getStartupPath() const {
	return Common::Path(_options.startupPath, g_director->_dirSeparator);
}

bool DirectorEngine::desktopEnabled() {
	return !(_wmMode & Graphics::kWMModeNoDesktop);
}

PatternTile::~PatternTile() {
	delete img;
}

} // End of namespace Director
