/* Copyright (c) 2013-2017 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/core/library.h>

#include <mgba/core/core.h>
#include <mgba-util/vfs.h>

#ifdef USE_SQLITE3

#include <sqlite3.h>
#include "feature/sqlite3/no-intro.h"

DEFINE_VECTOR(mLibraryListing, struct mLibraryEntry);

struct mLibrary {
	sqlite3* db;
	sqlite3_stmt* insertPath;
	sqlite3_stmt* insertRom;
	sqlite3_stmt* insertRoot;
	sqlite3_stmt* selectRom;
	sqlite3_stmt* selectRoot;
	sqlite3_stmt* deletePath;
	sqlite3_stmt* deleteRoot;
	sqlite3_stmt* count;
	sqlite3_stmt* select;
	const struct NoIntroDB* gameDB;
};

#define CONSTRAINTS_ROMONLY \
	"CASE WHEN :useSize THEN roms.size = :size ELSE 1 END AND " \
	"CASE WHEN :usePlatform THEN roms.platform = :platform ELSE 1 END AND " \
	"CASE WHEN :useCrc32 THEN roms.crc32 = :crc32 ELSE 1 END AND " \
	"CASE WHEN :useInternalCode THEN roms.internalCode = :internalCode ELSE 1 END"

#define CONSTRAINTS \
	CONSTRAINTS_ROMONLY " AND " \
	"CASE WHEN :useFilename THEN paths.path = :path ELSE 1 END AND " \
	"CASE WHEN :useRoot THEN roots.path = :root ELSE 1 END"

static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry);
static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry);
static void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf);

static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry* constraints) {
	if (!constraints) {
		return;
	}

	int useIndex, index;
	if (constraints->crc32) {
		useIndex = sqlite3_bind_parameter_index(statement, ":useCrc32");
		index = sqlite3_bind_parameter_index(statement, ":crc32");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_int(statement, index, constraints->crc32);
	}

	if (constraints->filesize) {
		useIndex = sqlite3_bind_parameter_index(statement, ":useSize");
		index = sqlite3_bind_parameter_index(statement, ":size");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_int64(statement, index, constraints->filesize);
	}

	if (constraints->filename) {
		useIndex = sqlite3_bind_parameter_index(statement, ":useFilename");
		index = sqlite3_bind_parameter_index(statement, ":path");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_text(statement, index, constraints->filename, -1, SQLITE_TRANSIENT);
	}

	if (constraints->base) {
		useIndex = sqlite3_bind_parameter_index(statement, ":useRoot");
		index = sqlite3_bind_parameter_index(statement, ":root");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_text(statement, index, constraints->base, -1, SQLITE_TRANSIENT);
	}

	if (constraints->internalCode[0]) {
		useIndex = sqlite3_bind_parameter_index(statement, ":useInternalCode");
		index = sqlite3_bind_parameter_index(statement, ":internalCode");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_text(statement, index, constraints->internalCode, -1, SQLITE_TRANSIENT);
	}

	if (constraints->platform != PLATFORM_NONE) {
		useIndex = sqlite3_bind_parameter_index(statement, ":usePlatform");
		index = sqlite3_bind_parameter_index(statement, ":platform");
		sqlite3_bind_int(statement, useIndex, 1);
		sqlite3_bind_int(statement, index, constraints->platform);
	}
}

struct mLibrary* mLibraryCreateEmpty(void) {
	return mLibraryLoad(":memory:");
}

struct mLibrary* mLibraryLoad(const char* path) {
	struct mLibrary* library = malloc(sizeof(*library));
	memset(library, 0, sizeof(*library));

	if (sqlite3_open_v2(path, &library->db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL)) {
		goto error;
	}

	static const char createTables[] =
		"   PRAGMA foreign_keys = ON;"
		"\n PRAGMA journal_mode = MEMORY;"
		"\n PRAGMA synchronous = NORMAL;"
		"\n CREATE TABLE IF NOT EXISTS version ("
		"\n 	tname TEXT NOT NULL PRIMARY KEY,"
		"\n 	version INTEGER NOT NULL DEFAULT 1"
		"\n );"
		"\n CREATE TABLE IF NOT EXISTS roots ("
		"\n 	rootid INTEGER NOT NULL PRIMARY KEY ASC,"
		"\n 	path TEXT NOT NULL UNIQUE,"
		"\n 	mtime INTEGER NOT NULL DEFAULT 0"
		"\n );"
		"\n CREATE TABLE IF NOT EXISTS roms ("
		"\n 	romid INTEGER NOT NULL PRIMARY KEY ASC,"
		"\n 	internalTitle TEXT,"
		"\n 	internalCode TEXT,"
		"\n 	platform INTEGER NOT NULL DEFAULT -1,"
		"\n 	size INTEGER,"
		"\n 	crc32 INTEGER,"
		"\n 	md5 BLOB,"
		"\n 	sha1 BLOB"
		"\n );"
		"\n CREATE TABLE IF NOT EXISTS paths ("
		"\n 	pathid INTEGER NOT NULL PRIMARY KEY ASC,"
		"\n 	romid INTEGER NOT NULL REFERENCES roms(romid) ON DELETE CASCADE,"
		"\n 	path TEXT NOT NULL,"
		"\n 	mtime INTEGER NOT NULL DEFAULT 0,"
		"\n 	rootid INTEGER REFERENCES roots(rootid) ON DELETE CASCADE,"
		"\n 	customTitle TEXT,"
		"\n 	CONSTRAINT location UNIQUE (path, rootid)"
		"\n );"
		"\n CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);"
		"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('version', 1);"
		"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roots', 1);"
		"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 1);"
		"\n INSERT OR IGNORE INTO version (tname, version) VALUES ('paths', 1);";
	if (sqlite3_exec(library->db, createTables, NULL, NULL, NULL)) {
		goto error;
	}

	static const char insertPath[] = "INSERT INTO paths (romid, path, customTitle, rootid) VALUES (?, ?, ?, ?);";
	if (sqlite3_prepare_v2(library->db, insertPath, -1, &library->insertPath, NULL)) {
		goto error;
	}

	static const char insertRom[] = "INSERT INTO roms (crc32, size, internalCode, platform) VALUES (:crc32, :size, :internalCode, :platform);";
	if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) {
		goto error;
	}

	static const char insertRoot[] = "INSERT INTO roots (path) VALUES (?);";
	if (sqlite3_prepare_v2(library->db, insertRoot, -1, &library->insertRoot, NULL)) {
		goto error;
	}

	static const char deleteRoot[] = "DELETE FROM roots WHERE path = ?;";
	if (sqlite3_prepare_v2(library->db, deleteRoot, -1, &library->deleteRoot, NULL)) {
		goto error;
	}

	static const char deletePath[] = "DELETE FROM paths WHERE path = ?;";
	if (sqlite3_prepare_v2(library->db, deletePath, -1, &library->deletePath, NULL)) {
		goto error;
	}

	static const char selectRom[] = "SELECT romid FROM roms WHERE " CONSTRAINTS_ROMONLY ";";
	if (sqlite3_prepare_v2(library->db, selectRom, -1, &library->selectRom, NULL)) {
		goto error;
	}

	static const char selectRoot[] = "SELECT rootid FROM roots WHERE path = ? AND CASE WHEN :useMtime THEN mtime <= :mtime ELSE 1 END;";
	if (sqlite3_prepare_v2(library->db, selectRoot, -1, &library->selectRoot, NULL)) {
		goto error;
	}

	static const char count[] = "SELECT count(pathid) FROM paths JOIN roots USING (rootid) JOIN roms USING (romid) WHERE " CONSTRAINTS ";";
	if (sqlite3_prepare_v2(library->db, count, -1, &library->count, NULL)) {
		goto error;
	}

	static const char select[] = "SELECT *, paths.path AS filename, roots.path AS base FROM paths JOIN roots USING (rootid) JOIN roms USING (romid) WHERE " CONSTRAINTS " LIMIT :count OFFSET :offset;";
	if (sqlite3_prepare_v2(library->db, select, -1, &library->select, NULL)) {
		goto error;
	}

	return library;

error:
	mLibraryDestroy(library);
	return NULL;
}

void mLibraryDestroy(struct mLibrary* library) {
	sqlite3_finalize(library->insertPath);
	sqlite3_finalize(library->insertRom);
	sqlite3_finalize(library->insertRoot);
	sqlite3_finalize(library->deletePath);
	sqlite3_finalize(library->deleteRoot);
	sqlite3_finalize(library->selectRom);
	sqlite3_finalize(library->selectRoot);
	sqlite3_finalize(library->select);
	sqlite3_finalize(library->count);
	sqlite3_close(library->db);
	free(library);
}

void mLibraryLoadDirectory(struct mLibrary* library, const char* base) {
	struct VDir* dir = VDirOpenArchive(base);
	if (!dir) {
		dir = VDirOpen(base);
	}
	sqlite3_exec(library->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
	if (!dir) {
		sqlite3_clear_bindings(library->deleteRoot);
		sqlite3_reset(library->deleteRoot);
		sqlite3_bind_text(library->deleteRoot, 1, base, -1, SQLITE_TRANSIENT);
		sqlite3_step(library->deleteRoot);
		sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
		return;
	}

	struct mLibraryEntry entry;
	memset(&entry, 0, sizeof(entry));
	entry.base = base;
	struct mLibraryListing entries;
	mLibraryListingInit(&entries, 0);
	mLibraryGetEntries(library, &entries, 0, 0, &entry);
	size_t i;
	for (i = 0; i < mLibraryListingSize(&entries); ++i) {
		struct mLibraryEntry* current = mLibraryListingGetPointer(&entries, i);
		struct VFile* vf = dir->openFile(dir, current->filename, O_RDONLY);
		_mLibraryDeleteEntry(library, current);
		if (!vf) {
			continue;
		}
		_mLibraryAddEntry(library, current->filename, base, vf);
	}
	mLibraryListingDeinit(&entries);

	dir->rewind(dir);
	struct VDirEntry* dirent = dir->listNext(dir);
	while (dirent) {
		struct VFile* vf = dir->openFile(dir, dirent->name(dirent), O_RDONLY);
		if (!vf) {
			dirent = dir->listNext(dir);
			continue;
		}
		_mLibraryAddEntry(library, dirent->name(dirent), base, vf);
		dirent = dir->listNext(dir);
	}
	dir->close(dir);
	sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
}

void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
	struct mCore* core;
	if (!vf) {
		return;
	}
	core = mCoreFindVF(vf);
	if (core) {
		struct mLibraryEntry entry;
		memset(&entry, 0, sizeof(entry));
		core->init(core);
		core->loadROM(core, vf);

		core->getGameTitle(core, entry.internalTitle);
		core->getGameCode(core, entry.internalCode);
		core->checksum(core, &entry.crc32, CHECKSUM_CRC32);
		entry.platform = core->platform(core);
		entry.title = NULL;
		entry.base = base;
		entry.filename = filename;
		entry.filesize = vf->size(vf);
		_mLibraryInsertEntry(library, &entry);
		// Note: this destroys the VFile
		core->deinit(core);
	} else {
		vf->close(vf);
	}
}

static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
	sqlite3_clear_bindings(library->selectRom);
	sqlite3_reset(library->selectRom);
	struct mLibraryEntry constraints = *entry;
	constraints.filename = NULL;
	constraints.base = NULL;
	_bindConstraints(library->selectRom, &constraints);
	sqlite3_int64 romId;
	if (sqlite3_step(library->selectRom) == SQLITE_DONE) {
		sqlite3_clear_bindings(library->insertRom);
		sqlite3_reset(library->insertRom);
		_bindConstraints(library->insertRom, entry);
		sqlite3_step(library->insertRom);
		romId = sqlite3_last_insert_rowid(library->db);
	} else {
		romId = sqlite3_column_int64(library->selectRom, 0);
	}

	sqlite3_int64 rootId = 0;
	if (entry->base) {
		sqlite3_clear_bindings(library->selectRoot);
		sqlite3_reset(library->selectRoot);
		sqlite3_bind_text(library->selectRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
		if (sqlite3_step(library->selectRoot) == SQLITE_DONE) {
			sqlite3_clear_bindings(library->insertRoot);
			sqlite3_reset(library->insertRoot);
			sqlite3_bind_text(library->insertRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
			sqlite3_step(library->insertRoot);
			rootId = sqlite3_last_insert_rowid(library->db);
		} else {
			rootId = sqlite3_column_int64(library->selectRoot, 0);
		}
	}

	sqlite3_clear_bindings(library->insertPath);
	sqlite3_reset(library->insertPath);
	sqlite3_bind_int64(library->insertPath, 1, romId);
	sqlite3_bind_text(library->insertPath, 2, entry->filename, -1, SQLITE_TRANSIENT);
	sqlite3_bind_text(library->insertPath, 3, entry->title, -1, SQLITE_TRANSIENT);
	if (rootId > 0) {
		sqlite3_bind_int64(library->insertPath, 4, rootId);
	}
	sqlite3_step(library->insertPath);
}

static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
	sqlite3_clear_bindings(library->deletePath);
	sqlite3_reset(library->deletePath);
	sqlite3_bind_text(library->deletePath, 1, entry->filename, -1, SQLITE_TRANSIENT);
	sqlite3_step(library->insertPath);
}

void mLibraryClear(struct mLibrary* library) {
	int result = sqlite3_exec(library->db,
		"   BEGIN TRANSACTION;"
		"\n DELETE FROM roots;"
		"\n DELETE FROM roms;"
		"\n DELETE FROM paths;"
		"\n COMMIT;"
		"\n VACUUM;", NULL, NULL, NULL);
}

size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints) {
	sqlite3_clear_bindings(library->count);
	sqlite3_reset(library->count);
	_bindConstraints(library->count, constraints);
	if (sqlite3_step(library->count) != SQLITE_ROW) {
		return 0;
	}
	return sqlite3_column_int64(library->count, 0);
}

size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, size_t numEntries, size_t offset, const struct mLibraryEntry* constraints) {
	mLibraryListingClear(out); // TODO: Free memory
	sqlite3_clear_bindings(library->select);
	sqlite3_reset(library->select);
	_bindConstraints(library->select, constraints);

	int countIndex = sqlite3_bind_parameter_index(library->select, ":count");
	int offsetIndex = sqlite3_bind_parameter_index(library->select, ":offset");
	sqlite3_bind_int64(library->select, countIndex, numEntries ? numEntries : -1);
	sqlite3_bind_int64(library->select, offsetIndex, offset);

	size_t entryIndex;
	for (entryIndex = 0; (!numEntries || entryIndex < numEntries) && sqlite3_step(library->select) == SQLITE_ROW; ++entryIndex) {
		struct mLibraryEntry* entry = mLibraryListingAppend(out);
		memset(entry, 0, sizeof(*entry));
		int nCols = sqlite3_column_count(library->select);
		int i;
		for (i = 0; i < nCols; ++i) {
			const char* colName = sqlite3_column_name(library->select, i);
			if (strcmp(colName, "crc32") == 0) {
				entry->crc32 = sqlite3_column_int(library->select, i);
				struct NoIntroGame game;
				if (NoIntroDBLookupGameByCRC(library->gameDB, entry->crc32, &game)) {
					entry->title = strdup(game.name);
				}
			} else if (strcmp(colName, "platform") == 0) {
				entry->platform = sqlite3_column_int(library->select, i);
			} else if (strcmp(colName, "size") == 0) {
				entry->filesize = sqlite3_column_int64(library->select, i);
			} else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
				strncpy(entry->internalCode, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalCode) - 1);
			} else if (strcmp(colName, "internalTitle") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
				strncpy(entry->internalTitle, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalTitle) - 1);
			} else if (strcmp(colName, "filename") == 0) {
				entry->filename = strdup((const char*) sqlite3_column_text(library->select, i));
			} else if (strcmp(colName, "base") == 0) {
				entry->base =  strdup((const char*) sqlite3_column_text(library->select, i));
			}
		}
	}
	return mLibraryListingSize(out);
}

void mLibraryEntryFree(struct mLibraryEntry* entry) {
	free((void*) entry->title);
	free((void*) entry->filename);
	free((void*) entry->base);
}

struct VFile* mLibraryOpenVFile(struct mLibrary* library, const struct mLibraryEntry* entry) {
	struct mLibraryListing entries;
	mLibraryListingInit(&entries, 0);
	if (!mLibraryGetEntries(library, &entries, 0, 0, entry)) {
		mLibraryListingDeinit(&entries);
		return NULL;
	}
	struct VFile* vf = NULL;
	size_t i;
	for (i = 0; i < mLibraryListingSize(&entries); ++i) {
		struct mLibraryEntry* e = mLibraryListingGetPointer(&entries, i);
		struct VDir* dir = VDirOpenArchive(e->base);
		bool isArchive = true;
		if (!dir) {
			dir = VDirOpen(e->base);
			isArchive = false;
		}
		if (!dir) {
			continue;
		}
		vf = dir->openFile(dir, e->filename, O_RDONLY);
		if (vf && isArchive) {
			struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
			uint8_t buffer[2048];
			ssize_t read;
			while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
				vfclone->write(vfclone, buffer, read);
			}
			vf->close(vf);
			vf = vfclone;
		}
		dir->close(dir);
		if (vf) {
			break;
		}
	}
	return vf;
}

void mLibraryAttachGameDB(struct mLibrary* library, const struct NoIntroDB* db) {
	library->gameDB = db;
}

#endif
