/*
 * Kangaroo Punch MultiPlayer Game Server Mark II
 * Copyright (C) 2020-2021  Scott Duensing
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 */


#include "thirdparty/SHA256/sha256.h"

#include "db.h"
#include "image.h"

#include "cache.h"


static char *cacheEntryNameDirGet(char *entryName, uint8_t includeCache);
static char *cacheEntryNameGenerate(void);


uint8_t cacheDelete(char *virtualPath) {
	char    *dir2         = NULL;
	char    *dir1         = NULL;
	char    *entryDir     = NULL;

	// Deletes entry from cache index AND the data from the cache.

	entryDir = cacheFilenameGet(virtualPath);
	if (entryDir) {
		// Found!  Delete data from disk.
		unlink(entryDir);
		// Attempt to remove the second-level directory.
		dir2 = osLastPathComponentGetUpTo(entryDir);
		if (rmdir(dir2) == 0) {
			// Attempt to move the top level directory.
			dir1 = osLastPathComponentGetUpTo(dir2);
			rmdir(dir1);
			DEL(dir1);
		}
		DEL(dir2);
	}

	return SUCCESS;
}


uint8_t cacheEntryAdd(char *sha256, char *entryName, char *virtualPath) {

	// This adds or updates an entry to the index.
	// It does not add data to the actual cache.

	dbExecute(
		"REPLACE INTO cache (sha, entry, path) VALUES (?, ?, ?)",
		"vvv",
		sha256, entryName, virtualPath
	);

	return SUCCESS;
}


static char *cacheEntryNameDirGet(char *entryName, uint8_t includeCache) {
	uint8_t     i       = 0;
	uint8_t     j       = 0;
	static char dir[21] = "CACHE";

	dir[5] = OS_PATH_SLASH;

	j = 6;
	for (i=0; i<13; i++) {
		dir[j++] = entryName[i];
		if ((i == 1) || (i == 3)) {
			dir[j++] = OS_PATH_SLASH;
		}
	}

	dir[20] = 0;

	return includeCache ? dir : &dir[6];
}


static char *cacheEntryNameGenerate(void) {
	uint8_t     i        = 0;
	static char name[13] = { 0 };

	// Cached files are stored in a two-layer directory tree to
	// reduce the number of files in any single folder.
	// The folder names are two digits.
	// The filename is eight random letters.

	// First directory level.
	i = random() % 100;
	sprintf(&name[0], "%02d", i);

	// Second directory level.
	i = random() % 100;
	sprintf(&name[2], "%02d", i);

	// Random name.
	for (i=4; i<12; i++) name[i] = 'A' + (random() % 26);

	name[12] = 0;

	return name;
}


char *cacheEntryNameGet(char *virtualPath) {
	char *temp = NULL;
	dbQuerySingle('v', (void **)&temp, "SELECT entry FROM cache WHERE path = ?", "v", virtualPath);
	if (DB_RESULT_EMPTY(temp)) temp = NULL;
	return temp;
}


void cacheFClose(FILE *handle) {
	// Just wrap fclose() for now.
	fclose(handle);
}


char *cacheFilenameGet(char *virtualPath) {
	char *temp = NULL;
	dbQuerySingle('v', (void **)&temp, "SELECT entry FROM cache WHERE path = ?", "v", virtualPath);
	if (DB_RESULT_EMPTY(temp)) return NULL;
	return cacheEntryNameDirGet(temp, 1);
}


FILE *cacheFOpen(char *vpath, char *mode) {
	FILE *handle = NULL;
	char *entry  = NULL;
	char *dir    = NULL;
	char *temp   = NULL;

	// Does this file already exist?
	entry = cacheEntryNameGet(vpath);

	// If we're writing or appending, do we need to create it?
	if ((entry == NULL) && (mode[0] == 'w' || mode[0] == 'a')) {
		do {
			entry = cacheEntryNameGenerate();
			dir = cacheEntryNameDirGet(entry, 1);
			temp = osLastPathComponentGetUpTo(dir);
			osMkDirP(temp);
			DEL(temp);
		} while (osFileExists(dir));
		// Dummy SHA for files we create ourselves.  They're never checked against the server.
		cacheEntryAdd("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", entry, vpath);
	}

	// Did we get a file?
	if (entry) {
		dir = cacheEntryNameDirGet(entry, 1);
		handle = fopen(dir, mode);
	}

	return handle;
}


