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


#define _XOPEN_SOURCE 500
#include <ftw.h>
#include <errno.h>
#include <strings.h>

#include "console.h"
#include "settings.h"
#include "database.h"
#include "array.h"

#include "update.h"


static pthread_t  _updateThreadHandle  = { 0 };
static uint8_t    _updateThreadRunning = 0;


static void    *updateAll(void *path);
static void     updateConfig(void);
static void     updateFiles(void *path);
static int32_t  updateFileEntry(const char *filepath, const struct stat *info, const int typeflag, struct FTW *pathinfo);
static void     updateGames(void);



void update(void) {
	pthread_attr_t  updateThreadAttributes = { 0 };

	if (_updateThreadRunning) {
		consoleMessageQueue("An update is already running.  Wait until it completes.\n\r");
		return;
	}

	// Start update thread.
	_updateThreadRunning = 1;
	if (pthread_attr_init(&updateThreadAttributes) != 0) utilDie("Unable to create update thread attributes.\n");
	pthread_attr_setdetachstate(&updateThreadAttributes, PTHREAD_CREATE_JOINABLE);
	if (pthread_create(&_updateThreadHandle, &updateThreadAttributes, updateAll, NULL) != 0) utilDie("Unable to start update thread.\n");
}


static void *updateAll(void *data) {
	(void)data;

	updateConfig();
	updateGames();
	updateFiles("/");

	pthread_exit(NULL);
}


static void updateConfig(void) {
	DbClientConfigT  *record         = NULL;
	char              file[MAX_PATH] = { 0 };
	FILE             *f              = NULL;
	DbClientConfigT **config = NULL;

	consoleMessageQueue("Updating client configuration data.\n");

	snprintf(file, MAX_PATH, "%s/files/generated/client.dat", __settingsFile);
	f = fopen(file, "wb");
	if (f) {
		// Fetch string table from DB.
		config = dbClientConfigGet();
		if (config) {
			while (arrlen(config)) {
				record = config[0];
				arrdel(config, 0);
				// Write to config file that is sent to the client.
				fwrite(record->name, strlen(record->name) + 1, 1, f);
				fwrite(record->data, strlen(record->data) + 1, 1, f);
				dbClientConfigRelease(&record);
			}
			arrfree(config);
		}
		fclose(f);
	}
}


static void updateFiles(void *data) {
	char       file[MAX_PATH] = { 0 };
	char      *path           = (char *)data;
	int32_t    result         = 0;

	consoleMessageQueue("Updating file information starting from:  %s\n", data == NULL ? "/" : (char *)data);

	if (!path) {
		snprintf(file, MAX_PATH, "%s/files", __settingsFile);
	} else {
		if (path[0] == '/') {
			snprintf(file, MAX_PATH, "%s/files%s", __settingsFile, path);
		} else {
			snprintf(file, MAX_PATH, "%s/files/%s", __settingsFile, path);
		}
	}

	if (dbUpdateFilesStart() == SUCCESS) {
		result = nftw(file, updateFileEntry, 15, FTW_PHYS);
		dbUpdateFilesFinish();
	}
}


static int32_t updateFileEntry(const char *filepath, const struct stat *info, const int typeflag, struct FTW *pathinfo) {
	// const char *const filename = filepath + pathinfo->base;
	const double  bytes  = (double)info->st_size;  // Not exact if large!
	struct tm     mtime;
	char         *target;
	size_t        maxlen = MAX_PATH;
	ssize_t       len;
	char          time[64];

	(void)pathinfo;

	localtime_r(&(info->st_mtime), &mtime);

	snprintf(time, 64, "%04d-%02d-%02d %02d:%02d:%02d",
		mtime.tm_year + 1900, mtime.tm_mon + 1, mtime.tm_mday,
		mtime.tm_hour, mtime.tm_min, mtime.tm_sec);

	consoleMessageQueue("%s", time);

	if (bytes >= 1099511627776.0) {
		consoleMessageQueue(" %9.3f TiB", bytes / 1099511627776.0);
	} else {
		if (bytes >= 1073741824.0) {
			consoleMessageQueue(" %9.3f GiB", bytes / 1073741824.0);
		} else {
			if (bytes >= 1048576.0) {
				consoleMessageQueue(" %9.3f MiB", bytes / 1048576.0);
			} else {
				if (bytes >= 1024.0) {
					consoleMessageQueue(" %9.3f KiB", bytes / 1024.0);
				} else {
					consoleMessageQueue(" %9.0f B  ", bytes);
				}
			}
		}
	}

	if (typeflag == FTW_SL) {
		while (1) {
			target = malloc(maxlen + 1);
			if (target == NULL) return ENOMEM;
			len = readlink(filepath, target, maxlen);
			if (len == (ssize_t) - 1) {
				const int saved_errno = errno;
				free(target);
				return saved_errno;
			}
			if (len >= (ssize_t)maxlen) {
				free(target);
				maxlen += 1024;
				continue;
			}
			target[len] = '\0';
			break;
		}
		consoleMessageQueue(" %s -> %s", filepath, target);
		dbUpdateFileData((char *)filepath, (uint64_t)bytes, time);
		free(target);
	} else {
		if (typeflag == FTW_SLN) {
			consoleMessageQueue(" %s (dangling symlink)", filepath);
		} else {
			if (typeflag == FTW_F) {
				consoleMessageQueue(" %s", filepath);
				dbUpdateFileData((char *)filepath, (uint64_t)bytes, time);
			} else {
				if (typeflag == FTW_D || typeflag == FTW_DP) {
					consoleMessageQueue(" %s/", filepath);
				} else {
					if (typeflag == FTW_DNR) {
						consoleMessageQueue(" %s/ (unreadable)", filepath);
					} else {
						consoleMessageQueue(" %s (unknown)", filepath);
					}
				}
			}
		}
	}

	consoleMessageQueue("\n");

	return 0;
}


static void updateGames(void) {
	GameT   **gameList       = NULL;
	GameT    *game           = NULL;
	char      file[MAX_PATH] = { 0 };
	FILE     *f              = NULL;
	uint8_t   c              = 0;

	consoleMessageQueue("Updating client game database.\n");

	snprintf(file, MAX_PATH, "%s/files/generated/games.dat", __settingsFile);
	f = fopen(file, "wb");
	if (f) {
		gameList = dbGamesGet();
		while (arrlen(gameList) > 0) {
			game = gameList[0];

			fwrite(game->title,       strlen(game->title)       + 1, 1, f);
			fwrite(game->publisher,   strlen(game->publisher)   + 1, 1, f);
			fwrite(game->developer,   strlen(game->developer)   + 1, 1, f);
			fwrite(game->description, strlen(game->description) + 1, 1, f);
			fwrite(game->releaseDate, strlen(game->releaseDate) + 1, 1, f);
			fwrite(game->rating,      strlen(game->rating)      + 1, 1, f);
			fwrite(game->series,      strlen(game->series)      + 1, 1, f);
			fwrite(game->origin,      strlen(game->origin)      + 1, 1, f);
			fwrite(game->shortName,   strlen(game->shortName)   + 1, 1, f);
			fwrite(game->worksWith,   strlen(game->worksWith)   + 1, 1, f);
			c = game->type;        fputc(c, f);
			c = game->maxPlayers;  fputc(c, f);
			c = game->joinable;    fputc(c, f);
			c = game->screens;     fputc(c, f);
			c = game->boxes;       fputc(c, f);

			dbGameRelease(&game);
			arrdel(gameList, 0);
		}
		arrfree(gameList);
		fclose(f);
	}
}
