/* 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 "glk/level9/detection.h"
#include "glk/level9/detection_tables.h"
#include "glk/level9/level9_main.h"
#include "glk/level9/os_glk.h"
#include "glk/blorb.h"
#include "glk/detection.h"
#include "common/debug.h"
#include "common/file.h"
#include "common/md5.h"
#include "engines/game.h"

namespace Glk {
namespace Level9 {

long Scanner::scanner(byte *startFile, uint32 size, byte **dictData, byte **aCodePtr) {
	_dictData = dictData;
	_aCodePtr = aCodePtr;

#ifdef FULLSCAN
	FullScan(startfile, FileSize);
#endif

	int offset = scan(startFile, size);
	if (offset < 0) {
		offset = ScanV2(startFile, size);
		_gameType = L9_V2;
		if (offset < 0) {
			offset = ScanV1(startFile, size);
			_gameType = L9_V1;
			if (offset < 0) {
				return -1;
			}
		}
	}

	return offset;
}

const L9V1GameInfo &Scanner::v1Game() const {
	assert(_gameType == L9_V1);
	return L9_V1_GAMES[_l9V1Game];
}

long Scanner::scan(byte *startFile, uint32 size) {
	uint32 i, num, Size, MaxSize = 0;
	int j;
	uint16 d0 = 0, l9, md, ml, dd, dl;
	uint32 Min, Max;
	long offset = -1;
	bool JumpKill, DriverV4;

	if (size < 33)
		return -1;

	byte *Chk = (byte *)malloc(size + 1);
	byte *Image = (byte *)calloc(size, 1);

	if ((Chk == nullptr) || (Image == nullptr)) {
		error("Unable to allocate memory for game scan! Exiting...");
	}

	Chk[0] = 0;
	for (i = 1; i <= size; i++)
		Chk[i] = Chk[i - 1] + startFile[i - 1];

	for (i = 0; i < size - 33; i++) {
		num = L9WORD(startFile + i) + 1;
		/*
		        Chk[i] = 0 +...+ i-1
		        Chk[i+n] = 0 +...+ i+n-1
		        Chk[i+n] - Chk[i] = i + ... + i+n
		*/
		if (num > 0x2000 && i + num <= size && Chk[i + num] == Chk[i]) {
			md = L9WORD(startFile + i + 0x2);
			ml = L9WORD(startFile + i + 0x4);
			dd = L9WORD(startFile + i + 0xa);
			dl = L9WORD(startFile + i + 0xc);

			if (ml > 0 && md > 0 && i + md + ml <= size && dd > 0 && dl > 0 && i + dd + dl * 4 <= size) {
				/* v4 files may have acodeptr in 8000-9000, need to fix */
				for (j = 0; j < 12; j++) {
					d0 = L9WORD(startFile + i + 0x12 + j * 2);
					if (j != 11 && d0 >= 0x8000 && d0 < 0x9000) {
						if (d0 >= 0x8000 + LISTAREASIZE) break;
					} else if (i + d0 > size) break;
				}
				/* list9 ptr must be in listarea, acode ptr in data */
				if (j < 12 /*|| (d0>=0x8000 && d0<0x9000)*/) continue;

				l9 = L9WORD(startFile + i + 0x12 + 10 * 2);
				if (l9 < 0x8000 || l9 >= 0x8000 + LISTAREASIZE) continue;

				Size = 0;
				Min = Max = i + d0;
				DriverV4 = 0;
				if (ValidateSequence(startFile, Image, i + d0, i + d0, &Size, size, &Min, &Max, false, &JumpKill, &DriverV4)) {
					if (Size > MaxSize && Size > 100) {
						offset = i;
						MaxSize = Size;
						_gameType = DriverV4 ? L9_V4 : L9_V3;
					}
				}
			}
		}
	}

	free(Chk);
	free(Image);
	return offset;
}

