/* 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/compression/vise.h"
#include "common/macresman.h"
#include "common/memstream.h"
#include "common/savefile.h"
#include "director/director.h"

namespace Director {

class CachedArchive : public Common::Archive {
public:
	struct InputEntry {
		Common::Path name;

		const byte *data;
		uint32 size;

		InputEntry(Common::String n, const byte *d, uint32 s) : name(n), data(d), size(s) {}
	};

	typedef Common::List<InputEntry> FileInputList;

	CachedArchive(const FileInputList &files);
	~CachedArchive() override;

	bool hasFile(const Common::Path &path) const override;
	int listMembers(Common::ArchiveMemberList &list) const override;
	const Common::ArchiveMemberPtr getMember(const Common::Path &path) const override;
	Common::SeekableReadStream *createReadStreamForMember(const Common::Path &path) const override;
private:
	struct Entry {
		const byte *data;
		uint32 size;
	};

	typedef Common::HashMap<Common::Path, Entry, Common::Path::IgnoreCase_Hash, Common::Path::IgnoreCase_EqualTo> FileMap;
	FileMap _files;
};

struct CachedFile {
	const char *target;
	Common::Platform platform;

	const char *fileName;
	const byte *data;
	int32 size;			// Specify -1 if strlen(data) is the size
} const cachedFiles[] = {
	{
		"directortest", Common::kPlatformUnknown,
		"0testfile",
			(const byte *)"", 0
	},
	{ "trektech", Common::kPlatformWindows,
		"NCC1701D.INI",
			(const byte *)"cdromdrive=D\n", -1
	},
	{ "wolfgang", Common::kPlatformUnknown,
		"WOLFGANG.dat",	// It needs an empty file
			(const byte *)"", 0
	},
	{ "teamxtreme1", Common::kPlatformWindows,
		// In Operation: Weather Disaster, the game will try and check if the
		// save file exists with getNthFileNameInFolder before attempting to
		// read it with FileIO (which uses the save data store).
		"WINDOWS/TXSAVES",
			(const byte *)"", 0
	},
	{ "teamxtreme2", Common::kPlatformWindows,
		// In Operation: Eco-Nightmare, the game will try and check if the
		// save file exists with getNthFileNameInFolder before attempting to
		// read it with FileIO (which uses the save data store).
		"WINDOWS/TX2SAVES",
			(const byte *)"", 0
	},
	{ "paws", Common::kPlatformWindows,
		// PAWS: Personal Automated Wagging System checks a file to determine
		// the location of the CD.
		"INSTALL.INF",
			(const byte *)"CDDrive=D:\\\r\nSourcePath=D:\\\r\nDestPath=C:\\", -1
	},
	{ nullptr, Common::kPlatformUnknown, nullptr, nullptr, 0 }
};

struct SaveFilePath {
	const char *target;
	Common::Platform platform;
	const char *path;
} const saveFilePaths[] = {
	{ "darkeye", Common::kPlatformWindows, "SAVEDDKY/" },
	{ nullptr, Common::kPlatformUnknown, nullptr },
};


static void quirkWarlock() {
	g_director->_loadSlowdownFactor = 150000;  // emulate a 1x CD drive
	g_director->_fpsLimit = 15;
}

static void quirkLimit15FPS() {
	g_director->_fpsLimit = 15;
}

static void quirkVirtualNightclub() {
	g_director->_colorDepth = 16;
}

static void quirkHollywoodHigh() {
	// Hollywood High demo has a killswitch that stops playback
	// if the year is after 1996.
	g_director->_forceDate.tm_year = 1996 - 1900;
	g_director->_forceDate.tm_mon = 0;
	g_director->_forceDate.tm_mday = 1;
	g_director->_forceDate.tm_wday = 0;
}

static void quirkLzone() {
	SearchMan.addSubDirectoryMatching(g_director->_gameDataDir, "win_data", 0, 2);
}

static void quirkMcLuhanWin() {
	g_director->_extraSearchPath.push_back("mcluhan\\");
	Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan;
	fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCBOLD13.FON");
	fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCLURG__.FON");
	fontMan->loadWindowsFont("MCLUHAN/SYSTEM/MCL1N___.FON");
}

static void quirkTrekTechWin() {
	Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan;
	fontMan->loadWindowsFont("TREKCON4.FON");
}

static void quirkTrekGuideTNGWin() {
	Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan;
	fontMan->loadWindowsFont("OMNI2/TREKENCY.FON");
	fontMan->loadWindowsFont("OMNI2/TREKOMNI.FON");
}


static void quirkPipCatalog() {
	g_director->_dirSeparator = '/';
}

static void quirkMcLuhanMac() {
	Common::SeekableReadStream *installer = Common::MacResManager::openFileOrDataFork("Understanding McLuhan Installer");

	if (!installer) {
		warning("quirkMcLuhanMac(): Cannot open installer file");
		return;
	}

	Common::Archive *archive = Common::createMacVISEArchive(installer);

	if (!archive) {
		warning("quirkMcLuhanMac(): Failed to open installer");
		return;
	}

	Common::MacResManager font;

	if (!font.open("McLuhan-Regular", *archive)) {
		warning("quirkMcLuhanMac(): Failed to load font file \"McLuhan-Regular\"");
		return;
	}

	Graphics::MacFontManager *fontMan = g_director->_wm->_fontMan;
	fontMan->loadFonts(&font);

	delete archive;
	delete installer;
}

const struct Quirk {
	const char *target;
	Common::Platform platform;
	void (*quirk)();
} quirks[] = {
	// Spaceship Warlock is designed to run as quickly as possible on a
	// single speed CD drive; there's often content just before a movie
	// transition which would otherwise get skipped past.
	{ "warlock", Common::kPlatformMacintosh, &quirkWarlock },
	{ "warlock", Common::kPlatformWindows, &quirkWarlock },

	// Eastern Mind sets the score to play back at a high frame rate,
	// however the developers were using slow hardware, so some
	// animations play back much faster than intended.
	// Limit the score framerate to be no higher than 15fps.
	{ "easternmind", Common::kPlatformMacintosh, &quirkLimit15FPS },
	{ "easternmind", Common::kPlatformWindows, &quirkLimit15FPS },

	// Sections of Hell Cab such as the prehistoric times need capped framerate.
	{ "hellcab", Common::kPlatformMacintosh, &quirkLimit15FPS },
	{ "hellcab", Common::kPlatformWindows, &quirkLimit15FPS },

	// Wrath of the Gods has shooting gallery minigames which are
	// clocked to 60fps; in reality this is far too fast to be playable.
	{ "wrath", Common::kPlatformMacintosh, &quirkLimit15FPS },
	{ "wrath", Common::kPlatformWindows, &quirkLimit15FPS },

	{ "hollywoodhigh", Common::kPlatformWindows, &quirkHollywoodHigh },

	{ "lzone", Common::kPlatformWindows, &quirkLzone },

	{ "mcluhan", Common::kPlatformWindows, &quirkMcLuhanWin },
	{ "mcluhan", Common::kPlatformMacintosh, &quirkMcLuhanMac },

	// Star Trek titles install fonts into the system
	{ "trektech", Common::kPlatformWindows, &quirkTrekTechWin },
	{ "trekguidetng", Common::kPlatformWindows, &quirkTrekGuideTNGWin },

	// Pippin game that uses Unix path separators rather than Mac
	{ "pipcatalog", Common::kPlatformPippin, &quirkPipCatalog },

	// Virtual Nightclub pops up a nag mesasage if the color depth isn't
	// exactly 16 bit.
	{ "vnc", Common::kPlatformWindows, &quirkVirtualNightclub },

	{ nullptr, Common::kPlatformUnknown, nullptr }
};

void DirectorEngine::gameQuirks(const char *target, Common::Platform platform) {
	for (auto q = quirks; q->target != nullptr; q++) {
		if (q->platform == Common::kPlatformUnknown || q->platform == platform)
			if (!strcmp(q->target, target)) {
				debugC(1, kDebugLoading, "Applying quirk for the target %s", target);

				q->quirk();
				break;
			}
	}

	CachedArchive::FileInputList list;
	for (auto f = cachedFiles; f->target != nullptr; f++) {
		if (f->platform == Common::kPlatformUnknown || f->platform == platform)
			if (!strcmp(f->target, target)) {
				int32 size = f->size;
				if (size == -1)
					size = strlen((const char *)f->data);
				list.push_back(CachedArchive::InputEntry(f->fileName, f->data, size));

				debugC(1, kDebugLoading, "Added file '%s' of size %d to the file cache", f->fileName, size);
			}
	}

	for (auto f = saveFilePaths; f->target != nullptr; f++) {
		if (f->platform == Common::kPlatformUnknown || f->platform == platform)
			if (!strcmp(f->target, target)) {
				// Inject files from the save game storage into the path
				Common::SaveFileManager *saves = g_system->getSavefileManager();
				// As save games are name-mangled by FileIO, demangle them here
				Common::String prefix = savePrefix() + '*';
				for (auto &it : saves->listSavefiles(prefix.c_str())) {
					Common::String demangled = f->path + it.substr(prefix.size() - 1);
					if (demangled.hasSuffixIgnoreCase(".txt")) {
						demangled = demangled.substr(0, demangled.size() - 4);
					}
					list.push_back(CachedArchive::InputEntry(demangled, nullptr, 0));
				}
			}

	}

	if (!list.empty()) {
		CachedArchive *archive = new CachedArchive(list);

		SearchMan.add(kQuirksCacheArchive, archive);
	}
}

void DirectorEngine::loadSlowdownCooloff(uint32 delay) {
	if (_loadSlowdownFactor)
		_loadSlowdownCooldownTime = g_system->getMillis() + delay;
}

/*****************
 * CachedArchive
 *****************/

