#include "WimpLib:XFer.h"

#include <stdio.h>
#include <string.h>

#include "swis.h"

#include "WimpLib:Exception.h"
#include "WimpLib:File.h"
#include "WimpLib:mem.h"
#include "WimpLib:Task.h"
#include "WimpLib:Utils.h"
#include "WimpLib:Log.h"

const _kernel_oserror XFer_Error_EOF = {1, "XFerEOF:Unexpected end of file."};
static _kernel_oserror XFer_Abort = {1, "XFerAbort:RAM transfer aborted"};
static const char* wimp_scrap_file = "<Wimp$Scrap>";

/*
 * Structure for data transfer
 */

struct XFer
{
	bool                   asfileonly;
	bool                   bInSendLoop;
	bool                   bTransferOk;
	FILE*                  file;
	// RAM transfer
	HTask                  task;
	int                    msgref;
	int                    msgtype;
	char*                  pbuffer;
	char*                  pbufend;
	char*                  pfillpos;
	sXFer_Chunk            chunk;
	file_type              type;
	int                    currentpos;
	int                    totalsize;
	XFer_SendProc          FNSend;
	XFer_RemoveProc        FNRemove;
	XFer_OnSafeProc        FNOnSafe;
	XFer_PrintProc         FNPrint;
	XFer_OnErrorProc       FNOnError;
	void*                  handle;
	// Error handling
	const _kernel_oserror* err;
};

/*-----------------------*
 *- Send data protocols -*
 *-----------------------*/

/*
 * Send data to a file
 */

const _kernel_oserror* XFer_SendToFile
		( const char* filename
		, file_type type
		, XFer_SendProc FNsend
		, XFer_OnSafeProc FNonsafe
		, void* handle
		)
{
	const _kernel_oserror* err = NULL;
	FILE* file;
	sXFer_Chunk chunk;

	// create typed empty file, filename, type
	err = _swix(OS_File, _INR(0,5), 11, filename, type, 0, 0, 0);
	if (err) return err;

	// open file
	if ((file = fopen(filename, "wb")) == NULL)
		return Task_LastOSError();

	// Call the callback for writing data
	chunk.pdata = NULL;
	chunk.size = 0;
	chunk.ref = 0;
	chunk.filename = mem_allocstring(filename);
	while((err == NULL) && FNsend(handle, type, &chunk))
	{
		if (chunk.size > 0)
		{
			// Write n bytes to a file
			if (fwrite(chunk.pdata, 1, chunk.size, file) < chunk.size)
				err = Task_LastOSError();
		}
	}

	// close the file, could report error due to buffer still to write
	if (fclose(file) && (err == NULL))
		err = Task_LastOSError();

	// If transfer was completed correctly, set the correct file type
	if (!err) File_SetFileType(filename, type);

	/*
	 * If transfer was completed correctly, call on safe callback.
	 * Such a callback will update the document filename and modif status.
	 */
	if ((err == NULL)
	&&  (FNonsafe != NULL))
		FNonsafe(handle, filename);

	mem_free(chunk.filename);

	return err;
}

/*
 * Wait for reply of message DataLoad and possible following RAMTransmit ones
 */