long Scanner::ScanV2(byte *startFile, uint32 size) {
	uint32 i, Size, MaxSize = 0, num;
	int j;
	uint16 d0 = 0, l9;
	uint32 Min, Max;
	long offset = -1;
	bool JumpKill;

	if (size < 28)
		return -1;

	byte *Chk = (byte *)malloc(size + 1);
	byte *Image = (byte *)calloc(size, 1);

	if ((Chk == nullptr) || (Image == nullptr)) {
		error("Unable to allocate memory for game scan! Exiting...");
	}

	Chk[0] = 0;
	for (i = 1; i <= size; i++)
		Chk[i] = Chk[i - 1] + startFile[i - 1];

	for (i = 0; i < size - 28; i++) {
		num = L9WORD(startFile + i + 28) + 1;
		if ((i + num) <= size && i < (size - 32) && ((Chk[i + num] - Chk[i + 32]) & 0xff) == startFile[i + 0x1e]) {
			for (j = 0; j < 14; j++) {
				d0 = L9WORD(startFile + i + j * 2);
				if (j != 13 && d0 >= 0x8000 && d0 < 0x9000) {
					if (d0 >= 0x8000 + LISTAREASIZE) break;
				} else if (i + d0 > size) break;
			}
			/* list9 ptr must be in listarea, acode ptr in data */
			if (j < 14 /*|| (d0>=0x8000 && d0<0x9000)*/) continue;

			l9 = L9WORD(startFile + i + 6 + 9 * 2);
			if (l9 < 0x8000 || l9 >= 0x8000 + LISTAREASIZE) continue;

			Size = 0;
			Min = Max = i + d0;
			if (ValidateSequence(startFile, Image, i + d0, i + d0, &Size, size, &Min, &Max, false, &JumpKill, nullptr)) {
#ifdef L9DEBUG
				printf("Found valid V2 header at %ld, code size %ld", i, Size);
#endif
				if (Size > MaxSize && Size > 100) {
					offset = i;
					MaxSize = Size;
				}
			}
		}
	}
	free(Chk);
	free(Image);
	return offset;
}

long Scanner::ScanV1(byte *startFile, uint32 size) {
	uint32 i, Size;
	int Replace;
	byte *ImagePtr;
	long MaxPos = -1;
	uint32 MaxCount = 0;
	uint32 Min, Max; //, MaxMax, MaxMin;
	bool JumpKill; // , MaxJK;

	int dictOff1 = 0, dictOff2 = 0;
	byte dictVal1 = 0xff, dictVal2 = 0xff;

	if (size < 20)
		return -1;

	byte *Image = (byte *)calloc(size, 1);
	if (Image == nullptr) {
		error("Unable to allocate memory for game scan! Exiting...");
	}

	for (i = 0; i < size; i++) {
		if ((startFile[i] == 0 && startFile[i + 1] == 6) || (startFile[i] == 32 && startFile[i + 1] == 4)) {
			Size = 0;
			Min = Max = i;
			Replace = 0;
			if (ValidateSequence(startFile, Image, i, i, &Size, size, &Min, &Max, false, &JumpKill, nullptr)) {
				if (Size > MaxCount && Size > 100 && Size < 10000) {
					MaxCount = Size;
					//MaxMin = Min;
					//MaxMax = Max;

					MaxPos = i;
					//MaxJK = JumpKill;
				}
				Replace = 0;
			}
			for (ImagePtr = Image + Min; ImagePtr <= Image + Max; ImagePtr++) {
				if (*ImagePtr == 2)
					*ImagePtr = Replace;
			}
		}
	}

	/* V1 dictionary detection from L9Cut by Paul David Doherty */
	for (i = 0; i < size - 20; i++) {
		if (startFile[i] == 'A') {
			if (startFile[i + 1] == 'T' && startFile[i + 2] == 'T' && startFile[i + 3] == 'A' && startFile[i + 4] == 'C' && startFile[i + 5] == 0xcb) {
				dictOff1 = i;
				dictVal1 = startFile[dictOff1 + 6];
				break;
			}
		}
	}
	for (i = dictOff1; i < size - 20; i++) {
		if (startFile[i] == 'B') {
			if (startFile[i + 1] == 'U' && startFile[i + 2] == 'N' && startFile[i + 3] == 'C' && startFile[i + 4] == 0xc8) {
				dictOff2 = i;
				dictVal2 = startFile[dictOff2 + 5];
				break;
			}
		}
	}
	_l9V1Game = -1;
	if (dictVal1 != 0xff || dictVal2 != 0xff) {
		for (i = 0; i < sizeof L9_V1_GAMES / sizeof L9_V1_GAMES[0]; i++) {
			if ((L9_V1_GAMES[i].dictVal1 == dictVal1) && (L9_V1_GAMES[i].dictVal2 == dictVal2)) {
				_l9V1Game = i;
				if (_dictData)
					(*_dictData) = startFile + dictOff1 - L9_V1_GAMES[i].dictStart;
			}
		}
	}

	free(Image);

	if (MaxPos > 0) {
		if (_aCodePtr)
			(*_aCodePtr) = startFile + MaxPos;
		return 0;
	}

	return -1;
}

