/* 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 "cruise/cruise_main.h"
#include "common/endian.h"
#include "common/memstream.h"
#include "common/textconsole.h"

namespace Cruise {

enum fileTypeEnum {
	type_UNK,
	type_SPL,
	type_SET,
	type_FNT
};

int loadSingleFile;

/**
 * Takes care of decoding a compressed graphic
 */
void decodeGfxUnified(dataFileEntry *pCurrentFileEntry, int16 format) {
	uint8 *dataPtr = pCurrentFileEntry->subData.ptr;
	int spriteSize;

	// Unified how to get spriteSize
	switch (format) {
	case 1:
	case 4:
		spriteSize = pCurrentFileEntry->height * pCurrentFileEntry->width;
		break;
	case 5:
		spriteSize = pCurrentFileEntry->height * pCurrentFileEntry->widthInColumn;
		break;

	default:
		error("Unknown gfx format %d", format);
	}

	uint8 *buffer = (uint8 *)MemAlloc(spriteSize);

	// Perform format specific decoding
	switch (format) {
	case 1:
	case 4: {
		int x = 0;
		while (x < spriteSize) {
			uint8 c;
			uint16 p0;
			// Format 4
			uint16 p1 = 0, p2 = 0, p3 = 0;

			p0 = (dataPtr[0] << 8) | dataPtr[1];

			// Format 4
			if (format == 4) {
				p1 = (dataPtr[2] << 8) | dataPtr[3];
				p2 = (dataPtr[4] << 8) | dataPtr[5];
				p3 = (dataPtr[6] << 8) | dataPtr[7];
			}

			/* decode planes */
			for (c = 0; c < 16; c++) {
				// Format 4
				if (format == 4) {
					buffer[x + c] = ((p0 >> 15) & 1) | ((p1 >> 14) & 2) | ((p2 >> 13) & 4) | ((p3 >> 12) & 8);
				} else {
					buffer[x + c] = ((p0 >> 15) & 1);
				}

				p0 <<= 1;

				// Format 4
				if (format == 4) {
					p1 <<= 1;
					p2 <<= 1;
					p3 <<= 1;
				}
			}

			x += 16;

			dataPtr += (2 * format);
		}

		break;
	}
	case 5: {
		uint8 *destP = buffer;
		int range = pCurrentFileEntry->height * pCurrentFileEntry->width;

		for (int line = 0; line < pCurrentFileEntry->height; line++) {
			uint8 p0, p1, p2, p3, p4;

			for (int x = 0; x < pCurrentFileEntry->widthInColumn; x++) {
				int bit = 7 - (x % 8);
				int col = x / 8;

				p0 = (dataPtr[line*pCurrentFileEntry->width + col + range * 0] >> bit) & 1;
				p1 = (dataPtr[line*pCurrentFileEntry->width + col + range * 1] >> bit) & 1;
				p2 = (dataPtr[line*pCurrentFileEntry->width + col + range * 2] >> bit) & 1;
				p3 = (dataPtr[line*pCurrentFileEntry->width + col + range * 3] >> bit) & 1;
				p4 = (dataPtr[line*pCurrentFileEntry->width + col + range * 4] >> bit) & 1;

				*destP++ = p0 | (p1 << 1) | (p2 << 2) | (p3 << 3) | (p4 << 4);
			}
		}

		break;
	}
	default:
		break;
	}

	MemFree(pCurrentFileEntry->subData.ptr);
	pCurrentFileEntry->subData.ptr = buffer;
}

int updateResFileEntry(int height, int width, int size, int entryNumber, int resType) {
	int div = 0;

	resetFileEntry(entryNumber);

	filesDatabase[entryNumber].subData.compression = 0;

	int maskSize = size;

	if (resType == 4) {
		div = maskSize / 4;
	} else if (resType == 5) {
		width = (width * 8) / 5;
		maskSize = MAX(size, height * width);
	}

	filesDatabase[entryNumber].subData.ptr = (uint8 *)mallocAndZero(maskSize + div);

	if (!filesDatabase[entryNumber].subData.ptr)
		return (-2);

	filesDatabase[entryNumber].widthInColumn = width;
	filesDatabase[entryNumber].subData.ptrMask = (uint8 *) mallocAndZero(maskSize);
	filesDatabase[entryNumber].width = width / 8;
	filesDatabase[entryNumber].resType = resType;
	filesDatabase[entryNumber].height = height;
	filesDatabase[entryNumber].subData.index = -1;

	return entryNumber;
}