CachedArchive::CachedArchive(const FileInputList &files)
	: _files() {
	for (FileInputList::const_iterator i = files.begin(); i != files.end(); ++i) {
		Entry entry;

		entry.data = i->data;
		entry.size = i->size;

		Common::Path name = i->name;
		name.toLowercase();
		_files[name] = entry;
	}
}

CachedArchive::~CachedArchive() {
	_files.clear();
}

bool CachedArchive::hasFile(const Common::Path &path) const {
	return (_files.find(path) != _files.end());
}

int CachedArchive::listMembers(Common::ArchiveMemberList &list) const {
	int count = 0;

	for (FileMap::const_iterator i = _files.begin(); i != _files.end(); ++i) {
		list.push_back(Common::ArchiveMemberList::value_type(new Common::GenericArchiveMember(i->_key, *this)));
		++count;
	}

	return count;
}

const Common::ArchiveMemberPtr CachedArchive::getMember(const Common::Path &path) const {
	if (!hasFile(path))
		return Common::ArchiveMemberPtr();

	return Common::ArchiveMemberPtr(new Common::GenericArchiveMember(path, *this));
}

Common::SeekableReadStream *CachedArchive::createReadStreamForMember(const Common::Path &path) const {
	FileMap::const_iterator fDesc = _files.find(path);
	if (fDesc == _files.end())
		return nullptr;

	return new Common::MemoryReadStream(fDesc->_value.data, fDesc->_value.size, DisposeAfterUse::NO);
}


} // End of namespace Director