static EListenerAction XFer_Send_Handler(void* pHandle, const Event* pe)
{
	const _kernel_oserror* err = NULL;
	XFer* pxfer = pHandle;
	bool bDone = false;
	bool bSaved = false;
	const Msg* reply = pe->pData;

	// We are only interested in replies or acks to our messages
	switch (pe->Type)
	{
		case EEvent_Message:
		case EEvent_MessageWantAck:
		{
			if (reply->hdr.ref != pxfer->msgref) // reply
				return EListenerAction_ContinueEvent;
		}
		break;
		case EEvent_MessageAck:
		{
			if ((reply->hdr.id != pxfer->msgref)   // msg bouncing
			&&  (reply->hdr.ref != pxfer->msgref)) // reply
				return EListenerAction_ContinueEvent;
		}
		break;
		default:
			return EListenerAction_ContinueEvent;
	}

	// message type we expect a reply
	switch (pxfer->msgtype)
	{
		case EMsg_DataLoad:
		{
			Msg_FileData* pmsg = (Msg_FileData*) reply;

			if (pmsg->hdr.id == pxfer->msgref)   // msg bouncing
			{
				/* It looks as if he hasn't acknowledged my DataLoad acknowledge:
				   thus it may be a loose scrap file, and must be deleted. */
				_swix(OS_File, _INR(0,1), 6, &pmsg->name[0]);
			}
			// else DataLoadAck or strange reply

			bDone = true;
			bSaved = true;
		}
		break;
		case EMsg_DataSave:
		{
			Msg_FileData* pmsg = (Msg_FileData*) reply;

			if (pmsg->hdr.id == pxfer->msgref)   // msg bouncing
			{
				bDone = true;
				bSaved = false;
				break;
			}

			if (pmsg->hdr.action == EMsg_DataSaveAck)
			{
				// Remove Safe callback if transfer is unsafe
				if (pmsg->size == -1) pxfer->FNOnSafe = NULL;

				// He wants a transfer to a file
				err = XFer_SendToFile
							( &pmsg->name[0]
							, pxfer->type
							, pxfer->FNSend
							, pxfer->FNOnSafe
							, pxfer->handle
							);

				if (err == NULL)
				{
					Msg_FileData msg;

					if (pxfer->FNRemove) pxfer->FNRemove(pxfer->handle);

					// Save occured correctly so we need to reply with a DataLoad
					msg = *pmsg;
					msg.hdr.ref = pmsg->hdr.id;
					msg.hdr.action = EMsg_DataLoad;
					msg.type = pxfer->type;

					err = Task_PostMsg((Msg*) &msg, reply->hdr.task);

					pxfer->msgref = msg.hdr.id;
					pxfer->msgtype = msg.hdr.action;
				}
				break;
			}
			else if (pmsg->hdr.action != EMsg_RAMFetch)
			{
				// Get strange message as reply, dunno what to do
				bDone = true;
				bSaved = false;
				break;
			}
			// RAMFetch
			if (pxfer->asfileonly)
			{
				// don't do anything, wait for the EMsg_DataSaveAck that will follow
				break;
			}

			pxfer->chunk.pdata = NULL;
			pxfer->chunk.size = 0;
			pxfer->chunk.ref = 0;
			pxfer->chunk.filename = NULL;
		}
		// no break
		case EMsg_RAMTransmit:
		{
			Msg_RAMData* pmsg = (Msg_RAMData*) reply;
			int send;

			if (pmsg->hdr.action != EMsg_RAMFetch)
			{
				// Abort, but report no error
				bDone = true;
				bSaved = false;
				break;
			}

			// Store info required by the data transfer
			pxfer->task = pmsg->hdr.task;
			pxfer->msgref = pmsg->hdr.id;
			pxfer->pbuffer = pmsg->buffer;
			pxfer->pbufend = pxfer->pbuffer + pmsg->len;
			pxfer->pfillpos = pxfer->pbuffer;

			// Fill data in buffer
			while (pxfer->pfillpos < pxfer->pbufend)
			{
				// if data chunk is empty, get next one, end loop if none
				if (pxfer->chunk.size <= 0)
				{
					if (!pxfer->FNSend(pxfer->handle, pxfer->type, &pxfer->chunk))
						break;
				}
				if (pxfer->chunk.size <= 0) continue;

				// How many bytes will we fill into the buffer?
				send = pxfer->pbufend - pxfer->pfillpos;
				if (send > pxfer->chunk.size) send = pxfer->chunk.size;

				// Fill the buffer
				// "XWimp_TransferBlock", my_task, pos, send, is_task, pbufpos
				err = _swix( Wimp_TransferBlock, _INR(0,4)
				           , Task_GetId(), pxfer->chunk.pdata, pxfer->task, pxfer->pfillpos, send);
				if (err) break;

				pxfer->pfillpos += send;
				pxfer->chunk.pdata += send;
				pxfer->chunk.size -= send;
			}

			if (err == NULL)
			{
				// ack transfer and wait for next ramfech
				Msg_RAMData msg;

				// Send RAMTransmit message
				msg.hdr.size = 28;
				msg.hdr.ref = pxfer->msgref;
				msg.hdr.action = EMsg_RAMTransmit;
				msg.buffer = pxfer->pbuffer;
				msg.len = pxfer->pfillpos - pxfer->pbuffer;

				if (pxfer->pfillpos < pxfer->pbufend)
				{
					// Buffer not filled, it is the last one, so send without ack and exit
					err = Task_SendMsg((Msg*) &msg, pxfer->task);
					if ((err == NULL) && (pxfer->FNRemove)) pxfer->FNRemove(pxfer->handle);
					bDone = true;
					bSaved = true;
				}
				else
				{
					// Buffer filled, send and ask for a reply
					err = Task_PostMsg((Msg*) &msg, pxfer->task);
				}

				pxfer->msgref = msg.hdr.id;
				pxfer->msgtype = msg.hdr.action;
			}
		}
		break;
	}

	if (err != NULL)
	{
		if (pxfer->FNOnError) pxfer->FNOnError(err);
		bDone = true;
		bSaved = false;
	}

	if (bDone)
	{
		pxfer->bInSendLoop = false;
		pxfer->bTransferOk = bSaved;
		Task_DeRegisterEventHandler(XFer_Send_Handler, pHandle);
	}

	return EListenerAction_StopEvent;
}

