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

/* Compress SAGA engine digital sound files into MP3 and Ogg Vorbis format */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#include "compress.h"
#include "compress_saga.h"
#include "common/md5.h"
#include "common/file.h"
#include "common/util.h"
#include "sound/audiostream.h"
#include "sound/voc.h"
#include "sound/wave.h"
#include "sound/adpcm.h"

#define FILE_MD5_BYTES 5000
#define RSC_TABLEINFO_SIZE 8
#define RSC_TABLEENTRY_SIZE 8
#define HEADER_SIZE 9

// Known ITE files
static CompressSaga::GameFileDescription ITE_GameFiles[] = {
	//	Common::Filename					swapEndian	md5									resourceType	frequency	stereo
	{"sounds.rsc",					false,		"e2ccb61c325d6d1ead3be0e731fe29fe", kSoundPCM,		22050,		false},	// PC CD/disk
	{"sounds.rsc",					true,		"95863b89a0916941f6c5e1789843ba14", kSoundPCM,		22050,		false},	// Mac
	{"soundsd.rsc",					false,		"95a6c148e22e99a8c243f2978223583c", kSoundPCM,		22050,		false},	// New PC demos
	{"soundsd.rsc",					true,		"b3a831fbed337d1f1300fee1dd474f6c", kSoundPCM,		22050,		false},	// Mac demos
	// Unsupported (8 bit unsigned sound) - used in the early ITE Win32 demo
	//{"soundsd.rsc",				false,		"a741139dd7365a13f463cd896ff9969a", kSoundPCM,		22050,		false},	// Old Win32 demo
	{"ite sounds.bin",				true,		"441426c6bb2a517f65c7e49b57f7a345", kSoundMacPCM,	22050,		false}, // MacBinary

	{"music.rsc",					false,		"d6454756517f042f01210458abe8edd4", kSoundPCM,		11025,		true},	// PC CD/disk with digital music
	{"music.rsc",					true,		"1a91cd60169f367ecb6c6e058d899b2f", kSoundPCM,		11025,		true},	// Mac
	{"musicd.rsc",					true,		"d6454756517f042f01210458abe8edd4", kSoundPCM,		11025,		true},	// New PC demos
	{"musicd.rsc",					true,		"495bdde51fd9f4bea2b9c911091b1ab2", kSoundPCM,		11025,		false},	// New Mac demo
	{"musicd.rsc",					true,		"1a91cd60169f367ecb6c6e058d899b2f", kSoundPCM,		11025,		true},	// Old Mac demo
	{"ite music.bin",				true,		"441426c6bb2a517f65c7e49b57f7a345", kSoundMacPCM,	22050,		false}, // MacBinary

	{"inherit the earth voices",	true,		"c14c4c995e7a0d3828e3812a494301b7", kSoundPCM,		22050,		false},	// Mac
	{"voices.rsc",					false,		"41bb6b95d792dde5196bdb78740895a6", kSoundPCM,		22050,		false},	// CD
	{"voices.rsc",					false,		"2fbad5d10b9b60a3415dc4aebbb11718", kSoundPCM,		22050,		false},	// German CD
	{"voices.rsc",					false,		"c46e4392fcd2e89bc91e5567db33b62d", kSoundVOC,		-1,			false},	// Disk
	{"voices.rsc",					false,		"0c9113e630f97ef0996b8c3114badb08", kSoundVOC,		-1,			false},	// German disk
	{"voices.rsc",					false,		"c58e67c506af4ffa03fd0aac2079deb0", kSoundVOC,		-1,			false},	// Early DOS demo
	{"voicesd.rsc",					false,		"e139d86bab2ee8ba3157337f894a92d4", kSoundVOX,		22050,		false},	// New PC demos and all Mac demos
	// Unsupported (8 bit unsigned sound) - used in the early ITE Win32 demo
	//{"voicesd.rsc",				false,		"0759eaf5b64ae19fd429920a70151ad3", kSoundPCM,		22050,		false},	// Old Win32 demo
	{"ite voices.bin",				true,		"dba92ae7d57e942250fe135609708369", kSoundMacPCM,	22050,		false}	// MacBinary
	// TODO: Add known Amiga files
};

// Known IHNM files
static CompressSaga::GameFileDescription IHNM_GameFiles[] = {
	//	Common::Filename					swapEndian	md5									resourceType	frequency	stereo
	// FIXME: sfx.res is disabled for now, as there are issues when trying to encode it
	//{"sfx.res",					false,		"1c610d543f32ec8b525e3f652536f269", kSoundWAV,		-1,			false},
	{"voicess.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices1.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices2.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices3.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices4.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices5.res",					false,		"-1",								kSoundWAV,		-1,			false},
	{"voices6.res",					false,		"-1",								kSoundWAV,		-1,			false},
	// Demo
	{"voicesd.res",					false,		"-1",								kSoundWAV,		-1,			false},
};

