/* 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/savefile.h"
#include "common/stream.h"

#include "sci/sci.h"
#include "sci/engine/file.h"
#include "sci/engine/kernel.h"
#include "sci/engine/savegame.h"
#include "sci/engine/selector.h"
#include "sci/engine/state.h"

namespace Sci {

/*
 * Note on how file I/O is implemented: In ScummVM, one can not create/write
 * arbitrary data files, simply because many of our target platforms do not
 * support this. The only files one can create are savestates. But SCI has an
 * opcode to create and write to seemingly 'arbitrary' files. This is mainly
 * used in LSL3 for LARRY3.DRV (which is a game data file, not a driver, used
 * for persisting the results of the "age quiz" across restarts) and in LSL5
 * for MEMORY.DRV (which is again a game data file and contains the game's
 * password, XOR encrypted).
 * To implement that opcode, we combine the SaveFileManager with regular file
 * code, similarly to how the SCUMM HE engine does it.
 *
 * To handle opening a file called "foobar", what we do is this: First, we
 * create an 'augmented file name', by prepending the game target and a dash,
 * so if we running game target sq1sci, the name becomes "sq1sci-foobar".
 * Next, we check if such a file is known to the SaveFileManager. If so, we
 * we use that for reading/writing, delete it, whatever.
 *
 * If no such file is present but we were only asked to *read* the file,
 * we fallback to looking for a regular file called "foobar", and open that
 * for reading only.
 */

reg_t file_open(EngineState *s, const Common::String &filename, int mode, bool unwrapFilename) {
	Common::String englishName = g_sci->getSciLanguageString(filename, K_LANG_ENGLISH);
	englishName.toLowercase();

	Common::String wrappedName = unwrapFilename ? g_sci->wrapFilename(englishName) : englishName;
	Common::SeekableReadStream *inFile = 0;
	Common::WriteStream *outFile = 0;
	Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();

	bool isCompressed = true;
	const SciGameId gameId = g_sci->getGameId();

	// QFG Characters are saved via the CharSave object.
	// We leave them uncompressed so that they can be imported in later QFG
	// games, even when using the original interpreter.
	// We check for room numbers in here, because the file suffix can be changed by the user.
	// Rooms/Scripts: QFG1(EGA/VGA): 601, QFG2: 840, QFG3/4: 52
	switch (gameId) {
	case GID_QFG1:
	case GID_QFG1VGA:
		if (s->currentRoomNumber() == 601)
			isCompressed = false;
		break;
	case GID_QFG2:
		if (s->currentRoomNumber() == 840)
			isCompressed = false;
		break;
	case GID_QFG3:
	case GID_QFG4:
		if (s->currentRoomNumber() == 52)
			isCompressed = false;
		break;
	default:
		break;
	}

	if (mode == _K_FILE_MODE_OPEN_OR_FAIL) {
		// Try to open file, abort if not possible
		inFile = saveFileMan->openForLoading(wrappedName);
		// If no matching savestate exists: fall back to reading from a regular
		// file
		if (!inFile)
			inFile = SearchMan.createReadStreamForMember(englishName);

		if (!inFile)
			debugC(kDebugLevelFile, "  -> file_open(_K_FILE_MODE_OPEN_OR_FAIL): failed to open file '%s'", englishName.c_str());
	} else if (mode == _K_FILE_MODE_CREATE) {
		// Create the file, destroying any content it might have had
		outFile = saveFileMan->openForSaving(wrappedName, isCompressed);
		if (!outFile)
			debugC(kDebugLevelFile, "  -> file_open(_K_FILE_MODE_CREATE): failed to create file '%s'", englishName.c_str());
	} else if (mode == _K_FILE_MODE_OPEN_OR_CREATE) {
		// Try to open file, create it if it doesn't exist
		outFile = saveFileMan->openForSaving(wrappedName, isCompressed);
		if (!outFile)
			debugC(kDebugLevelFile, "  -> file_open(_K_FILE_MODE_CREATE): failed to create file '%s'", englishName.c_str());

		// QfG1 opens the character export file with _K_FILE_MODE_CREATE first,
		// closes it immediately and opens it again with this here. Perhaps
		// other games use this for read access as well. I guess changing this
		// whole code into using virtual files and writing them after close
		// would be more appropriate.
	} else {
		error("file_open: unsupported mode %d (filename '%s')", mode, englishName.c_str());
	}

	if (!inFile && !outFile) { // Failed
		debugC(kDebugLevelFile, "  -> file_open() failed");
		return SIGNAL_REG;
	}

	// Find a free file handle
	uint handle = 1; // Ignore _fileHandles[0]
	while ((handle < s->_fileHandles.size()) && s->_fileHandles[handle].isOpen())
		handle++;

	if (handle == s->_fileHandles.size()) {
		// Hit size limit => Allocate more space
		s->_fileHandles.resize(s->_fileHandles.size() + 1);
	}

	s->_fileHandles[handle]._in = inFile;
	s->_fileHandles[handle]._out = outFile;
	s->_fileHandles[handle]._name = englishName;

	debugC(kDebugLevelFile, "  -> opened file '%s' with handle %d", englishName.c_str(), handle);
	return make_reg(0, handle);
}