/*
 * Send data to receiver task or window on return of drag, drag and drop or for clipboard protocol
 */

bool XFer_Send
		( unsigned int msg_ref
		, HTask task
		, HWind w
		, HIcon i
		, int x
		, int y
		, int estsize
		, file_type type
		, const char* filename
		, XFer_SendProc FNSend
		, XFer_RemoveProc FNRemove
		, XFer_OnSafeProc FNOnSafe
		, XFer_PrintProc FNPrint
		, XFer_OnErrorProc FNOnError
		, bool asfileonly
		, void* handle
		)
{
	const _kernel_oserror* err;
	Msg_FileData msg;
	XFer* volatile pxfer = NULL;
	bool bTransferOk = false;

	if (!FNOnError) FNOnError = Task_ReportOSError;

	try
	{
		pxfer = throw_mem_alloc(sizeof(XFer));

		// Send a DataSave message to the receiver task
		msg.hdr.size = 44;
		msg.hdr.ref = msg_ref;
		msg.hdr.action = EMsg_DataSave;
		msg.pos.w = w;
		msg.pos.i = i;
		msg.pos.pt.x = x;
		msg.pos.pt.y = y;
		msg.size = estsize;
		msg.type = type;
		snprintf(msg.name, sizeof(msg.name), "%s", File_GetLeafName(filename));
		msg.hdr.size += strlen(msg.name) + 1;

		if (msg_ref)
			err = Task_PostMsg((Msg*) &msg, task);
		else
			err = Task_WPostMsg((Msg*) &msg, w, i);

		if (err) throw_os(err);

		// Someone accepts the transfer
		pxfer->task = task;
		pxfer->msgref = msg.hdr.id;
		pxfer->msgtype = msg.hdr.action;
		pxfer->type = type;
		pxfer->FNSend = FNSend;
		pxfer->FNRemove = FNRemove;
		pxfer->FNOnSafe = FNOnSafe;
		pxfer->FNPrint = FNPrint;
		pxfer->FNOnError = FNOnError;
		pxfer->asfileonly = asfileonly;
		pxfer->handle = handle;

		// Add an event handler to deal with further exchanges
		throw_Task_RegisterEventHandler(XFer_Send_Handler, pxfer, true);

		// Loop until transfer is completed
		pxfer->bInSendLoop = true;
		pxfer->bTransferOk = false;

		while (pxfer->bInSendLoop == true)
		{
			Task_ProcessEvent(Task_PollIdle(Task_GetPollMask(), Task_GetPollTimeStep()));
		}
		bTransferOk = pxfer->bTransferOk;
	}
	catch
	{
		FNOnError(&exception_current()->error);
		bTransferOk = false;
	}
	catch_end

	mem_free(pxfer);

	return bTransferOk;
}

/*--------------------------*
 *- Receive data protocols -*
 *--------------------------*/

/*
 * Receive a data packet from sender task by ram transfer
 */