int createResFileEntry(int width, int height, int size, int resType) {
	error("Executing untested createResFileEntry");
	return 0;	// for compilers that don't support NORETURN

#if 0
	int entryNumber;
	int div = 0;

	int i = 0;
	for (; i < NUM_FILE_ENTRIES; i++) {
		if (!filesDatabase[i].subData.ptr)
			break;
	}

	if (i >= NUM_FILE_ENTRIES) {
		return (-19);
	}

	entryNumber = i;

	filesDatabase[entryNumber].subData.compression = 0;

	if (resType == 4) {
		div = size / 4;
	} else if (resType == 5) {
		width = (width * 8) / 5;
	}

	filesDatabase[entryNumber].subData.ptr = (uint8 *) mallocAndZero(size + div);

	if (!filesDatabase[entryNumber].subData.ptr) {
		return (-2);
	}

	filesDatabase[entryNumber].widthInColumn = width;
	filesDatabase[entryNumber].subData.ptrMask = filesDatabase[entryNumber].subData.ptr + size;
	filesDatabase[entryNumber].width = width / 8;
	filesDatabase[entryNumber].resType = resType;
	filesDatabase[entryNumber].height = height;
	filesDatabase[entryNumber].subData.index = -1;

	return entryNumber;
#endif
}

fileTypeEnum getFileType(const char *name) {
	char extentionBuffer[16];

	fileTypeEnum newFileType = type_UNK;

	getFileExtention(name, extentionBuffer);

	if (!strcmp(extentionBuffer, ".SPL")) {
		newFileType = type_SPL;
	} else if (!strcmp(extentionBuffer, ".SET")) {
		newFileType = type_SET;
	} else if (!strcmp(extentionBuffer, ".FNT")) {
		newFileType = type_FNT;
	}

	assert(newFileType != type_UNK);

	return newFileType;
}

int getNumMaxEntiresInSet(uint8 *ptr) {
	uint16 numEntries = READ_BE_UINT16(ptr + 4);
	return numEntries;
}

int loadFile(const char* name, int idx, int destIdx) {
	uint8 *ptr = nullptr;
	fileTypeEnum fileType;

	fileType = getFileType(name);

	loadFileSub1(&ptr, name, nullptr);

	switch (fileType) {
	case type_SET: {

		int numMaxEntriesInSet = getNumMaxEntiresInSet(ptr);

		if (destIdx > numMaxEntriesInSet) {
			MemFree(ptr);
			return 0;	// exit if limit is reached
		}
		int res = loadSetEntry(name, ptr, destIdx, idx);
		MemFree(ptr);

		return res;
	}
	case type_FNT: {
		int res = loadFNTSub(ptr, idx);
		MemFree(ptr);

		return res;
	}
	case type_SPL: {
		// Sound file
		loadSPLSub(ptr, idx);
		break;
	}
	default:
		error("Unknown fileType in loadFile");
	}

	MemFree(ptr);

	return -1;
}

int loadFileRange(const char *name, int startIdx, int currentEntryIdx, int numIdx) {
	uint8 *ptr = nullptr;
	fileTypeEnum fileType;

	fileType = getFileType(name);

	loadFileSub1(&ptr, name, nullptr);

	switch (fileType) {
	case type_SET: {
		int numMaxEntriesInSet = getNumMaxEntiresInSet(ptr);

		for (int i = 0; i < numIdx; i++) {
			if ((startIdx + i) > numMaxEntriesInSet) {
				MemFree(ptr);
				return 0;	// exit if limit is reached
			}
			loadSetEntry(name, ptr, startIdx + i, currentEntryIdx + i);
		}

		break;
	}
	case type_FNT: {
		loadFNTSub(ptr, startIdx);
		break;
	}
	case type_SPL: {
		// Sound file
		loadSPLSub(ptr, startIdx);
		break;
	}
	default:
		error("Unknown fileType in loadFileRange");
	}

	MemFree(ptr);

	return 0;
}