FileHandle *getFileFromHandle(EngineState *s, uint handle) {
	if ((handle == 0) || ((handle >= VIRTUALFILE_HANDLE_START) && (handle <= VIRTUALFILE_HANDLE_END))) {
		error("Attempt to use invalid file handle (%d)", handle);
		return 0;
	}

	if ((handle >= s->_fileHandles.size()) || !s->_fileHandles[handle].isOpen()) {
		warning("Attempt to use invalid/unused file handle %d", handle);
		return 0;
	}

	return &s->_fileHandles[handle];
}

int fgets_wrapper(EngineState *s, char *dest, int maxsize, int handle) {
	FileHandle *f = getFileFromHandle(s, handle);
	if (!f)
		return 0;

	if (!f->_in) {
		error("fgets_wrapper: Trying to read from file '%s' opened for writing", f->_name.c_str());
		return 0;
	}
	int readBytes = 0;
	if (maxsize > 1) {
		memset(dest, 0, maxsize);
		f->_in->readLine(dest, maxsize);
		readBytes = strlen(dest); // FIXME: sierra sci returned byte count and didn't react on NUL characters
		// The returned string must not have an ending LF
		if (readBytes > 0) {
			if (dest[readBytes - 1] == 0x0A)
				dest[readBytes - 1] = 0;
		}
	} else {
		*dest = 0;
	}

	debugC(kDebugLevelFile, "  -> FGets'ed \"%s\"", dest);
	return readBytes;
}

static bool _savegame_sort_byDate(const SavegameDesc &l, const SavegameDesc &r) {
	if (l.date != r.date)
		return (l.date > r.date);
	return (l.time > r.time);
}

// Create a sorted array containing all found savedgames
void listSavegames(Common::Array<SavegameDesc> &saves) {
	Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();

	// Load all saves
	Common::StringArray saveNames = saveFileMan->listSavefiles(g_sci->getSavegamePattern());

	for (Common::StringArray::const_iterator iter = saveNames.begin(); iter != saveNames.end(); ++iter) {
		Common::String filename = *iter;
		Common::SeekableReadStream *in;
		if ((in = saveFileMan->openForLoading(filename))) {
			SavegameMetadata meta;
			if (!get_savegame_metadata(in, &meta) || meta.name.empty()) {
				// invalid
				delete in;
				continue;
			}
			delete in;

			SavegameDesc desc;
			desc.id = strtol(filename.end() - 3, NULL, 10);
			desc.date = meta.saveDate;
			// We need to fix date in here, because we save DDMMYYYY instead of
			// YYYYMMDD, so sorting wouldn't work
			desc.date = ((desc.date & 0xFFFF) << 16) | ((desc.date & 0xFF0000) >> 8) | ((desc.date & 0xFF000000) >> 24);
			desc.time = meta.saveTime;
			desc.version = meta.version;

			if (meta.name.lastChar() == '\n')
				meta.name.deleteLastChar();

			Common::strlcpy(desc.name, meta.name.c_str(), SCI_MAX_SAVENAME_LENGTH);

			debug(3, "Savegame in file %s ok, id %d", filename.c_str(), desc.id);

			saves.push_back(desc);
		}
	}

	// Sort the list by creation date of the saves
	Common::sort(saves.begin(), saves.end(), _savegame_sort_byDate);
}