static bool XFer_ReceivePacket(XFer* pxfer)
{
	Msg_RAMData msg;
	Msg_RAMData* reply;

	if (pxfer->msgref == 0)
	{
		pxfer->pbufend = pxfer->pbuffer;
		pxfer->err = &XFer_Error_EOF;
		return true;
	}

	// Send RAMFetch message
	msg.hdr.size = 28;
	msg.hdr.ref = pxfer->msgref;
	msg.hdr.action = EMsg_RAMFetch;
	msg.buffer = pxfer->pbuffer;
	msg.len = pxfer->pbufend - pxfer->pbuffer;

	// Send and ask for a reply
	reply = (Msg_RAMData*) Task_PostAndWaitForReply((Msg*) &msg, pxfer->task);

	if ((reply != NULL)
	&&  (reply->hdr.action == EMsg_RAMTransmit)
	&&  (reply->buffer == pxfer->pbuffer)
	&&  (reply->len <= msg.len))
	{
		// Well, we received another data packet
		pxfer->task = reply->hdr.task;
		pxfer->msgref = reply->hdr.id;
		pxfer->pbufend = pxfer->pbuffer + reply->len;

		// Check if more chunks to load
		if (reply->len < msg.len)
			pxfer->msgref = 0;
		return true;
	}

	// Incorrect or no reply
	pxfer->err = &XFer_Abort;
	return false;
}

/*
 * Receive data from receiver task by ram transfer
 */

static const _kernel_oserror* XFer_ReceiveFromRam
		( const Msg_RAMData* rcv
		, char* pbuffer
		, int bufsize
		, int type
		, const ScreenPos* ppos
		, XFer_ReceiveProc FNreceive
		, void* handle
		)
{
	XFer xfer;

	// Store info required by the data transfer
	xfer.task = rcv->hdr.task;
	xfer.msgref = rcv->hdr.id;
	xfer.pbuffer = pbuffer;
	xfer.pbufend = pbuffer + rcv->len;
	xfer.pfillpos = xfer.pbuffer;
	xfer.file = NULL;
	xfer.err = NULL;
	xfer.currentpos = 0;
	xfer.totalsize = -1;

	// Check if more chunks to load
	if  (rcv->len < bufsize)
		xfer.msgref = 0;

	// Call the callback for transfering data
	xfer.err = FNreceive(handle, &xfer, type, ppos);

	return xfer.err;
}

/*
 * Reveive data from file
 */

const _kernel_oserror* XFer_ReceiveFromFile
		( const char* filename
		, XFer_ReceiveProc FNreceive
		, void* handle
		, const ScreenPos* ppos
		)
{
	XFer xfer;
	File_Info info;

	// Store info required by the file load
	xfer.task = (HTask) 0;
	xfer.msgref = 0;
	xfer.pbuffer = NULL;
	xfer.pbufend = NULL;
	xfer.pfillpos = NULL;
	xfer.err = NULL;

	// open file
	if ((xfer.file = fopen(filename, "rb")) == NULL)
	{
		xfer.err = Task_LastOSError();
		Log("Failed to open %s", filename);
		return xfer.err;
	}

	info = File_GetInfo(filename);
	xfer.currentpos = 0;
	xfer.totalsize = info.size;

	// Call the callback for writing data
	xfer.err = FNreceive(handle, &xfer, File_GetFileType(filename), ppos);

	// close the file
	fclose(xfer.file);

	return xfer.err;
}

/*
 * Use in Receive callback to read n bytes of data
 */

int XFer_ReceiveRead(XFer* pxfer, void* pdata, int size)
{
	int read;

	if (pxfer->pbuffer == NULL)
	{
		// Read n bytes from a file
		read = fread(pdata, 1, size, pxfer->file);
		pxfer->currentpos += read;
		if (read < size)
		{
			if (feof(pxfer->file))
				pxfer->err = &XFer_Error_EOF;
			else
			{
				Log("Failed to read some data");
				pxfer->err = Task_LastOSError();
			}
			return read;
		}
	}
	else
	{
		// Read n bytes from sender task
		char* pos = pdata;
		int len = size;

		// Fill data in buffer
		while(len > 0)
		{
			// If buffer is clear, ask a next ramtransmit
			if (pxfer->pfillpos >= pxfer->pbufend)
			{
				if ((pxfer->err == &XFer_Error_EOF) || !XFer_ReceivePacket(pxfer))
					return size - len; // Transfer cancelled

				// buffer is full again
				pxfer->pfillpos = pxfer->pbuffer;
			}

			// How many bytes will we read from buffer ?
			read = pxfer->pbufend - pxfer->pfillpos;
			if (read > len) read = len;

			// read from the buffer
			memcpy(pos, pxfer->pfillpos, read);

			// Move to next packet to read
			pos += read;
			len -= read;
			pxfer->pfillpos += read;
			pxfer->currentpos += read;
		}
	}

	return size;
}