bool Scanner::ValidateSequence(byte *Base, byte *Image, uint32 iPos, uint32 acode, uint32 *Size, uint32 size, uint32 *Min, uint32 *Max, bool Rts, bool *JumpKill, bool *DriverV4) {
	uint32 Pos;
	bool Finished = false, Valid;
	uint32 Strange = 0;
	int ScanCodeMask;
	int Code;
	*JumpKill = false;

	if (iPos >= size)
		return false;
	Pos = iPos;
	if (Pos < *Min) *Min = Pos;

	if (Image[Pos]) return true; /* hit valid code */

	do {
		Code = Base[Pos];
		Valid = true;
		if (Image[Pos]) break; /* converged to found code */
		Image[Pos++] = 2;
		if (Pos > *Max) *Max = Pos;

		ScanCodeMask = 0x9f;
		if (Code & 0x80) {
			ScanCodeMask = 0xff;
			if ((Code & 0x1f) > 0xa)
				Valid = false;
			Pos += 2;
		}
		else switch (Code & 0x1f) {
		case 0: { /* goto */
			uint32 Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask);
			Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, true/*Rts*/, JumpKill, DriverV4);
			Finished = true;
			break;
		}
		case 1: { /* intgosub */
			uint32 Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask);
			Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, true, JumpKill, DriverV4);
			break;
		}
		case 2: /* intreturn */
			Valid = Rts;
			Finished = true;
			break;
		case 3: /* printnumber */
			Pos++;
			break;
		case 4: /* messagev */
			Pos++;
			break;
		case 5: /* messagec */
			scangetcon(Code, &Pos, &ScanCodeMask);
			break;
		case 6: /* function */
			switch (Base[Pos++]) {
			case 2:/* random */
				Pos++;
				break;
			case 1:/* calldriver */
				if (DriverV4) {
					if (CheckCallDriverV4(Base, Pos - 2))
						*DriverV4 = true;
				}
				break;
			case 3:/* save */
			case 4:/* restore */
			case 5:/* clearworkspace */
			case 6:/* clear stack */
				break;
			case 250: /* printstr */
				while (Base[Pos++]);
				break;

			default:
				Valid = false;
				break;
			}
			break;
		case 7: /* input */
			Pos += 4;
			break;
		case 8: /* varcon */
			scangetcon(Code, &Pos, &ScanCodeMask);
			Pos++;
			break;
		case 9: /* varvar */
			Pos += 2;
			break;
		case 10: /* _add */
			Pos += 2;
			break;
		case 11: /* _sub */
			Pos += 2;
			break;
		case 14: /* jump */
			*JumpKill = true;
			Finished = true;
			break;
		case 15: /* exit */
			Pos += 4;
			break;
		case 16: /* ifeqvt */
		case 17: /* ifnevt */
		case 18: /* ifltvt */
		case 19: { /* ifgtvt */
			uint32 Val;
			Pos += 2;
			Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask);
			Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, Rts, JumpKill, DriverV4);
			break;
		}
		case 20: /* screen */
			if (Base[Pos++]) Pos++;
			break;
		case 21: /* cleartg */
			Pos++;
			break;
		case 22: /* picture */
			Pos++;
			break;
		case 23: /* getnextobject */
			Pos += 6;
			break;
		case 24: /* ifeqct */
		case 25: /* ifnect */
		case 26: /* ifltct */
		case 27: { /* ifgtct */
			uint32 Val;
			Pos++;
			scangetcon(Code, &Pos, &ScanCodeMask);
			Val = scangetaddr(Code, Base, &Pos, acode, &ScanCodeMask);
			Valid = ValidateSequence(Base, Image, Val, acode, Size, size, Min, Max, Rts, JumpKill, DriverV4);
			break;
		}
		case 28: /* printinput */
			break;
		case 12: /* ilins */
		case 13: /* ilins */
		case 29: /* ilins */
		case 30: /* ilins */
		case 31: /* ilins */
			Valid = false;
			break;
		}
		if (Valid && (Code & ~ScanCodeMask))
			Strange++;
	} while (Valid && !Finished && Pos < size); /* && Strange==0); */
	(*Size) += Pos - iPos;
	(void)Strange;
	return Valid; /* && Strange==0; */
}