// Find a savedgame according to virtualId and return the position within our array
int findSavegame(Common::Array<SavegameDesc> &saves, int16 savegameId) {
	for (uint saveNr = 0; saveNr < saves.size(); saveNr++) {
		if (saves[saveNr].id == savegameId)
			return saveNr;
	}
	return -1;
}


FileHandle::FileHandle() : _in(0), _out(0) {
}

FileHandle::~FileHandle() {
	close();
}

void FileHandle::close() {
	delete _in;
	delete _out;
	_in = 0;
	_out = 0;
	_name.clear();
}

bool FileHandle::isOpen() const {
	return _in || _out;
}


void DirSeeker::addAsVirtualFiles(Common::String title, Common::String fileMask) {
	Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
	Common::StringArray foundFiles = saveFileMan->listSavefiles(fileMask);
	if (!foundFiles.empty()) {
		// Sort all filenames alphabetically
		Common::sort(foundFiles.begin(), foundFiles.end());

		_files.push_back(title);
		_virtualFiles.push_back("");
		Common::StringArray::iterator it;
		Common::StringArray::iterator it_end = foundFiles.end();

		for (it = foundFiles.begin(); it != it_end; it++) {
			Common::String regularFilename = *it;
			Common::String wrappedFilename = Common::String(regularFilename.c_str() + fileMask.size() - 1);

			Common::SeekableReadStream *testfile = saveFileMan->openForLoading(regularFilename);
			int32 testfileSize = testfile->size();
			delete testfile;
			if (testfileSize > 1024) // check, if larger than 1k. in that case its a saved game.
				continue; // and we dont want to have those in the list
			// We need to remove the prefix for display purposes
			_files.push_back(wrappedFilename);
			// but remember the actual name as well
			_virtualFiles.push_back(regularFilename);
		}
	}
}

Common::String DirSeeker::getVirtualFilename(uint fileNumber) {
	if (fileNumber >= _virtualFiles.size())
		error("invalid virtual filename access");
	return _virtualFiles[fileNumber];
}

reg_t DirSeeker::firstFile(const Common::String &mask, reg_t buffer, SegManager *segMan) {
	// Verify that we are given a valid buffer
	if (!buffer.getSegment()) {
		error("DirSeeker::firstFile('%s') invoked with invalid buffer", mask.c_str());
		return NULL_REG;
	}
	_outbuffer = buffer;
	_files.clear();
	_virtualFiles.clear();

	int QfGImport = g_sci->inQfGImportRoom();
	if (QfGImport) {
		_files.clear();
		addAsVirtualFiles("-QfG1-", "qfg1-*");
		addAsVirtualFiles("-QfG1VGA-", "qfg1vga-*");
		if (QfGImport > 2)
			addAsVirtualFiles("-QfG2-", "qfg2-*");
		if (QfGImport > 3)
			addAsVirtualFiles("-QfG3-", "qfg3-*");

		if (QfGImport == 3) {
			// QfG3 sorts the filelisting itself, we can't let that happen otherwise our
			//  virtual list would go out-of-sync
			reg_t savedHeros = segMan->findObjectByName("savedHeros");
			if (!savedHeros.isNull())
				writeSelectorValue(segMan, savedHeros, SELECTOR(sort), 0);
		}

	} else {
		// Prefix the mask
		const Common::String wrappedMask = g_sci->wrapFilename(mask);

		// Obtain a list of all files matching the given mask
		Common::SaveFileManager *saveFileMan = g_sci->getSaveFileManager();
		_files = saveFileMan->listSavefiles(wrappedMask);
	}

	// Reset the list iterator and write the first match to the output buffer,
	// if any.
	_iter = _files.begin();
	return nextFile(segMan);
}

