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


// ***TODO*** Maybe add a cacheFOpen() and cacheFClose() for reading and writing directly to virtual pathnames?


#include "thirdparty/SHA256/sha256.h"

#include "cache.h"


#define CACHE_FIELD_SHA256     0
#define CACHE_FIELD_ENTRYNAME  1
#define CACHE_FIELD_FILENAME   2


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


uint8_t cacheDelete(char *virtualPath) {
	FILE    *in           = NULL;
	FILE    *out          = NULL;
	char     index[16]    = { 0 };
	char     indexNew[16] = { 0 };
	char     buffer[1024] = { 0 };
	char    *name         = NULL;
	char    *path         = NULL;
	char    *dir2         = NULL;
	char    *dir1         = NULL;
	char    *entryDir     = NULL;

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

	sprintf(index,    "CACHE%cINDEX.DAT", OS_PATH_SLASH);
	sprintf(indexNew, "CACHE%cINDEX.NEW", OS_PATH_SLASH);

	// Do we have an index yet?
	if (osFileExists(index)) {
		in = fopen(index, "rt");
		if (in) {
			out = fopen(indexNew, "wt");
			if (out) {
				while (fgets(buffer, 1024, in) != 0) {
					name = strstr(buffer, " ");
					*name = 0;
					name++;
					path = strstr(name, " ");
					*path = 0;
					path++;
					path[strlen(path) - 1] = 0;
					if (strcmp(virtualPath, path) == 0) {
						// Found!  Delete data from disk.
						entryDir = cacheEntryNameDirGet(name, 1);
						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);
					} else {
						// Not what we're after, copy to new index.
						fprintf(out, "%s %s %s\n", buffer, name, path);
					}
				}
			} else {
				fclose(in);
				return FAIL;
			}
			fclose(in);
			unlink(index);
			rename(indexNew, index);
			return SUCCESS;
		}
	}

	return FAIL;
}


uint8_t cacheEntryAdd(char *sha256, char *entryName, char *virtualPath) {
	FILE    *in           = NULL;
	FILE    *out          = NULL;
	char     index[16]    = { 0 };
	char     indexNew[16] = { 0 };
	char    *name         = NULL;
	char    *path         = NULL;
	char     buffer[1024] = { 0 };
	uint8_t  found        = 0;

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

	// Index format is:  SHA256 ENTRYNAME VIRTUALPATH

	logWrite("Adding SHA [%s] Entry [%s] VPath [%s]\n", sha256, entryName, virtualPath);

	sprintf(index,    "CACHE%cINDEX.DAT", OS_PATH_SLASH);
	sprintf(indexNew, "CACHE%cINDEX.NEW", OS_PATH_SLASH);

	// Do we have an index yet?
	if (!osFileExists(index)) {
		// Nope.  Just add this entry and be done.
		out = fopen(index, "wt");
		if (out) {
			fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
			fclose(out);
			return SUCCESS;
		}
		return FAIL;
	}

	// Read existing index, update the entry if it exists.
	// If not, we'll add it at the end.
	in = fopen(index, "rt");
	if (in) {
		out = fopen(indexNew, "wt");
		if (out) {
			while (fgets(buffer, 1024, in) != 0) {
				name = strstr(buffer, " ");
				*name = 0;
				name++;
				path = strstr(name, " ");
				*path = 0;
				path++;
				path[strlen(path) - 1] = 0;
				if (strcmp(virtualPath, path) == 0) {
					// Update this entry.
					fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
					found = 1;
				} else {
					// Not what we're after, copy to new index.
					fprintf(out, "%s %s %s\n", buffer, name, path);
				}
			}
			// Did we replace an entry?
			if (!found) {
				// Add new entry to end.
				fprintf(out, "%s %s %s\n", sha256, entryName, virtualPath);
			}
			fclose(out);
		} else {
			fclose(in);
			return FAIL;
		}
		fclose(in);
		unlink(index);
		rename(indexNew, index);
		return SUCCESS;
	}

	return FAIL;
}


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) {
	return cacheFieldGet(virtualPath, CACHE_FIELD_ENTRYNAME);
}


uint8_t cacheExists(void) {
	char index[16] = { 0 };

	sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);

	return osFileExists(index);
}


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