// --------------------------------------------------------------------------------

static CompressSaga::GameDescription gameDescriptions[] = {
	// Inherit the earth
	{
		GType_ITE,
		ARRAYSIZE(ITE_GameFiles),
		ITE_GameFiles,
	},

	// I Have No Mouth And I Must Scream
	{
		GType_IHNM,
		ARRAYSIZE(IHNM_GameFiles),
		IHNM_GameFiles,
	},

};

typedef struct  {
	uint32 offset;
	uint32 size;
} Record;

// Constructor
CompressSaga::CompressSaga(const std::string &name) : CompressionTool(name, TOOLTYPE_COMPRESSION) {
	_currentGameDescription = NULL;
	_currentFileDescription = NULL;
	_supportsProgressBar = true;

	ToolInput input;
	// We accept many extensions, and the fact that "inherit the earth voices" does not have an extension
	// makes it pretty much unsupported by the standard dialogs to open
	input.format = "*.*";
	_inputPaths.push_back(input);

	_shorthelp = "Used to compress SAGA engine games such as Inherit the Earth and I Have no Mouth.";
	_helptext = "\nUsage: " + getName() +" [mode] [mode params] [-o outputfile = infile.cmp] <inputfile>\n";
}

InspectionMatch CompressSaga::inspectInput(const Common::Filename &filename) {
	if (detectFile(&filename))
		return IMATCH_PERFECT;
	return IMATCH_AWFUL;
}

// --------------------------------------------------------------------------------

bool CompressSaga::detectFile(const Common::Filename *infile) {
	int gamesCount = ARRAYSIZE(gameDescriptions);
	int i, j;
	uint8 md5sum[16];
	char md5str[32+1];

	Common::md5_file(infile->getFullPath().c_str(), md5sum, FILE_MD5_BYTES);
	print("Input file name: %s", infile->getFullPath().c_str());
	for (j = 0; j < 16; j++) {
		sprintf(md5str + j*2, "%02x", (int)md5sum[j]);
	}
	print("md5: %s", md5str);

	for (i = 0; i < gamesCount; i++) {
		for (j = 0; j < gameDescriptions[i].filesCount; j++) {
			if (i == 0) {		// ITE
				// MD5 based detection, needed to distinguish the different file encodings
				// of the ITE sound files
				if (strcmp(gameDescriptions[i].filesDescriptions[j].md5, md5str) == 0) {
					_currentGameDescription = &gameDescriptions[i];
					_currentFileDescription = &_currentGameDescription->filesDescriptions[j];

					print("Matched game: Inherit the Earth: Quest for the Orb");
					return true;
				}
			} else {			// IHNM
				// Common::Filename based detection, used in IHNM, as all its sound files have the
				// same encoding

				if (scumm_stricmp(gameDescriptions[i].filesDescriptions[j].fileName, infile->getFullName().c_str()) == 0) {
					_currentGameDescription = &gameDescriptions[i];
					_currentFileDescription = &_currentGameDescription->filesDescriptions[j];

					print("Matched game: I Have No Mouth, and I Must Scream");
					return true;
				}
			}
		}
	}
	print("Unsupported file");
	return false;
}

uint32 CompressSaga::copyFile(const char *fromFileName, Common::File &outputFile) {
	size_t size;
	char fbuf[2048];
	Common::File tempf(fromFileName, "rb");

	if (!tempf.isOpen())
		error("Unable to open %s", fromFileName);

	while ((size = tempf.read_noThrow(fbuf, sizeof(fbuf))) > 0) {
		outputFile.write(fbuf, size);
	}
	size = tempf.pos();
	return size;
}

void CompressSaga::copyFile(Common::File &inputFile, uint32 inputSize, const char *toFileName) {
	size_t size;
	char fbuf[2048];
	Common::File tempf(toFileName, "wb");

	if (!tempf.isOpen())
		error("Unable to open %s", toFileName);
	while (inputSize > 0) {
		size = inputFile.read_noThrow(fbuf, inputSize > sizeof(fbuf) ? sizeof(fbuf) : inputSize);
		if (size == 0) {
			error("Unable to copy file");
		}
		tempf.write(fbuf, size);
		inputSize -= size;
	}
}

void CompressSaga::writeBufferToFile(uint8 *data, uint32 inputSize, const char *toFileName) {
	Common::File tempf(toFileName, "wb");
	if (!tempf.isOpen())
		error("Unable to open %s", toFileName);
	tempf.write(data, inputSize);
}