uint16 Scanner::scanmovewa5d0(byte *Base, uint32 *Pos) {
	uint16 ret = L9WORD(Base + *Pos);
	(*Pos) += 2;
	return ret;
}

uint32 Scanner::scangetaddr(int Code, byte *Base, uint32 *Pos, uint32 acode, int *Mask) {
	(*Mask) |= 0x20;
	if (Code & 0x20) {
		/* getaddrshort */
		signed char diff = Base[*Pos];
		(*Pos)++;
		return (*Pos) + diff - 1;
	} else {
		return acode + scanmovewa5d0(Base, Pos);
	}
}

void Scanner::scangetcon(int Code, uint32 *Pos, int *Mask) {
	(*Pos)++;
	if (!(Code & 64))(*Pos)++;
	(*Mask) |= 0x40;
}

bool Scanner::CheckCallDriverV4(byte *Base, uint32 Pos) {
	int i, j;

	// Look back for an assignment from a variable to list9[0], which is used
	// to specify the driver call.
	for (i = 0; i < 2; i++) {
		int x = Pos - ((i + 1) * 3);
		if ((Base[x] == 0x89) && (Base[x + 1] == 0x00)) {
			// Get the variable being copied to list9[0]
			int var = Base[x + 2];

			// Look back for an assignment to the variable
			for (j = 0; j < 2; j++) {
				int y = x - ((j + 1) * 3);
				if ((Base[y] == 0x48) && (Base[y + 2] == var)) {
					// If this a V4 driver call?
					switch (Base[y + 1]) {
					case 0x0E:
					case 0x20:
					case 0x22:
						return TRUE;
					}
					return FALSE;
				}
			}
		}
	}
	return FALSE;
}

