/*
 * 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 "network.h"
#include "packet.h"
#include "array.h"

#include "taglist.h"
#include "window.h"
#include "label.h"
#include "timer.h"
#include "msgbox.h"

#include "file.h"
#include "hangup.h"
#include "welcome.h"


// All this queue nonsense allows you to request more
// downloads while downloads are currently running.
typedef struct FileListS {
	char         **files;
	fileCallback   callback;
} FileListT;


static uint8_t        _channel       =  0;
static uint8_t        _dialogVisible =  0;
static FileListT    **_fileList      =  NULL;
static FileListT     *_current       =  NULL;
static uint32_t       _currentLength =  0;
static char          *_currentSha256 =  NULL;
static char          *_file          =  NULL;
static FILE          *_handle        =  NULL;
static WindowT       *_winFile       =  NULL;
static LabelT        *_lblFile       =  NULL;
static TimerT        *_timTimer      =  NULL;


static void btnMsgBoxOkay(MsgBoxButtonT button);
static void fileCheckNext(void);
static void fileShowDialog(void);
static void fileShowError(char *message);
static void packetHandler(PacketDecodeDataT *packet);
static void timTimerTimeout(WidgetT *widget);


static void btnMsgBoxOkay(MsgBoxButtonT button) {
	(void)button;
	hangupShow(welcomeShow);
}


void fileCacheCheck(fileCallback callback, char *vpaths[]) {
	FileListT *newList = NULL;
	uint16_t   i       = 0;

	// Add new entries to anything already in the queue.
	NEW(FileListT, newList);
	newList->callback = callback;
	newList->files    = NULL;
	while (vpaths[i] != NULL) {
		arrput(newList->files, strdup(vpaths[i]));
		i++;
	}
	arrput(_fileList, newList);

	// New queue?
	if (_channel == 0) {
		_dialogVisible = 0;
		_channel       = netChannelGet(packetHandler);
	}

	fileCheckNext();
}


static void fileCheckNext(void) {
	PacketEncodeDataT  encoded    = { 0 };
	char              *packetData = NULL;
	uint16_t           length     = 0;
	uint32_t           temp       = 0;
	uint8_t            doRecheck  = 0;
	char              *shaPointer = NULL;
	static char       *badSHA     = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";

	do {
		// This is ugly since both lists kind of depend on each other.
		doRecheck = 0;

		// Do we need a new entry from the queue?
		if (_current == NULL) {
			// End of queue?
			if (arrlen(_fileList) == 0) {
				logWrite("End of queue.\n");
				arrfree(_fileList);
				if (_dialogVisible) guiDelete(D(_winFile));
				netChannelRelease(_channel);
				return;
			}

			// Get next queue entry.
			logWrite("Next queue entry.\n");
			_currentLength = 0;
			_current       = _fileList[0];
			arrdel(_fileList, 0);
		}

		if (_file) DEL(_file);

		// End of current queue file list?
		if (arrlen(_current->files) == 0) {
			logWrite("End of files.\n");
			// End of list!
			arrfree(_current->files);
			_current->callback();
			DEL(_current);
			// See if there's more.
			doRecheck = 1;
		} else {
			// No.  Get next file.
			logWrite("Next file.\n");
			_file = _current->files[0];
			arrdel(_current->files, 0);
		}
	} while (doRecheck);

	// Check next file.
	temp = FILE_REQUEST_CHECK;
	shaPointer = cacheSha256Get(_file);
	if (!shaPointer) shaPointer = badSHA;
	packetData = packetContentPack(&length, "iss", temp, shaPointer, _file);
	logWrite("Checking [%d] [%s] [%s]\n", temp, shaPointer, _file);
	encoded.control    = PACKET_CONTROL_DAT;
	encoded.packetType = PACKET_TYPE_FILE_REQUEST;
	encoded.channel    = _channel;
	encoded.encrypt    = 0;
	packetEncode(__packetThreadData, &encoded, packetData, length);
	packetSend(__packetThreadData, &encoded);
	DEL(packetData);
}


static void fileShowDialog(void) {
	TagItemT uiFile[] = {
		T_START,
			T_WINDOW, O(_winFile),
				T_TITLE, P("Updating Cache"),
				T_WIDTH, 200, T_HEIGHT, 100,
				T_LABEL, O(_lblFile),
					T_X, 41, T_Y, 25,
					T_TITLE, P("Downloading..."),
				T_LABEL, T_DONE,
				T_TIMER, O(_timTimer),
					T_EVENT, P(timTimerTimeout),
					T_VALUE, 4 * 5,
					T_ENABLED, 1,
				T_TIMER, T_DONE,
			T_WINDOW, T_DONE,
		T_END
	};

	tagListRun(uiFile);
	_dialogVisible = 1;
}


static void fileShowError(char *message) {
	// ***TODO*** This should close all open windows.

	// Show error.  This is fatal.
	if (_dialogVisible) guiDelete(D(_winFile));
	netChannelRelease(_channel);
	msgBoxOne("Uh Oh!", MSGBOX_ICON_QUESTION, message, "Okay", btnMsgBoxOkay);
}


static void packetHandler(PacketDecodeDataT *packet) {
	FileResponseT      response   = 0;
	PacketEncodeDataT  encoded    = { 0 };
	char              *packetData = NULL;
	uint16_t           length     = 0;
	uint32_t           temp       = 0;

	if (packet->packetType == PACKET_TYPE_FILE_RESPONSE) {
		// Extract the response type.  We get more data later.
		packetContentUnpack(packet->data, "i", &response);
		switch (response) {
			case FILE_RESPONSE_UNKNOWN:
				logWrite("Unknown file transfer response.\n");
				fileShowError("Unknown file transfer response.");
				break;

			case FILE_RESPONSE_OKAY:
				// This file is already up-to-date, move on to next.
				logWrite("Got FILE_RESPONSE_OKAY\n");
				fileCheckNext();
				break;

			case FILE_RESPONSE_SEND:
				// Start of new file.  Get SHA and length.
				logWrite("Got FILE_RESPONSE_SEND\n");
				packetContentUnpack(packet->data, "iis", &response, &_currentLength, &_currentSha256);
				break;

			case FILE_RESPONSE_DATA:
				logWrite("Got FILE_RESPONSE_DATA\n");
				// Receive new file data.
				if (!_dialogVisible) fileShowDialog();
				// Are we starting a new file?
				if (_handle == NULL) {
					logWrite("Opening [%s]\n", _file);
					_handle = cacheFOpen(_file, "wb");
					if (!_handle) {
						fileShowError("Unable to write to cache.");
						break;
					}
				}
				// Write data to file, skipping first integer stored in packet.
				logWrite("Writing %d bytes to [%s]\n", packet->length - 4, _file);
				fwrite(&packet->data[4], packet->length - 4, 1, _handle);
				// Is this file complete?
				if (ftell(_handle) >= _currentLength) {
					logWrite("Closing [%s]\n", _file);
					// Close this file.
					cacheFClose(_handle);
					_handle = NULL;
					// Update cache entry to include SHA.
					cacheEntryAdd(_currentSha256, cacheEntryNameGet(_file), _file);
					// Next file!
					fileCheckNext();
				} else {
					logWrite("Sending FILE_REQUEST_NEXT\n");
					// Tell the server we got this bit of data, send the next.
					temp               = FILE_REQUEST_NEXT;
					packetData         = packetContentPack(&length, "i", temp);
					encoded.control    = PACKET_CONTROL_DAT;
					encoded.packetType = PACKET_TYPE_FILE_REQUEST;
					encoded.channel    = _channel;
					encoded.encrypt    = 0;
					packetEncode(__packetThreadData, &encoded, packetData, length);
					packetSend(__packetThreadData, &encoded);
					DEL(packetData);
				}
				// Reset timeout timer.
				timerReset(_timTimer);
				break;
		}
	}

	packetDecodeDataDestroy(&packet);
}


static void timTimerTimeout(WidgetT *widget) {
	uint8_t missing = 0;

	(void)widget;

	// Download timed out.
	timerStop(_timTimer);

	// If we at least have some version of the file, try to continue.
	while (arrlen(_fileList) > 0) {
		while (arrlen(_fileList[0]->files) > 0) {
			if (cacheFilenameGet(_fileList[0]->files[0])) {
				arrdel(_fileList[0]->files, 0);
			} else {
				missing = 1;
			}
		}
		// If we're good so far, call the callback.
		if (!missing) _fileList[0]->callback();
		arrfree(_fileList[0]->files);
		arrdel(_fileList, 0);
	}
	arrfree(_fileList);

	if (missing) fileShowError("Unable to download needed files.");
}