static char *cacheFieldGet(char *virtualPath, uint8_t field) {
	FILE        *in           = NULL;
	char         index[16]    = { 0 };
	static char  buffer[2048] = { 0 };
	static char *name         = NULL;
	static char *path         = NULL;
	static char *result       = NULL;

	// Return SHA256 given virtual path or NULL if not found.

	sprintf(index, "CACHE%cINDEX.DAT", OS_PATH_SLASH);

	result = NULL;

	// Do we have an index yet?
	if (osFileExists(index)) {
		in = fopen(index, "rt");
		if (in) {
			// Be sure the fread is the last conditional so it short-circuts properly.
			while (result == NULL && (fgets(buffer, 2048, in) != 0)) {
				name = strstr(buffer, " ");
				*name = 0;
				name++;
				path = strstr(name, " ");
				*path = 0;
				path++;
				path[strlen(path) - 1] = 0;
				if (strcmp(virtualPath, path) == 0) {
					// Found!  Return requested data.
					switch (field) {
						case CACHE_FIELD_SHA256:
							result = buffer;
							break;

						case CACHE_FIELD_ENTRYNAME:
							result = name;
							break;

						case CACHE_FIELD_FILENAME:
							result = cacheEntryNameDirGet(name, 1);
							break;
					}
				}
			}
			fclose(in);
		}
	}

	return result;
}


char *cacheFilenameGet(char *virtualPath) {
	return cacheFieldGet(virtualPath, CACHE_FIELD_FILENAME);
}


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 cachePrePack(char *name, CachePreMakeListT *list) {

	FILE           *in           = NULL;
	FILE           *out          = NULL;
	uint16_t        i            = 0;
	uint8_t         result       = SUCCESS;
	uint32_t        length       = 0;
	uint32_t        total        = 0;
	uint32_t        temp         = 0;
	int8_t          buffer[8192] = { 0 };
	sha256_context  ctx          = { 0 };
	uint8_t         hv[32]       = { 0 };

	// Prebuilt cache format:
	// - Virtual Path (zero terminated)
	// - x bytes hash
	// - 4 bytes unsigned length of data
	// - length bytes of data
	// ... repeat for each file ...
	// - 4 bytes length of entire cache (used for embedding cache data)

	out = fopen(name, "wb");
	if (out) {
		while (list[i].filename != NULL) {
			in = fopen(list[i].filename, "rb");
			if (in) {
				// Get length of file.
				fseek(in, 0, SEEK_END);
				length = ftell(in);
				fseek(in, 0, SEEK_SET);
				// Generate SHA256.
				sha256_init(&ctx);
				do {
					temp = fread(buffer, 1, sizeof(buffer), in);
					if (temp) sha256_hash(&ctx, (uint8_t *)buffer, temp);
				} while (temp > 0);
				sha256_done(&ctx, hv);
				fseek(in, 0, SEEK_SET);
				// Write header.
				temp = strlen(list[i].virtualPath) + 1;
				fwrite(list[i].virtualPath, temp, 1, out);
				fwrite(hv, sizeof(hv), 1, out);
				fwrite(&length, sizeof(uint32_t), 1, out);
				total += temp + sizeof(hv) + sizeof(uint32_t) + length;
				// Copy data.
				do {
					temp = fread(buffer, 1, sizeof(buffer), in);
					if (temp) fwrite(buffer, 1, temp, out);
				} while (temp > 0);
				fclose(in);
			} else {
				result = FAIL;
			}
			i++;
		}
		// Write length of entire block.
		total += sizeof(uint32_t);
		fwrite(&total, sizeof(uint32_t), 1, out);
		fclose(out);
	} else {
		result = FAIL;
	}

	return result;
}


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 };
	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 *)&buffer[i * 2], "%02x", hv[i]);
			buffer[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 *)buffer, 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);
			} else {
				result = FAIL;
			}
		} while ((unsigned)ftell(in) < (total - sizeof(uint32_t)));
		fclose(in);
	} else {
		result = FAIL;
	}

	return result;
}


char *cacheSha256Get(char *virtualPath) {
	return cacheFieldGet(virtualPath, CACHE_FIELD_SHA256);
}


void cacheShutdown(void) {
	// Nada.
}


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

	// Do we need to unpack the initial cache?
	if (!cacheExists()) {
		temp = utilAppNameWithNewExtensionGet(appName, "pre");
		cachePreUnpack(temp);
		DEL(temp);
	}
}