int loadFullBundle(const char *name, int startIdx) {
	uint8 *ptr = nullptr;
	fileTypeEnum fileType;

	fileType = getFileType(name);

	loadFileSub1(&ptr, name, nullptr);

	if (ptr == nullptr)
		return 0;

	switch (fileType) {
	case type_SET: {
		// Sprite set
		int numMaxEntriesInSet = getNumMaxEntiresInSet(ptr);	// get maximum number of sprites/animations in SET file

		for (int i = 0; i < numMaxEntriesInSet; i++) {
			loadSetEntry(name, ptr, i, startIdx + i);
		}

		break;
	}
	case type_FNT: {
		// Font file
		loadFNTSub(ptr, startIdx);
		break;
	}
	case type_SPL: {
		// Sound file
		loadSPLSub(ptr, startIdx);
		break;
	}
	default:
		error("Unknown fileType in loadFullBundle");
	}

	MemFree(ptr);

	return 0;
}

int loadFNTSub(uint8 *ptr, int destIdx) {
	uint8 *ptr2 = ptr + 4;
	loadFileVar1 = READ_BE_UINT32(ptr2);

	int fileIndex;
	if (destIdx == -1)
		fileIndex = createResFileEntry(loadFileVar1, 1, loadFileVar1, 1);
	else
		fileIndex = updateResFileEntry(loadFileVar1, 1, loadFileVar1, destIdx, 1);

	if (fileIndex < 0)
		error("Unable to load FNT resource");

	uint8 *destPtr = filesDatabase[fileIndex].subData.ptr;

	if (destPtr != nullptr) {
		memcpy(destPtr, ptr2, loadFileVar1);

		destPtr = filesDatabase[fileIndex].subData.ptr;

		bigEndianLongToNative((int32 *) destPtr);
		bigEndianLongToNative((int32 *)(destPtr + 4));
		flipGen(destPtr + 8, 6);

		uint8 *currentPtr = destPtr + 14;

		for (int i = 0; i < (int16)READ_UINT16(destPtr + 8); i++) {
			bigEndianLongToNative((int32 *) currentPtr);
			currentPtr += 4;

			flipGen(currentPtr, 8);
			currentPtr += 8;
		}
	}

	return 1;
}

int loadSPLSub(uint8 *ptr, int destIdx) {
	int fileIndex;

	if (destIdx == -1)
		fileIndex = createResFileEntry(loadFileVar1, 1, loadFileVar1, 1);
	else
		fileIndex = updateResFileEntry(loadFileVar1, 1, loadFileVar1, destIdx, 1);

	if (fileIndex < 0)
		error("Unable to load SPL resource");

	uint8* destPtr = filesDatabase[fileIndex].subData.ptr;
	memcpy(destPtr, ptr, loadFileVar1);

	return 1;
}