#ifdef FULLSCAN
void Scanner::fullScan(byte *startFile, uint32 size) {
	byte *Image = (byte *)calloc(size, 1);
	uint32 i, Size;
	int Replace;
	byte *ImagePtr;
	uint32 MaxPos = 0;
	uint32 MaxCount = 0;
	uint32 Min, Max, MaxMin, MaxMax;
	int offset;
	bool JumpKill, MaxJK;
	for (i = 0; i < size; i++) {
		Size = 0;
		Min = Max = i;
		Replace = 0;
		if (ValidateSequence(startFile, Image, i, i, &Size, size, &Min, &Max, FALSE, &JumpKill, nullptr)) {
			if (Size > MaxCount) {
				MaxCount = Size;
				MaxMin = Min;
				MaxMax = Max;

				MaxPos = i;
				MaxJK = JumpKill;
			}
			Replace = 0;
		}
		for (ImagePtr = Image + Min; ImagePtr <= Image + Max; ImagePtr++) {
			if (*ImagePtr == 2)
				*ImagePtr = Replace;
		}
	}
	printf("%ld %ld %ld %ld %s", MaxPos, MaxCount, MaxMin, MaxMax, MaxJK ? "jmp killed" : "");
	/* search for reference to MaxPos */
	offset = 0x12 + 11 * 2;
	for (i = 0; i < size - offset - 1; i++) {
		if ((L9WORD(startFile + i + offset)) + i == MaxPos) {
			printf("possible v3,4 Code reference at : %ld", i);
			/* startdata=startFile+i; */
		}
	}
	offset = 13 * 2;
	for (i = 0; i < size - offset - 1; i++) {
		if ((L9WORD(startFile + i + offset)) + i == MaxPos)
			printf("possible v2 Code reference at : %ld", i);
	}
	free(Image);
}
#endif

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

GameDetection::GameDetection(byte *&startData, uint32 &fileSize) :
		_startData(startData), _fileSize(fileSize), _crcInitialized(false), _gameName(nullptr) {
	Common::fill(&_crcTable[0], &_crcTable[256], 0);
}

gln_game_tableref_t GameDetection::gln_gameid_identify_game() {
	uint16 length, crc;
	byte checksum;
	int is_version2;
	gln_game_tableref_t game;
	gln_patch_tableref_t patch;

	/* Handle v1 games */
	if (_l9V1Game >= 0) {
		assert((unsigned)_l9V1Game < sizeof (GLN_V1GAME_TABLE) / sizeof (GLN_V1GAME_TABLE[0]));
		return &GLN_V1GAME_TABLE[_l9V1Game];
	}

	/* If the data file appears too short for a header, give up now. */
	if (_fileSize < 30)
		return nullptr;

	/*
	 * Find the version of the game, and the length of game data.  This logic
	 * is taken from L9cut, with calcword() replaced by simple byte comparisons.
	 * If the length exceeds the available data, fail.
	 * The alternative conditions are exceptional with Colossal Adventure v2 for MSX.
	 */
	assert(_startData);
	is_version2 = ((_startData[4] == 0x20 && _startData[5] == 0x00)
	                || (_startData[6] == 0x20 && _startData[7] == 0x00))
		&& ((_startData[10] == 0x00 && _startData[11] == 0x80)
		     || (_startData[8] == 0x00 && _startData[9] == 0x80))
		&& _startData[20] == _startData[22]
		&& _startData[21] == _startData[23];

	length = is_version2
		? _startData[28] | _startData[29] << BITS_PER_BYTE
		: _startData[0] | _startData[1] << BITS_PER_BYTE;
	if (length >= _fileSize)
		return nullptr;

	/* Calculate or retrieve the checksum, in a version specific way. */
	if (is_version2) {
		int index;

		checksum = 0;
		for (index = 0; index < length + 1; index++)
			checksum += _startData[index];
	}
	else
		checksum = _startData[length];

	/*
	 * Generate a CRC for this data.  When L9cut calculates a CRC, it's using a
	 * copy taken up to length + 1 and then padded with two NUL bytes, so we
	 * mimic that here.
	 */
	crc = gln_get_buffer_crc(_startData, length + 1, 2);

	/*
	 * See if this is a patched file.  If it is, look up the game based on the
	 * original CRC and checksum.  If not, use the current CRC and checksum.
	 */
	patch = gln_gameid_lookup_patch(length, checksum, crc);
	game = gln_gameid_lookup_game(length,
		patch ? patch->orig_checksum : checksum,
		patch ? patch->orig_crc : crc,
		false);

	/* If no game identified, retry without the CRC.  This is guesswork. */
	if (!game)
		game = gln_gameid_lookup_game(length, checksum, crc, true);

	if (!game) {
		if (is_version2)
			game = &GLN_UNGAME_TABLE[1];
		else if (length >= 0x8500)
			game = &GLN_UNGAME_TABLE[3];
		else
			game = &GLN_UNGAME_TABLE[2];
        }

	return game;
}

