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

#include "common/file.h"
#include "common/savefile.h"
#include "graphics/scaler.h"
#include "graphics/thumbnail.h"
#include "titanic/game_manager.h"
#include "titanic/titanic.h"
#include "titanic/core/dont_save_file_item.h"
#include "titanic/core/node_item.h"
#include "titanic/core/project_item.h"
#include "titanic/core/view_item.h"
#include "titanic/pet_control/pet_control.h"

namespace Titanic {

#define CURRENT_SAVEGAME_VERSION 1
#define MAX_SAVEGAME_SLOTS 99
#define MINIMUM_SAVEGAME_VERSION 1

static const char *const SAVEGAME_STR = "TNIC";
#define SAVEGAME_STR_SIZE 4

EMPTY_MESSAGE_MAP(CProjectItem, CFileItem);

void CFileListItem::save(SimpleFile *file, int indent) {
	file->writeNumberLine(0, indent);
	file->writeQuotedLine(_name, indent);

	ListItem::save(file, indent);
}

void CFileListItem::load(SimpleFile *file) {
	file->readNumber();
	_name = file->readString();

	ListItem::load(file);
}

/*------------------------------------------------------------------------*/

CProjectItem::CProjectItem() : _nextRoomNumber(0), _nextMessageNumber(0),
		_nextObjectNumber(0), _gameManager(nullptr) {
}

void CProjectItem::save(SimpleFile *file, int indent) {
	file->writeNumberLine(6, indent);
	file->writeQuotedLine("Next Avail. Object Number", indent);
	file->writeNumberLine(_nextObjectNumber, indent);
	file->writeQuotedLine("Next Avail. Message Number", indent);
	file->writeNumberLine(_nextMessageNumber, indent);

	file->writeQuotedLine("", indent);
	_files.save(file, indent);

	file->writeQuotedLine("Next Avail. Room Number", indent);
	file->writeNumberLine(_nextRoomNumber, indent);

	CTreeItem::save(file, indent);
}

void CProjectItem::buildFilesList() {
	_files.destroyContents();

	CTreeItem *treeItem = getFirstChild();
	while (treeItem) {
		if (treeItem->isFileItem()) {
			CString name = static_cast<CFileItem *>(treeItem)->getFilename();
			_files.add()->_name = name;
		}

		treeItem = getNextSibling();
	}
}

void CProjectItem::load(SimpleFile *file) {
	int val = file->readNumber();
	_files.destroyContents();
	int count;

	switch (val) {
	case 1:
		file->readBuffer();
		_nextRoomNumber = file->readNumber();
		// Deliberate fall-through

	case 0:
		// Load the list of files
		count = file->readNumber();
		for (int idx = 0; idx < count; ++idx) {
			CString name = file->readString();
			_files.add()->_name = name;
		}
		break;

	case 6:
		file->readBuffer();
		_nextObjectNumber = file->readNumber();
		// Deliberate fall-through

	case 5:
		file->readBuffer();
		_nextMessageNumber = file->readNumber();
		// Deliberate fall-through

	case 4:
		file->readBuffer();
		// Deliberate fall-through

	case 2:
	case 3:
		_files.load(file);
		file->readBuffer();
		_nextRoomNumber = file->readNumber();
		break;

	default:
		break;
	}

	CTreeItem::load(file);
}

CGameManager *CProjectItem::getGameManager() const {
	return _gameManager;
}

void CProjectItem::setGameManager(CGameManager *gameManager) {
	if (!_gameManager)
		_gameManager = gameManager;
}

void CProjectItem::resetGameManager() {
	_gameManager = nullptr;
}

void CProjectItem::loadGame(int slotId) {
	CompressedFile file;

	// Clear any existing project contents and call preload code
	clear();
	preLoad();

	// Open either an existing savegame slot or the new game template
	if (slotId >= 0) {
		Common::InSaveFile *saveFile = g_system->getSavefileManager()->openForLoading(
			g_vm->generateSaveName(slotId));
		file.open(saveFile);
	} else {
		Common::File *newFile = new Common::File();
		if (!newFile->open("newgame.st"))
			error("Could not open newgame.st");
		file.open(newFile);
	}

	// Load the savegame header in
	TitanicSavegameHeader header;
	readSavegameHeader(&file, header);
	delete header._thumbnail;

	// Load the contents in
	CProjectItem *newProject = loadData(&file);
	file.IsClassStart();
	getGameManager()->load(&file);

	file.close();

	// Clear existing project
	clear();

	// Detach each item under the loaded project, and re-attach them
	// to the existing project instance (this)
	CTreeItem *item;
	while ((item = newProject->getFirstChild()) != nullptr) {
		item->detach();
		item->addUnder(this);
	}

	// Loaded project instance is no longer needed
	newProject->destroyAll();

	// Post-load processing
	postLoad();
}

void CProjectItem::saveGame(int slotId, const CString &desc) {
	CompressedFile file;
	Common::OutSaveFile *saveFile = g_system->getSavefileManager()->openForSaving(
		g_vm->generateSaveName(slotId), false);
	file.open(saveFile);

	// Signal the game is being saved
	preSave();

	// Write out the savegame header
	TitanicSavegameHeader header;
	header._saveName = desc;
	writeSavegameHeader(&file, header);

	// Save the contents out
	saveData(&file, this);

	// Save the game manager data
	_gameManager->save(&file);

	// Close the file and signal that the saving has finished
	file.close();
	postSave();
}

void CProjectItem::clear() {
	CTreeItem *item;
	while ((item = getFirstChild()) != nullptr)
		item->destroyAll();
}

CProjectItem *CProjectItem::loadData(SimpleFile *file) {
	if (!file->IsClassStart())
		return nullptr;

	CProjectItem *root = nullptr;
	CTreeItem *parent = nullptr;
	CTreeItem *item = nullptr;

	do {
		CString entryString = file->readString();

		if (entryString == "ALONG") {
			// Move along, nothing needed
		} else if (entryString == "UP") {
			// Move up
			if (parent == nullptr ||
				(parent = parent->getParent()) == nullptr)
				break;
		} else if (entryString == "DOWN") {
			// Move down
			if (parent == nullptr)
				parent = item;
			else
				parent = parent->getLastChild();
		} else {
			// Create new class instance
			item = dynamic_cast<CTreeItem *>(CSaveableObject::createInstance(entryString));
			assert(item);

			if (root) {
				// Already created root project
				item->addUnder(parent);
			} else {
				root = dynamic_cast<CProjectItem *>(item);
				assert(root);
				root->_filename = _filename;
			}

			// Load the data for the item
			item->load(file);
		}

		file->IsClassStart();
	} while (file->IsClassStart());

	return root;
}

void CProjectItem::saveData(SimpleFile *file, CTreeItem *item) const {
	while (item) {
		item->saveHeader(file, 0);
		item->save(file, 1);
		item->saveFooter(file, 0);

		CTreeItem *child = item->getFirstChild();
		if (child) {
			file->write("\n{\n", 3);
			file->writeQuotedString("DOWN");
			file->write("\n}\n", 3);
			saveData(file, child);
			file->write("\n{\n", 3);
			file->writeQuotedString("UP");
		} else {
			file->write("\n{\n", 3);
			file->writeQuotedString("ALONG");
		}

		file->write("\n}\n", 3);
		item = item->getNextSibling();
	}
}

void CProjectItem::preLoad() {
	if (_gameManager)
		_gameManager->preLoad();
}

void CProjectItem::postLoad() {
	CGameManager *gameManager = getGameManager();
	if (gameManager)
		gameManager->postLoad(this);

	CPetControl *petControl = getPetControl();
	if (petControl)
		petControl->postLoad();
}

void CProjectItem::preSave() {
	if (_gameManager)
		_gameManager->preSave(this);
}

void CProjectItem::postSave() {
	if (_gameManager)
		_gameManager->postSave();
}

CPetControl *CProjectItem::getPetControl() const {
	CDontSaveFileItem *fileItem = getDontSaveFileItem();
	CTreeItem *treeItem;

	if (!fileItem || (treeItem = fileItem->getLastChild()) == nullptr)
		return nullptr;

	while (treeItem) {
		CPetControl *petControl = dynamic_cast<CPetControl *>(treeItem);
		if (petControl)
			return petControl;

		treeItem = treeItem->getPriorSibling();
	}

	return nullptr;
}

CRoomItem *CProjectItem::findFirstRoom() const {
	return dynamic_cast<CRoomItem *>(findChildInstance(CRoomItem::_type));
}

CTreeItem *CProjectItem::findChildInstance(ClassDef *classDef) const {
	CTreeItem *treeItem = getFirstChild();
	if (treeItem == nullptr)
		return nullptr;

	do {
		CTreeItem *childItem = treeItem->getFirstChild();
		if (childItem) {
			do {
				if (childItem->isInstanceOf(classDef))
					return childItem;
			} while ((childItem = childItem->getNextSibling()) != nullptr);
		}
	} while ((treeItem = treeItem->getNextSibling()) != nullptr);

	return nullptr;
}

CRoomItem *CProjectItem::findNextRoom(CRoomItem *priorRoom) const {
	return dynamic_cast<CRoomItem *>(findSiblingInstanceOf(CRoomItem::_type, priorRoom));
}

CTreeItem *CProjectItem::findSiblingInstanceOf(ClassDef *classDef, CTreeItem *startItem) const {
	CTreeItem *treeItem = startItem->getParent()->getNextSibling();
	if (treeItem == nullptr)
		return nullptr;

	return findChildInstance(classDef);
}

CDontSaveFileItem *CProjectItem::getDontSaveFileItem() const {
	for (CTreeItem *treeItem = getFirstChild(); treeItem; treeItem = treeItem->getNextSibling()) {
		if (treeItem->isInstanceOf(CDontSaveFileItem::_type))
			return dynamic_cast<CDontSaveFileItem *>(treeItem);
	}

	return nullptr;
}

CRoomItem *CProjectItem::findHiddenRoom() {
	return dynamic_cast<CRoomItem *>(findByName("HiddenRoom"));
}

CViewItem *CProjectItem::findView(int roomNumber, int nodeNumber, int viewNumber) {
	CTreeItem *treeItem = getFirstChild();
	CRoomItem *roomItem = nullptr;

	// Scan for the specified room
	if (treeItem) {
		do {
			CTreeItem *childItem = treeItem->getFirstChild();
			CRoomItem *rItem = dynamic_cast<CRoomItem *>(childItem);
			if (rItem && rItem->_roomNumber == roomNumber) {
				roomItem = rItem;
				break;
			}
		} while ((treeItem = treeItem->getNextSibling()) != nullptr);
	}
	if (!roomItem)
		return nullptr;

	// Scan for the specified node within the room
	CNodeItem *nodeItem = nullptr;

	CNodeItem *nItem = dynamic_cast<CNodeItem *>(
		roomItem->findChildInstanceOf(CNodeItem::_type));
	for (; nItem && !nodeItem; nItem = dynamic_cast<CNodeItem *>(
			findNextInstanceOf(CNodeItem::_type, nItem))) {
		if (nItem->_nodeNumber == nodeNumber)
			nodeItem = nItem;
	}
	if (!nodeItem)
		return nullptr;

	// Scan for the specified view within the node
	CViewItem *viewItem = dynamic_cast<CViewItem *>(
		nodeItem->findChildInstanceOf(CViewItem::_type));
	for (; viewItem; viewItem = dynamic_cast<CViewItem *>(
			findNextInstanceOf(CViewItem::_type, viewItem))) {
		if (viewItem->_viewNumber == viewNumber)
			return viewItem;
	}

	return nullptr;
}

SaveStateList CProjectItem::getSavegameList(const Common::String &target) {
	Common::SaveFileManager *saveFileMan = g_system->getSavefileManager();
	Common::StringArray filenames;
	Common::String saveDesc;
	Common::String pattern = Common::String::format("%s.0??", target.c_str());
	TitanicSavegameHeader header;

	filenames = saveFileMan->listSavefiles(pattern);
	sort(filenames.begin(), filenames.end());   // Sort to get the files in numerical order

	SaveStateList saveList;
	for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) {
		const char *ext = strrchr(file->c_str(), '.');
		int slot = ext ? atoi(ext + 1) : -1;

		if (slot >= 0 && slot < MAX_SAVEGAME_SLOTS) {
			Common::InSaveFile *in = g_system->getSavefileManager()->openForLoading(*file);

			if (in) {
				SimpleFile f;
				f.open(in);
				if (!readSavegameHeader(&f, header))
					continue;

				saveList.push_back(SaveStateDescriptor(slot, header._saveName));

				header._thumbnail->free();
				delete header._thumbnail;
				delete in;
			}
		}
	}