reg_t DirSeeker::nextFile(SegManager *segMan) {
	if (_iter == _files.end()) {
		return NULL_REG;
	}

	Common::String string;

	if (_virtualFiles.empty()) {
		// Strip the prefix, if we don't got a virtual filelisting
		const Common::String wrappedString = *_iter;
		string = g_sci->unwrapFilename(wrappedString);
	} else {
		string = *_iter;
	}
	if (string.size() > 12)
		string = Common::String(string.c_str(), 12);
	segMan->strcpy(_outbuffer, string.c_str());

	// Return the result and advance the list iterator :)
	++_iter;
	return _outbuffer;
}


#ifdef ENABLE_SCI32

VirtualIndexFile::VirtualIndexFile(Common::String fileName) : _fileName(fileName), _changed(false) {
	Common::SeekableReadStream *inFile = g_sci->getSaveFileManager()->openForLoading(fileName);

	_bufferSize = inFile->size();
	_buffer = new char[_bufferSize];
	inFile->read(_buffer, _bufferSize);
	_ptr = _buffer;
	delete inFile;
}

VirtualIndexFile::VirtualIndexFile(uint32 initialSize) : _changed(false) {
	_bufferSize = initialSize;
	_buffer = new char[_bufferSize];
	_ptr = _buffer;
}

VirtualIndexFile::~VirtualIndexFile() {
	close();

	_bufferSize = 0;
	delete[] _buffer;
	_buffer = 0;
}

uint32 VirtualIndexFile::read(char *buffer, uint32 size) {
	uint32 curPos = _ptr - _buffer;
	uint32 finalSize = MIN<uint32>(size, _bufferSize - curPos);
	char *localPtr = buffer;

	for (uint32 i = 0; i < finalSize; i++)
		*localPtr++ = *_ptr++;

	return finalSize;
}

uint32 VirtualIndexFile::write(const char *buffer, uint32 size) {
	_changed = true;
	uint32 curPos = _ptr - _buffer;

	// Check if the buffer needs to be resized
	if (curPos + size >= _bufferSize) {
		_bufferSize = curPos + size + 1;
		char *tmp = _buffer;
		_buffer = new char[_bufferSize];
		_ptr = _buffer + curPos;
		memcpy(_buffer, tmp, _bufferSize);
		delete[] tmp;
	}

	for (uint32 i = 0; i < size; i++)
		*_ptr++ = *buffer++;

	return size;
}

uint32 VirtualIndexFile::readLine(char *buffer, uint32 size) {
	uint32 startPos = _ptr - _buffer;
	uint32 bytesRead = 0;
	char *localPtr = buffer;

	// This is not a full-blown implementation of readLine, but it
	// suffices for Phantasmagoria
	while (startPos + bytesRead < size) {
		bytesRead++;

		if (*_ptr == 0 || *_ptr == 0x0A) {
			_ptr++;
			*localPtr = 0;
			return bytesRead;
		} else {
			*localPtr++ = *_ptr++;
		}
	}

	return bytesRead;
}

bool VirtualIndexFile::seek(int32 offset, int whence) {
	uint32 startPos = _ptr - _buffer;
	assert(offset >= 0);

	switch (whence) {
	case SEEK_CUR:
		assert(startPos + offset < _bufferSize);
		_ptr += offset;
		break;
	case SEEK_SET:
		assert(offset < (int32)_bufferSize);
		_ptr = _buffer + offset;
		break;
	case SEEK_END:
		assert((int32)_bufferSize - offset >= 0);
		_ptr = _buffer + (_bufferSize - offset);
		break;
	}

	return true;
}

void VirtualIndexFile::close() {
	if (_changed && !_fileName.empty()) {
		Common::WriteStream *outFile = g_sci->getSaveFileManager()->openForSaving(_fileName);
		outFile->write(_buffer, _bufferSize);
		delete outFile;
	}

	// Maintain the buffer, and seek to the beginning of it
	_ptr = _buffer;
}

#endif

} // End of namespace Sci