// CRC table initialization polynomial
static const uint16 GLN_CRC_POLYNOMIAL = 0xa001;

uint16 GameDetection::gln_get_buffer_crc(const void *void_buffer, size_t length, size_t padding) {
	const char *buffer = (const char *)void_buffer;
	uint16 crc;
	size_t index;

	/* Build the static CRC lookup table on first call. */
	if (!_crcInitialized) {
		for (index = 0; index < BYTE_MAX + 1; index++) {
			int bit;

			crc = (uint16)index;
			for (bit = 0; bit < BITS_PER_BYTE; bit++)
				crc = crc & 1 ? GLN_CRC_POLYNOMIAL ^ (crc >> 1) : crc >> 1;

			_crcTable[index] = crc;
		}

		_crcInitialized = true;

		/* CRC lookup table self-test, after is_initialized set -- recursion. */
		assert(gln_get_buffer_crc("123456789", 9, 0) == 0xbb3d);
	}

	/* Start with zero in the crc, then update using table entries. */
	crc = 0;
	for (index = 0; index < length; index++)
		crc = _crcTable[(crc ^ buffer[index]) & BYTE_MAX] ^ (crc >> BITS_PER_BYTE);

	/* Add in any requested NUL padding bytes. */
	for (index = 0; index < padding; index++)
		crc = _crcTable[crc & BYTE_MAX] ^ (crc >> BITS_PER_BYTE);

	return crc;
}

gln_game_tableref_t GameDetection::gln_gameid_lookup_game(uint16 length, byte checksum, uint16 crc, int ignore_crc) const {
	gln_game_tableref_t game;

	for (game = GLN_GAME_TABLE; game->length; game++) {
		if (game->length == length && game->checksum == checksum
			&& (ignore_crc || game->crc == crc))
			break;
	}

	return game->length ? game : nullptr;
}

gln_patch_tableref_t GameDetection::gln_gameid_lookup_patch(uint16 length, byte checksum, uint16 crc) const {
	gln_patch_tableref_t patch;

	for (patch = GLN_PATCH_TABLE; patch->length; patch++) {
		if (patch->length == length && patch->patch_checksum == checksum
			&& patch->patch_crc == crc)
			break;
	}

	return patch->length ? patch : nullptr;
}

const char *GameDetection::gln_gameid_get_game_name() {
	/*
	 * If no game name yet known, attempt to identify the game.  If it can't
	 * be identified, set the cached game name to "" -- this special value
	 * indicates that the game is an unknown one, but suppresses repeated
	 * attempts to identify it on successive calls.
	 */
	if (!_gameName) {
		gln_game_tableref_t game;

		/*
		 * If the interpreter hasn't yet loaded a game, startdata is nullptr
		 * (uninitialized, global).  In this case, we return nullptr, allowing
		 * for retries until a game is loaded.
		 */
		if (!_startData)
			return nullptr;

		game = gln_gameid_identify_game();
		_gameName = game ? game->name : "";
	}

	/* Return the game's name, or nullptr if it was unidentifiable. */
	assert(_gameName);
	return strlen(_gameName) > 0 ? _gameName : nullptr;
}

/**
 * Clear the saved game name, forcing a new lookup when next queried.  This
 * function should be called by actions that may cause the interpreter to
 * change game file, for example os_set_filenumber().
 */
void GameDetection::gln_gameid_game_name_reset() {
	_gameName = nullptr;
}

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