byte CompressSaga::compression_format(AudioFormat format) {
	switch(format) {
	case AUDIO_MP3:
		return 0;
	case AUDIO_VORBIS:
		return 1;
	case AUDIO_FLAC:
		return 2;
	case AUDIO_NONE:
	default:
		throw ToolException("Unknown compression format");
	}
}

void CompressSaga::writeHeader(Common::File &outputFile) {
	outputFile.writeByte(compression_format(_format));
	outputFile.writeUint16LE(_sampleRate);
	outputFile.writeUint32LE(_sampleSize);
	outputFile.writeByte(_sampleBits);
	outputFile.writeByte(_sampleStereo);
}

uint32 CompressSaga::encodeEntry(Common::File &inputFile, uint32 inputSize, Common::File &outputFile) {
	uint8 *inputData = 0;
	byte *buffer = 0;
	int rate, size;
	byte flags;

	if (_currentFileDescription->resourceType == kSoundVOC) {
		inputData = Audio::loadVOCFromStream(inputFile, size, rate);

		_sampleSize = size;
		_sampleRate = rate;
		_sampleBits = 8;
		_sampleStereo = 0;
		writeHeader(outputFile);

		writeBufferToFile(inputData, _sampleSize, TEMP_RAW);
		free(inputData);

		setRawAudioType( true, false, 8);
		encodeAudio(TEMP_RAW, true, _sampleRate, tempEncoded, _format);
		return copyFile(tempEncoded, outputFile) + HEADER_SIZE;
	}
	if (_currentFileDescription->resourceType == kSoundPCM) {
		_sampleSize = inputSize;
		_sampleRate = (uint16)_currentFileDescription->frequency;
		_sampleBits = 16;
		_sampleStereo = _currentFileDescription->stereo;
		writeHeader(outputFile);

		copyFile(inputFile, inputSize, TEMP_RAW);

		setRawAudioType( !_currentFileDescription->swapEndian, _sampleStereo != 0, _sampleBits);
		encodeAudio(TEMP_RAW, true, _sampleRate, tempEncoded, _format);
		return copyFile(tempEncoded, outputFile) + HEADER_SIZE;
	}
	if (_currentFileDescription->resourceType == kSoundWAV) {
		if (!Audio::loadWAVFromStream(inputFile, size, rate, flags))
			error("Unable to read WAV");

		_sampleSize = size;
		_sampleRate = rate;
		_sampleBits = ((flags & Audio::Mixer::FLAG_16BITS) != 0) ? 16 : 8;
		_sampleStereo = ((flags & Audio::Mixer::FLAG_STEREO) != 0);
		writeHeader(outputFile);

		copyFile(inputFile, size, TEMP_RAW);

		setRawAudioType( true, _sampleStereo != 0, _sampleBits);
		encodeAudio(TEMP_RAW, true, _sampleRate, tempEncoded, _format);
		return copyFile(tempEncoded, outputFile) + HEADER_SIZE;
	}
	if (_currentFileDescription->resourceType == kSoundVOX) {
		_sampleSize = inputSize * 4;
		_sampleRate = (uint16)_currentFileDescription->frequency;
		_sampleBits = 16;
		_sampleStereo = _currentFileDescription->stereo;
		writeHeader(outputFile);

		Audio::AudioStream *voxStream = Audio::makeADPCMStream(&inputFile, inputSize, Audio::kADPCMOki);
		buffer = (byte *)malloc(_sampleSize);
		uint32 voxSize = voxStream->readBuffer((int16*)buffer, inputSize * 2);
		if (voxSize != inputSize * 2)
			error("Wrong VOX output size");
		writeBufferToFile((uint8 *)buffer, _sampleSize, TEMP_RAW);
		free(buffer);

		setRawAudioType( !_currentFileDescription->swapEndian, _sampleStereo != 0, _sampleBits);
		encodeAudio(TEMP_RAW, true, _sampleRate, tempEncoded, _format);
		return copyFile(tempEncoded, outputFile) + HEADER_SIZE;
	}
	if (_currentFileDescription->resourceType == kSoundMacPCM) {
		error("MacBinary files are not supported yet");
		// TODO
		// Note: MacBinary files are unsigned. With the pending changes to setRawAudioType, there will need
		// to be some changes here
		/*
		copyFile(inputFile, inputSize, TEMP_RAW);
		_sampleSize = inputSize - 36;
		_sampleRate = (uint16)currentFileDescription->frequency;
		// The MAC CD Guild version has 8 bit sound, whereas the other versions have 16 bit sound
		_sampleBits = 8;
		_sampleStereo = currentFileDescription->stereo;
		writeHeader(outputFile);

		setRawAudioType( !currentFileDescription->swapEndian, currentFileDescription->stereo, _sampleBits);
		encodeAudio(TEMP_RAW, true, currentFileDescription->frequency, tempEncoded, gCompMode);
		return copyFile(tempEncoded, outputFile) + HEADER_SIZE;
		*/
	}

	error("Unsupported resourceType %ul\n", _currentFileDescription->resourceType);
	// Never reached
	return 0;
}

