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

/*
 * This code is based on original Hugo Trilogy source code
 *
 * Copyright (c) 1989-1995 David P. Gray
 *
 */

#include "common/debug.h"
#include "common/system.h"
#include "common/savefile.h"
#include "common/textconsole.h"
#include "common/config-manager.h"
#include "common/translation.h"

#include "graphics/surface.h"
#include "graphics/thumbnail.h"

#include "gui/saveload.h"

#include "image/pcx.h"

#include "hugo/hugo.h"
#include "hugo/file.h"
#include "hugo/schedule.h"
#include "hugo/display.h"
#include "hugo/util.h"
#include "hugo/object.h"
#include "hugo/text.h"
#include "hugo/mouse.h"

namespace Hugo {

namespace {
static const char s_bootCypher[] = "Copyright 1992, David P Gray, Gray Design Associates";
static const int s_bootCypherLen = sizeof(s_bootCypher) - 1;
}


FileManager::FileManager(HugoEngine *vm) : _vm(vm) {
	_hasReadHeader = false;
	_firstUIFFl = true;

	_UIFHeader->_size = 0;
	_UIFHeader->_offset = 0;
	_soundHdr->_size = 0;
	_soundHdr->_offset = 0;
}

FileManager::~FileManager() {
}

/**
 * Name scenery and objects picture databases
 */
const char *FileManager::getBootFilename() const {
	return "HUGO.BSF";
}

const char *FileManager::getObjectFilename() const {
	return "objects.dat";
}

const char *FileManager::getSceneryFilename() const {
	return "scenery.dat";
}

const char *FileManager::getSoundFilename() const {
	return "sounds.dat";
}

const char *FileManager::getStringFilename() const {
	return "strings.dat";
}

const char *FileManager::getUifFilename() const {
	return "uif.dat";
}

/**
 * Read a pcx file of length len.  Use supplied seqPtr and image_p or
 * allocate space if NULL.  Name used for errors.  Returns address of seqPtr
 * Set first TRUE to initialize b_index (i.e. not reading a sequential image in file).
 */
Seq *FileManager::readPCX(Common::SeekableReadStream &f, Seq *seqPtr, byte *imagePtr, const bool firstFl, const char *name) {
	debugC(1, kDebugFile, "readPCX(..., %s)", name);

	// Allocate memory for Seq if 0
	if (seqPtr == nullptr) {
		if ((seqPtr = (Seq *)malloc(sizeof(Seq))) == nullptr)
			error("Insufficient memory to run game.");
	}

	Image::PCXDecoder pcx;
	if (!pcx.loadStream(f))
		error("Error while reading PCX image");

	const Graphics::Surface *pcxSurface = pcx.getSurface();
	if (pcxSurface->format.bytesPerPixel != 1)
		error("Invalid bytes per pixel in PCX surface (%d)", pcxSurface->format.bytesPerPixel);

	// Find size of image data in 8-bit DIB format
	// Note save of x2 - marks end of valid data before garbage
	seqPtr->_lines = pcxSurface->h;
	seqPtr->_x2 = seqPtr->_bytesPerLine8 = pcxSurface->w;
	// Size of the image
	uint16 size = pcxSurface->w * pcxSurface->h;

	// Allocate memory for image data if NULL
	if (imagePtr == nullptr)
		imagePtr = (byte *)malloc((size_t) size);

	assert(imagePtr);

	seqPtr->_imagePtr = imagePtr;
	for (uint16 y = 0; y < pcxSurface->h; y++)
		memcpy(imagePtr + y * pcxSurface->w, pcxSurface->getBasePtr(0, y), pcxSurface->w);

	return seqPtr;
}

/**
 * Read object file of PCC images into object supplied
 */
void FileManager::readImage(const int objNum, Object *objPtr) {
	debugC(1, kDebugFile, "readImage(%d, Object *objPtr)", objNum);

	/**
	 * Structure of object file lookup entry
	 */
	struct objBlock_t {
		uint32 objOffset;
		uint32 objLength;
	};

	if (!objPtr->_seqNumb)                           // This object has no images
		return;

	if (_vm->isPacked()) {
		_objectsArchive.seek((uint32)objNum * sizeof(objBlock_t), SEEK_SET);

		objBlock_t objBlock;                        // Info on file within database
		objBlock.objOffset = _objectsArchive.readUint32LE();
		objBlock.objLength = _objectsArchive.readUint32LE();

		_objectsArchive.seek(objBlock.objOffset, SEEK_SET);
	} else {
		Common::Path buf;
		buf = _vm->_picDir.appendComponent(Common::String(_vm->_text->getNoun(objPtr->_nounIndex, 0)) + ".PIX");
		if (!_objectsArchive.open(buf)) {
			buf = Common::String(_vm->_text->getNoun(objPtr->_nounIndex, 0)) + ".PIX";
			if (!_objectsArchive.open(buf))
				error("File not found: %s", buf.toString().c_str());
		}
	}

	bool  firstImgFl = true;                        // Initializes pcx read function
	Seq *seqPtr = nullptr;                              // Ptr to sequence structure

	// Now read the images into an images list
	for (int j = 0; j < objPtr->_seqNumb; j++) {     // for each sequence
		for (int k = 0; k < objPtr->_seqList[j]._imageNbr; k++) { // each image
			if (k == 0) {                           // First image
				// Read this image - allocate both seq and image memory
				seqPtr = readPCX(_objectsArchive, nullptr, nullptr, firstImgFl, _vm->_text->getNoun(objPtr->_nounIndex, 0));
				objPtr->_seqList[j]._seqPtr = seqPtr;
				firstImgFl = false;
			} else {                                // Subsequent image
				// Read this image - allocate both seq and image memory
				seqPtr->_nextSeqPtr = readPCX(_objectsArchive, nullptr, nullptr, firstImgFl, _vm->_text->getNoun(objPtr->_nounIndex, 0));
				seqPtr = seqPtr->_nextSeqPtr;
			}

			// Compute the bounding box - x1, x2, y1, y2
			// Note use of x2 - marks end of valid data in row
			uint16 x2 = seqPtr->_x2;
			seqPtr->_x1 = seqPtr->_x2;
			seqPtr->_x2 = 0;
			seqPtr->_y1 = seqPtr->_lines;
			seqPtr->_y2 = 0;

			ImagePtr dibPtr = seqPtr->_imagePtr;
			for (int y = 0; y < seqPtr->_lines; y++, dibPtr += seqPtr->_bytesPerLine8 - x2) {
				for (int x = 0; x < x2; x++) {
					if (*dibPtr++) {                // Some data found
						if (x < seqPtr->_x1)
							seqPtr->_x1 = x;
						if (x > seqPtr->_x2)
							seqPtr->_x2 = x;
						if (y < seqPtr->_y1)
							seqPtr->_y1 = y;
						if (y > seqPtr->_y2)
							seqPtr->_y2 = y;
					}
				}
			}
		}
		assert(seqPtr);
		seqPtr->_nextSeqPtr = objPtr->_seqList[j]._seqPtr; // loop linked list to head
	}

	// Set the current image sequence to first or last
	switch (objPtr->_cycling) {
	case kCycleInvisible:                           // (May become visible later)
	case kCycleAlmostInvisible:
	case kCycleNotCycling:
	case kCycleForward:
		objPtr->_currImagePtr = objPtr->_seqList[0]._seqPtr;
		break;
	case kCycleBackward:
		objPtr->_currImagePtr = seqPtr;
		break;
	default:
		warning("Unexpected cycling: %d", objPtr->_cycling);
	}

	if (!_vm->isPacked())
		_objectsArchive.close();
}

/**
 * Read sound (or music) file data.  Call with SILENCE to free-up
 * any allocated memory.  Also returns size of data
 */
SoundPtr FileManager::getSound(const int16 sound, uint16 *size) {
	debugC(1, kDebugFile, "getSound(%d)", sound);

	// No more to do if SILENCE (called for cleanup purposes)
	if (sound == _vm->_soundSilence)
		return nullptr;

	// Open sounds file
	Common::File fp;                                // Handle to SOUND_FILE
	if (!fp.open(getSoundFilename())) {
		warning("Hugo Error: File not found %s", getSoundFilename());
		return nullptr;
	}

	if (!_hasReadHeader) {
		for (int i = 0; i < kMaxSounds; i++) {
			_soundHdr[i]._size = fp.readUint16LE();
			_soundHdr[i]._offset = fp.readUint32LE();
		}
		if (fp.err())
			error("Wrong sound file format");
		_hasReadHeader = true;
	}

	*size = _soundHdr[sound]._size;
	if (*size == 0)
		error("Wrong sound file format or missing sound %d", sound);

	// Allocate memory for sound or music, if possible
	SoundPtr soundPtr = (byte *)malloc(_soundHdr[sound]._size); // Ptr to sound data
	assert(soundPtr);

	// Seek to data and read it
	fp.seek(_soundHdr[sound]._offset, SEEK_SET);
	if (fp.read(soundPtr, _soundHdr[sound]._size) != _soundHdr[sound]._size)
		error("Wrong sound file format");

	fp.close();

	return soundPtr;
}

/**
 * Save game to supplied slot
 */
bool FileManager::saveGame(const int16 slot, const Common::String &descrip) {
	debugC(1, kDebugFile, "saveGame(%d, %s)", slot, descrip.c_str());

	int16 savegameId;
	Common::String savegameDescription;

	if (slot == -1) {
		GUI::SaveLoadChooser *dialog = new GUI::SaveLoadChooser(_("Save game:"), _("Save"), true);
		savegameId = dialog->runModalWithCurrentTarget();
		savegameDescription = dialog->getResultString();
		delete dialog;
	} else {
		savegameId = slot;
		if (!descrip.empty()) {
			savegameDescription = descrip;
		} else {
			savegameDescription = Common::String::format("Quick save #%d", slot);
		}
	}

	if (savegameId < 0)                             // dialog aborted
		return false;

	Common::String savegameFile = _vm->getSaveStateName(savegameId);
	Common::SaveFileManager *saveMan = g_system->getSavefileManager();
	Common::OutSaveFile *out = saveMan->openForSaving(savegameFile);

	if (!out) {
		warning("Can't create file '%s', game not saved", savegameFile.c_str());
		return false;
	}

	// Write version.  We can't restore from obsolete versions
	out->writeByte(kSavegameVersion);

	if (savegameDescription == "") {
		savegameDescription = "Untitled savegame";
	}

	out->writeSint16BE(savegameDescription.size() + 1);
	out->write(savegameDescription.c_str(), savegameDescription.size() + 1);

	Graphics::saveThumbnail(*out);

	TimeDate curTime;
	_vm->_system->getTimeAndDate(curTime);

	uint32 saveDate = (curTime.tm_mday & 0xFF) << 24 | ((curTime.tm_mon + 1) & 0xFF) << 16 | ((curTime.tm_year + 1900) & 0xFFFF);
	uint16 saveTime = (curTime.tm_hour & 0xFF) << 8 | ((curTime.tm_min) & 0xFF);

	out->writeUint32BE(saveDate);
	out->writeUint16BE(saveTime);

	_vm->_object->saveObjects(out);

	const Status &gameStatus = _vm->getGameStatus();

	// Save whether hero image is swapped
	out->writeByte(_vm->_heroImage);

	// Save score
	out->writeSint16BE(_vm->getScore());

	// Save story mode
	out->writeByte((gameStatus._storyModeFl) ? 1 : 0);

	// Save jumpexit mode
	out->writeByte((_vm->_mouse->getJumpExitFl()) ? 1 : 0);

	// Save gameover status
	out->writeByte((gameStatus._gameOverFl) ? 1 : 0);

	// Save screen states
	for (int i = 0; i < _vm->_numStates; i++)
		out->writeByte(_vm->_screenStates[i]);

	_vm->_scheduler->saveSchedulerData(out);
	// Save palette table
	_vm->_screen->savePal(out);

	// Save maze status
	out->writeByte((_vm->_maze._enabledFl) ? 1 : 0);
	out->writeByte(_vm->_maze._size);
	out->writeSint16BE(_vm->_maze._x1);
	out->writeSint16BE(_vm->_maze._y1);
	out->writeSint16BE(_vm->_maze._x2);
	out->writeSint16BE(_vm->_maze._y2);
	out->writeSint16BE(_vm->_maze._x3);
	out->writeSint16BE(_vm->_maze._x4);
	out->writeByte(_vm->_maze._firstScreenIndex);

	out->writeByte((byte)_vm->getGameStatus()._viewState);

	out->finalize();

	delete out;

	return true;
}

/**
 * Restore game from supplied slot number
 */
bool FileManager::restoreGame(const int16 slot) {
	debugC(1, kDebugFile, "restoreGame(%d)", slot);

	int16 savegameId;

	if (slot == -1) {
		GUI::SaveLoadChooser *dialog = new GUI::SaveLoadChooser(_("Restore game:"), _("Restore"), false);
		savegameId = dialog->runModalWithCurrentTarget();
		delete dialog;
	} else {
		savegameId = slot;
	}

	if (savegameId < 0)                             // dialog aborted
		return false;

	Common::String savegameFile = _vm->getSaveStateName(savegameId);
	Common::SaveFileManager *saveMan = g_system->getSavefileManager();
	Common::InSaveFile *in = saveMan->openForLoading(savegameFile);

	if (!in)
		return false;

	// Initialize new-game status
	_vm->initStatus();

	// Check version, can't restore from different versions
	int saveVersion = in->readByte();
	if (saveVersion != kSavegameVersion) {
		warning("Savegame of incompatible version");
		delete in;
		return false;
	}

	// Skip over description
	int32 saveGameNameSize = in->readSint16BE();
	in->skip(saveGameNameSize);

	Graphics::skipThumbnail(*in);

	in->skip(6);                                    // Skip date & time

	// If hero image is currently swapped, swap it back before restore
	if (_vm->_heroImage != kHeroIndex)
		_vm->_object->swapImages(kHeroIndex, _vm->_heroImage, true);

	_vm->_object->restoreObjects(in);

	_vm->_heroImage = in->readByte();

	// Restore ptrs to currently loaded objects
	_vm->_object->restoreAllSeq();

	// If hero swapped in saved game, swap it
	byte heroImg = _vm->_heroImage;
	if (heroImg != kHeroIndex)
		_vm->_object->swapImages(kHeroIndex, _vm->_heroImage, true);
	_vm->_heroImage = heroImg;

	Status &gameStatus = _vm->getGameStatus();

	int score = in->readSint16BE();
	_vm->setScore(score);
#ifdef USE_TTS
	_vm->_previousScore = -1;
#endif

	gameStatus._storyModeFl = (in->readByte() == 1);
	_vm->_mouse->setJumpExitFl(in->readByte() == 1);
	gameStatus._gameOverFl = (in->readByte() == 1);
	for (int i = 0; i < _vm->_numStates; i++)
		_vm->_screenStates[i] = in->readByte();

	_vm->_scheduler->restoreSchedulerData(in);

	// Restore palette and change it if necessary
	_vm->_screen->restorePal(in);

	// Restore maze status
	_vm->_maze._enabledFl = (in->readByte() == 1);
	_vm->_maze._size = in->readByte();
	_vm->_maze._x1 = in->readSint16BE();
	_vm->_maze._y1 = in->readSint16BE();
	_vm->_maze._x2 = in->readSint16BE();
	_vm->_maze._y2 = in->readSint16BE();
	_vm->_maze._x3 = in->readSint16BE();
	_vm->_maze._x4 = in->readSint16BE();
	_vm->_maze._firstScreenIndex = in->readByte();

	_vm->_scheduler->restoreScreen(*_vm->_screenPtr);
	if ((_vm->getGameStatus()._viewState = (Vstate) in->readByte()) != kViewPlay)
		_vm->_screen->hideCursor();


	delete in;
	return true;
}

/**
 * Reads boot file for program environment.  Fatal error if not there or
 * file checksum is bad.  De-crypts structure while checking checksum
 */
void FileManager::readBootFile() {
	debugC(1, kDebugFile, "readBootFile()");

	Common::File ofp;
	if (!ofp.open(getBootFilename())) {
		if (_vm->_gameVariant == kGameVariantH1Dos) {
			//TODO initialize properly _boot structure
			warning("readBootFile - Skipping as H1 Dos may be a freeware");
			memset(_vm->_boot._distrib, '\0', sizeof(_vm->_boot._distrib));
			_vm->_boot._registered = kRegFreeware;
			return;
		} else if (_vm->getPlatform() == Common::kPlatformDOS) {
			warning("readBootFile - Skipping as H2 and H3 Dos may be shareware");
			memset(_vm->_boot._distrib, '\0', sizeof(_vm->_boot._distrib));
			_vm->_boot._registered = kRegShareware;
			return;
		} else {
			Utils::notifyBox(Common::String::format("Missing startup file '%s'", getBootFilename()));
			_vm->getGameStatus()._doQuitFl = true;
			return;
		}
	}

	if (ofp.size() < (int32)sizeof(_vm->_boot)) {
		Utils::notifyBox(Common::String::format("Corrupted startup file '%s'", getBootFilename()));
		_vm->getGameStatus()._doQuitFl = true;
		return;
	}

	_vm->_boot._checksum = ofp.readByte();
	_vm->_boot._registered = ofp.readByte();
	ofp.read(_vm->_boot._pbswitch, sizeof(_vm->_boot._pbswitch));
	ofp.read(_vm->_boot._distrib, sizeof(_vm->_boot._distrib));
	_vm->_boot._exitLen = ofp.readUint16LE();
	ofp.close();

	byte *p = (byte *)&_vm->_boot;

	byte checksum = 0;
	for (uint32 i = 0; i < sizeof(_vm->_boot); i++) {
		checksum ^= p[i];
		p[i] ^= s_bootCypher[i % s_bootCypherLen];
	}

	if (checksum) {
		Utils::notifyBox(Common::String::format("Corrupted startup file '%s'", getBootFilename()));
		_vm->getGameStatus()._doQuitFl = true;
	}
}

/**
 * Returns address of uif_hdr[id], reading it in if first call
 * This file contains, between others, the bitmaps of the fonts used in the application
 * UIF means User interface database (Windows Only)
 */
UifHdr *FileManager::getUIFHeader(const Uif id) {
	debugC(1, kDebugFile, "getUIFHeader(%d)", id);

	// Initialize offset lookup if not read yet
	if (_firstUIFFl) {
		_firstUIFFl = false;
		// Open unbuffered to do far read
		Common::File ip;                            // Image data file
		if (!ip.open(getUifFilename()))
			error("File not found: %s", getUifFilename());

		if (ip.size() < (int32)sizeof(_UIFHeader))
			error("Wrong UIF file format");

		for (int i = 0; i < kMaxUifs; ++i) {
			_UIFHeader[i]._size = ip.readUint16LE();
			_UIFHeader[i]._offset = ip.readUint32LE();
		}

		ip.close();
	}
	return &_UIFHeader[id];
}

/**
 * Read uif item into supplied buffer.
 */
void FileManager::readUIFItem(const int16 id, byte *buf) {
	debugC(1, kDebugFile, "readUIFItem(%d, ...)", id);

	// Open uif file to read data
	Common::File ip;                                // UIF_FILE handle
	if (!ip.open(getUifFilename()))
		error("File not found: %s", getUifFilename());

	// Seek to data
	UifHdr *_UIFHeaderPtr = getUIFHeader((Uif)id);
	ip.seek(_UIFHeaderPtr->_offset, SEEK_SET);

	// We support pcx images and straight data
	Seq *dummySeq;                                // Dummy Seq for image data
	switch (id) {
	case UIF_IMAGES:                                // Read uif images file
		dummySeq = readPCX(ip, nullptr, buf, true, getUifFilename());
		free(dummySeq);
		break;
	default:                                        // Read file data into supplied array
		if (ip.read(buf, _UIFHeaderPtr->_size) != _UIFHeaderPtr->_size)
			error("Wrong UIF file format");
		break;
	}

	ip.close();
}

/**
 * Read the uif image file (inventory icons)
 */
void FileManager::readUIFImages() {
	debugC(1, kDebugFile, "readUIFImages()");

	readUIFItem(UIF_IMAGES, _vm->_screen->getGUIBuffer());   // Read all uif images
}

} // End of namespace Hugo