/*
 * Receive data from sender task drag, drag and drop or from clipboard protocol
 */

#define rcv_buff_len 32*1024

static const _kernel_oserror Task_Error_NoMem = {0x40000001, "NoMem:Not enough memory."};

const _kernel_oserror* XFer_Receive(const Msg* rcv, XFer_ReceiveProc FNreceive, void* handle)
{
	Msg_FileData refrcv = *((const Msg_FileData*) rcv);
	Msg msg;
	const _kernel_oserror* err;
	bool bScrap = false;

	if (refrcv.hdr.action == EMsg_DataSave)
	{
		char* pbuffer = mem_alloc(rcv_buff_len);
		if (!pbuffer) return &Task_Error_NoMem;

		{
			Msg_RAMData* prmsg = (Msg_RAMData*) &msg;
			Msg_RAMData* reply;

			// Send a RAMFetch message and wait for reply
			prmsg->hdr.size = 28;
			prmsg->hdr.ref = refrcv.hdr.id;
			prmsg->hdr.action = EMsg_RAMFetch;
			prmsg->buffer = pbuffer;
			prmsg->len = rcv_buff_len;

			reply = (Msg_RAMData*) Task_PostAndWaitForReply(&msg, refrcv.hdr.task);

			if ((reply != NULL)
			&&  (reply->hdr.action == EMsg_RAMTransmit))
			{
				// ram transfer accepted
				if ((reply->buffer == pbuffer)
				&&  (reply->len <= prmsg->len))
				{
					// ram transfer accepted and correct until now
					err = XFer_ReceiveFromRam
								( reply
								, pbuffer
								, rcv_buff_len
								, refrcv.type
								, &refrcv.pos
								, FNreceive
								, handle
								);
				}
				else
					err = &XFer_Abort;

				// ram transfer completed
				mem_free(pbuffer);
				return err;
			}
		}

		mem_free(pbuffer);

		{
			// No ram transfer, try transfer through Wimp$Scrap
			Msg_FileData* pfmsg = (Msg_FileData*) &msg;

			bScrap = true;
			pfmsg->hdr.size = 44;
			pfmsg->hdr.ref = refrcv.hdr.id;
			pfmsg->hdr.action = EMsg_DataSaveAck;
			pfmsg->pos = refrcv.pos;
			pfmsg->size = -1;
			pfmsg->type = refrcv.type;
			snprintf(pfmsg->name, sizeof(pfmsg->name), "%s", wimp_scrap_file);
			pfmsg->hdr.size = pfmsg->hdr.size + strlen(pfmsg->name) + 1;

			// Send and wait for the DataLoad reply
			rcv = Task_PostAndWaitForReply(&msg, refrcv.hdr.task);
			if (rcv == NULL) return XFer_NoTransfer;
		}
	}

	if ((rcv->hdr.action != EMsg_DataLoad)
	&&  (rcv->hdr.action != EMsg_DataOpen))
		return XFer_NoTransfer;

	{
		const Msg_FileData* pfmsg = (const Msg_FileData*) rcv;

		err = XFer_ReceiveFromFile
					( pfmsg->name
					, FNreceive
					, handle
					, &pfmsg->pos
					);
	}

	if (bScrap)
	{
		// Remove scrap file
		_swix(OS_File, _INR(0,1), 6, wimp_scrap_file);
	}

	if (err == NULL)
	{
		// Send a DataLoadAck message to the sender task
		msg = *rcv;
		msg.hdr.ref = rcv->hdr.id;
		msg.hdr.action = EMsg_DataLoadAck;

		err = Task_SendMsg(&msg, rcv->hdr.task);
	}

	return err;
}

const _kernel_oserror* XFer_GetErrorStatus(const XFer* pxfer)
{
	return pxfer->err;
}

int XFer_GetEstimatedTotalSize(const XFer* pxfer)
{
	return pxfer->totalsize;
}

int XFer_CountReceived(const XFer* pxfer)
{
	return pxfer->currentpos;
}