void CompressSaga::sagaEncode(Common::Filename *inpath, Common::Filename *outpath) {
	Common::File inputFile;
	Common::File outputFile;
	uint32 inputFileSize;
	uint32 resTableOffset;
	uint32 resTableCount;
	uint32 i;

	Record *inputTable;
	Record *outputTable;

	inputFile.open(*inpath, "rb");
	inputFileSize = inputFile.size();
	print("Filesize: %ul", inputFileSize);
	/*
	 * At the end of the resource file there are 2 values: one points to the
	 * beginning of the resource table the other gives the number of
	 * records in the table
	 */
	inputFile.seek(inputFileSize - RSC_TABLEINFO_SIZE, SEEK_SET);

	if (!_currentFileDescription->swapEndian) {
		resTableOffset = inputFile.readUint32LE();
		resTableCount = inputFile.readUint32LE();
	} else {
		resTableOffset = inputFile.readUint32BE();
		resTableCount = inputFile.readUint32BE();
	}

	print("Table offset: %ul\nnumber of records: %ul", resTableOffset, resTableCount);
	if (resTableOffset != inputFileSize - RSC_TABLEINFO_SIZE - RSC_TABLEENTRY_SIZE * resTableCount) {
		error("Something's wrong with your resource file");
	}

	// Go to beginning of the table
	inputFile.seek(resTableOffset, SEEK_SET);

	inputTable = (Record*)malloc(resTableCount * sizeof(Record));

	// Put offsets of all the records in a table
	for (i = 0; i < resTableCount; i++) {

		if (!_currentFileDescription->swapEndian) {
			inputTable[i].offset = inputFile.readUint32LE();
			inputTable[i].size = inputFile.readUint32LE();
		} else {
			inputTable[i].offset = inputFile.readUint32BE();
			inputTable[i].size = inputFile.readUint32BE();
		}

		print("Record: %ul, offset: %ul, size: %ul", i, inputTable[i].offset, inputTable[i].size);

		if ((inputTable[i].offset > inputFileSize) ||
			(inputTable[i].offset + inputTable[i].size > inputFileSize)) {
			error("The offset points outside the file");
		}

	}
	outputTable = (Record*)malloc(resTableCount * sizeof(Record));

	if (outpath->empty()) {
		*outpath = *inpath;
		outpath->setExtension(".cmp");
	}
	outputFile.open(*outpath, "wb");

	for (i = 0; i < resTableCount; i++) {
		// This is where compression takes place, and where all time is spent
		updateProgress(i, resTableCount);

		inputFile.seek(inputTable[i].offset, SEEK_SET);
		outputTable[i].offset = outputFile.pos();

		if (inputTable[i].size >= 8) {
			outputTable[i].size = encodeEntry(inputFile, inputTable[i].size, outputFile);
		} else {
			outputTable[i].size = inputTable[i].size;	// Empty sound resource
		}
	}
	inputFile.close();

	resTableOffset = outputFile.pos();
	for (i = 0; i < resTableCount; i++) {
		outputFile.writeUint32LE(outputTable[i].offset);
		outputFile.writeUint32LE(outputTable[i].size);
	}
	outputFile.writeUint32LE(resTableOffset);
	outputFile.writeUint32LE(resTableCount);	// Should be the same number of entries

	outputFile.close();

	free(inputTable);
	free(outputTable);

	// Cleanup
	Common::removeFile(TEMP_RAW);
	Common::removeFile(tempEncoded);

	print("Done!");
}

void CompressSaga::execute() {
	Common::Filename inpath(_inputPaths[0].path);
	Common::Filename &outpath = _outputPath;

	if (outpath.directory()) {
		outpath.setFullName(inpath.getName());
		outpath.setExtension(".cmp");
	}

	if (detectFile(&inpath)) {
		sagaEncode(&inpath, &outpath);
	} else {
		error("Failed to compress file %s", inpath.getFullPath().c_str());
	}
}

#ifdef STANDALONE_MAIN
int main(int argc, char *argv[]) {
	CompressSaga saga(argv[0]);
	return saga.run(argc, argv);
}
#endif