uint8_t cachePreUnpack(char *name) {
	FILE     *in                                  = NULL;
	FILE     *out                                 = NULL;
	char     *entryName                           = NULL;
	char     *entryDir                            = NULL;
	char     *temp                                = NULL;
	uint8_t   result                              = SUCCESS;
	uint32_t  i                                   = 0;
	uint32_t  length                              = 0;
	uint32_t  total                               = 0;
	int8_t    buffer[8192]                        = { 0 };
	int8_t    sha[65]                             = { 0 };
	uint8_t   hv[32]                              = { 0 };
	char      virtualPath[CACHE_VIRTUAL_PATH_MAX] = { 0 };

	in = fopen(name, "rb");
	if (in) {
		// Get entire pack size.
		fseek(in, -sizeof(uint32_t), SEEK_END);
		fread(&total, 1, sizeof(uint32_t), in);
		fseek(in, 0, SEEK_SET);
		do {
			// Read virtual path.
			i = 0;
			do {
				virtualPath[i++] = fgetc(in);
			} while (virtualPath[i - 1] != 0);
			// Read SHA256.
			fread(hv, 1, sizeof(hv), in);
			for (i=0; i<32; i++) sprintf((char *)&sha[i * 2], "%02x", hv[i]);
			sha[64] = 0;
			// Read length.
			fread(&length, 1, sizeof(uint32_t), in);
			// Get filename from cache manager & create missing directories.
			do {
				entryName = cacheEntryNameGenerate();
				entryDir = cacheEntryNameDirGet(entryName, 1);
				temp = osLastPathComponentGetUpTo(entryDir);
				osMkDirP(temp);
				DEL(temp);
			} while (osFileExists(entryDir));
			cacheEntryAdd((char *)sha, entryName, virtualPath);
			out = fopen(entryDir, "wb");
			if (out) {
				// Copy out data.
				do {
					if (length > sizeof(buffer)) {
						i = sizeof(buffer);
					} else {
						i = length;
					}
					length -= i;
					fread(buffer, 1, i, in);
					fwrite(buffer, 1, i, out);
				} while (length > 0);
				fclose(out);
				imageCacheIfNeeded(virtualPath, (char *)sha);
			} else {
				result = FAIL;
			}
		} while ((unsigned)ftell(in) < (total - sizeof(uint32_t)));
		fclose(in);
	} else {
		result = FAIL;
	}

	return result;
}


char *cacheSha256Get(char *virtualPath) {
	char *temp = NULL;
	dbQuerySingle('v', (void **)&temp, "SELECT sha FROM cache WHERE path = ?", "v", virtualPath);
	if (DB_RESULT_EMPTY(temp)) temp = NULL;
	return temp;
}


void cacheShutdown(void) {
	// Nada.
}


void cacheStartup(char *appName) {
	char *temp = NULL;

	// Do we need to create the cache database?
	dbExecute(
		"CREATE TABLE IF NOT EXISTS cache ("
		"sha   VARCHAR(65), "
		"entry VARCHAR(13), "
		"path  VARCHAR(1024) PRIMARY KEY"
		")",
	NULL);

	// Do we need to unpack the initial cache?
	dbQuerySingle('v', (void **)&temp, "SELECT sha FROM cache LIMIT 1", NULL);
	if (DB_RESULT_EMPTY(temp)) {
		temp = utilAppNameWithNewExtensionGet(appName, "pre");
		cachePreUnpack(temp);
		DEL(temp);
	}
}