void Level9MetaEngine::getSupportedGames(PlainGameList &games) {
	const char *prior_id = nullptr;

	for (const gln_game_table_t *pd = GLN_GAME_TABLE; pd->name; ++pd) {
		if (prior_id == nullptr || strcmp(pd->gameId, prior_id)) {
			PlainGameDescriptor gd;
			gd.gameId = pd->gameId;
			gd.description = pd->name;
			games.push_back(gd);

			prior_id = pd->gameId;
		}
	}
}

const GlkDetectionEntry* Level9MetaEngine::getDetectionEntries() {
	static Common::Array<GlkDetectionEntry> entries;
	for (const gln_game_table_t *entry = GLN_GAME_TABLE; entry->gameId; ++entry) {
		const char* crc = "";
		GlkDetectionEntry detection = {
			entry->gameId,
			entry->extra,
			crc,
			entry->length,
			Common::EN_ANY,
			Common::kPlatformUnknown
		};
		entries.push_back(detection);
	}

	entries.push_back({nullptr,
					   nullptr,
					   nullptr,
					   0,
					   Common::UNK_LANG,
					   Common::kPlatformUnknown});

	return entries.data();
}

GameDescriptor Level9MetaEngine::findGame(const char *gameId) {
	if (!strncmp(gameId, "level9v", 7)) {
		GameDescriptor gd(gameId, "Unknown Level 9 game or version", 0);
		return gd;
	}
	for (const gln_game_table_t *pd = GLN_GAME_TABLE; pd->gameId; ++pd) {
		if (!strcmp(gameId, pd->gameId)) {
			GameDescriptor gd(pd->gameId, pd->name, 0);
			return gd;
		}
	}

	return PlainGameDescriptor::empty();
}

bool Level9MetaEngine::detectGames(const Common::FSList &fslist, DetectedGames &gameList) {
	// Loop through the files of the folder
	for (Common::FSList::const_iterator file = fslist.begin(); file != fslist.end(); ++file) {
		// Check for a recognised filename
		if (file->isDirectory())
			continue;
		Common::String filename = file->getName();
		if (!filename.hasSuffixIgnoreCase(".l9") && !filename.hasSuffixIgnoreCase(".dat"))
			continue;

		// Open up the file so we can get it's size
		Common::File gameFile;
		if (!gameFile.open(*file))
			continue;

		uint32 fileSize = gameFile.size();
		if (fileSize == 0 || fileSize > 0xffff) {
			// Too big or too small to possibly be a Level 9 game
			gameFile.close();
			continue;
		}

		// Read in the game data
		Common::Array<byte> data;
		data.resize(fileSize + 1);
		gameFile.read(&data[0], fileSize);
		gameFile.close();

		// Check if it's a valid Level 9 game
		byte *startFile = &data[0];
		Scanner scanner;
		int offset = scanner.scanner(&data[0], fileSize) < 0;
		if (offset < 0)
			continue;

		// Check for the specific game
		byte *startData = startFile + offset;
		GameDetection detection(startData, fileSize);
		detection._gameType = scanner._gameType;
		detection._l9V1Game = scanner._l9V1Game;

		const gln_game_tableref_t game = detection.gln_gameid_identify_game();
		if (!game)
			continue;

		// Found the game, add a detection entry
		DetectedGame gd = DetectedGame("glk", game->gameId, game->name, Common::UNK_LANG,
			Common::kPlatformUnknown, game->extra);
		gd.addExtraEntry("filename", filename);
		gameList.push_back(gd);
	}

	return !gameList.empty();
}

void Level9MetaEngine::detectClashes(Common::StringMap &map) {
	const char *prior_id = nullptr;

	for (const gln_game_table_t *pd = GLN_GAME_TABLE; pd->name; ++pd) {
		if (prior_id == nullptr || strcmp(pd->gameId, prior_id)) {
			prior_id = pd->gameId;

			if (map.contains(pd->gameId))
				error("Duplicate game Id found - %s", pd->gameId);
			map[pd->gameId] = "";
		}
	}
}

} // End of namespace Level9
} // End of namespace Glk