int loadSetEntry(const char *name, uint8 *ptr, int currentEntryIdx, int currentDestEntry) {
	uint8 *ptr3;
	int offset;
	int sec = 0;
	uint16 numIdx;

	if (!strcmp((char *)ptr, "SEC"))
		sec = 1;

	numIdx = READ_BE_UINT16(ptr + 4);
	ptr3 = ptr + 6;
	offset = currentEntryIdx * 16;

	int resourceSize;
	int fileIndex;
	setHeaderEntry localBuffer;

	Common::MemoryReadStream s4(ptr + offset + 6, 16);

	localBuffer.offset = s4.readUint32BE();
	localBuffer.width = s4.readUint16BE();
	localBuffer.height = s4.readUint16BE();
	localBuffer.type = s4.readUint16BE();
	localBuffer.transparency = s4.readUint16BE() & 0x1F;
	localBuffer.hotspotY = s4.readUint16BE();
	localBuffer.hotspotX = s4.readUint16BE();

	if (sec == 1)
		// Type 1: Width - (1*2) , Type 5: Width - (5*2)
		localBuffer.width -= localBuffer.type * 2;

	resourceSize = localBuffer.width * localBuffer.height;

	if (!sec && (localBuffer.type == 5))
		// Type 5: Width - (2*5)
		localBuffer.width -= 10;

	if (currentDestEntry == -1)
		fileIndex = createResFileEntry(localBuffer.width, localBuffer.height, resourceSize, localBuffer.type);
	else
		fileIndex = updateResFileEntry(localBuffer.height, localBuffer.width, resourceSize, currentDestEntry, localBuffer.type);

	if (fileIndex < 0)
		return -1;	// TODO: buffer is not freed

	if (!sec && (localBuffer.type == 5)) {
		// There are sometimes sprites with a reduced width than what their pixels provide.
		// The original handled this here by copy parts of each line - for ScummVM, we're
		// simply setting the width in bytes and letting the decoder do the rest
		filesDatabase[fileIndex].width += 2;
	}

	uint8 *ptr5 = ptr3 + localBuffer.offset + numIdx * 16;
	memcpy(filesDatabase[fileIndex].subData.ptr, ptr5, resourceSize);

	switch (localBuffer.type) {
		case 0:   // polygon
			filesDatabase[fileIndex].subData.resourceType = OBJ_TYPE_POLY;
			filesDatabase[fileIndex].subData.index = currentEntryIdx;
			break;

		case 1:
			filesDatabase[fileIndex].width = filesDatabase[fileIndex].widthInColumn * 8;
			filesDatabase[fileIndex].subData.resourceType = OBJ_TYPE_BGMASK;
			decodeGfxUnified(&filesDatabase[fileIndex], localBuffer.type);
			filesDatabase[fileIndex].subData.index = currentEntryIdx;
			filesDatabase[fileIndex].subData.transparency = 0;
			break;

		case 4:
			filesDatabase[fileIndex].width = filesDatabase[fileIndex].widthInColumn * 2;
			filesDatabase[fileIndex].subData.resourceType = OBJ_TYPE_SPRITE;
			decodeGfxUnified(&filesDatabase[fileIndex], localBuffer.type);
			filesDatabase[fileIndex].subData.index = currentEntryIdx;
			filesDatabase[fileIndex].subData.transparency = localBuffer.transparency % 0x10;
			break;

		case 5:
			filesDatabase[fileIndex].subData.resourceType = OBJ_TYPE_SPRITE;
			decodeGfxUnified(&filesDatabase[fileIndex], localBuffer.type);
			filesDatabase[fileIndex].width = filesDatabase[fileIndex].widthInColumn;
			filesDatabase[fileIndex].subData.index = currentEntryIdx;
			filesDatabase[fileIndex].subData.transparency = localBuffer.transparency;
			break;

		case 8:
			filesDatabase[fileIndex].subData.resourceType = OBJ_TYPE_SPRITE;
			filesDatabase[fileIndex].width = filesDatabase[fileIndex].widthInColumn;
			filesDatabase[fileIndex].subData.index = currentEntryIdx;
			filesDatabase[fileIndex].subData.transparency = localBuffer.transparency;
			break;

		default:
			warning("Unsupported gfx loading type: %d", localBuffer.type);
			break;
	}

	if (name != filesDatabase[fileIndex].subData.name)
		Common::strlcpy(filesDatabase[fileIndex].subData.name, name, sizeof(filesDatabase[fileIndex].subData.name));

	// create the mask
	switch (localBuffer.type) {
		case 1:
		case 4:
		case 5:
		case 8:
			memset(filesDatabase[fileIndex].subData.ptrMask, 0, filesDatabase[fileIndex].width / 8 * filesDatabase[fileIndex].height);

			for (int maskY = 0; maskY < filesDatabase[fileIndex].height; maskY++) {
				for (int maskX = 0; maskX < filesDatabase[fileIndex].width; maskX++) {
					if (*(filesDatabase[fileIndex].subData.ptr + filesDatabase[fileIndex].width * maskY + maskX) != filesDatabase[fileIndex].subData.transparency) {
						*(filesDatabase[fileIndex].subData.ptrMask + filesDatabase[fileIndex].width / 8 * maskY + maskX / 8) |= 0x80 >> (maskX & 7);
					}
				}
			}
			break;

		default:
			break;
	}

	// TODO: free

	return 1;
}

} // End of namespace Cruise