	return saveList;
}

bool CProjectItem::readSavegameHeader(SimpleFile *file, TitanicSavegameHeader &header) {
	char saveIdentBuffer[SAVEGAME_STR_SIZE + 1];
	header._thumbnail = nullptr;

	// Validate the header Id
	file->unsafeRead(saveIdentBuffer, SAVEGAME_STR_SIZE + 1);
	if (strncmp(saveIdentBuffer, SAVEGAME_STR, SAVEGAME_STR_SIZE)) {
		file->seek(-SAVEGAME_STR_SIZE, SEEK_CUR);
		header._saveName = "Unnamed";
		return true;
	}

	header._version = file->readByte();
	if (header._version < MINIMUM_SAVEGAME_VERSION || header._version > CURRENT_SAVEGAME_VERSION)
		return false;

	// Read in the string
	header._saveName.clear();
	char ch;
	while ((ch = (char)file->readByte()) != '\0') header._saveName += ch;

	// Get the thumbnail
	header._thumbnail = Graphics::loadThumbnail(*file);
	if (!header._thumbnail)
		return false;

	// Read in save date/time
	header._year = file->readUint16LE();
	header._month = file->readUint16LE();
	header._day = file->readUint16LE();
	header._hour = file->readUint16LE();
	header._minute = file->readUint16LE();
	header._totalFrames = file->readUint32LE();

	return true;
}

void CProjectItem::writeSavegameHeader(SimpleFile *file, TitanicSavegameHeader &header) {
	// Write out a savegame header
	file->write(SAVEGAME_STR, SAVEGAME_STR_SIZE + 1);

	file->writeByte(CURRENT_SAVEGAME_VERSION);

	// Write savegame name
	file->write(header._saveName.c_str(), header._saveName.size());
	file->writeByte('\0');

	// Create a thumbnail of the screen and save it out
	Graphics::Surface *thumb = createThumbnail();
	Graphics::saveThumbnail(*file, *thumb);
	thumb->free();
	delete thumb;

	// Write out the save date/time
	TimeDate td;
	g_system->getTimeAndDate(td);
	file->writeUint16LE(td.tm_year + 1900);
	file->writeUint16LE(td.tm_mon + 1);
	file->writeUint16LE(td.tm_mday);
	file->writeUint16LE(td.tm_hour);
	file->writeUint16LE(td.tm_min);
	file->writeUint32LE(g_vm->_events->getFrameCounter());
}

Graphics::Surface *CProjectItem::createThumbnail() {
	Graphics::Surface *thumb = new Graphics::Surface();

	::createThumbnailFromScreen(thumb);
	return thumb;
}

} // End of namespace Titanic
