/*
This program is distributed under the terms of the 'MIT license'. The text
of this licence follows...

Copyright (c) 2006,2007 J.D.Medhurst (a.k.a. Tixy)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/**
@file

@brief Command-line progran for Y-Modem file transfer.
*/

#include "../../common/common.h"
#include "../ymodem_tx.h"
#include "../ymodem_rx.h"


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


/**
@defgroup ymodem_test Test - Command line program incorporating Y-Modem source
@ingroup ymodem
@brief Program for transferring files over a serial port using the Y-Modem (or X-Modem) transmission protocol.

This acts as a test program for the Y-Modem implementation and is a useful utility in its own right.
It also supports receiving a 'log' after transmitting files; this can be used when sending a file
to a development board and receiving instrumentation output which comes back on same port.

<pre>
Usage: ymodem [OPTION]... FILE [DST-FILE]
  -s[y|x|k]       Send file.
                  If 'y' is specifieied Y-Modem protocol is used. (Default)
                  If 'x' is specifieied X-Modem protocol is used.
                  If 'k' is specifieied X-Modem 1K protocol is used.
  -r[y|g|x|c]     Receive file.
                  If 'y' is specifieied Y-Modem protocol is used. (Default)
                  If 'g' is specifieied Y-Modem G protocol is used.
                  If 'x' is specifieied X-Modem protocol is used.
                  If 'c' is specifieied X-Modem CRC protocol is used.
  -pPORT          Use communications port PORT.
  -bBAUD          Set baud rate for port to BAUD.
  -tTIMEOUT       Set timeout for transfer start (in seconds).
  -lfLOG          Capture log after transfer and write it to file LOG.
  -ltLOGTIMEOUT   Timeout for log capture (in seconds).
  -lxSTRING       End log capture when STRING is received.
  -le             Echo the captured log to STDOUT.

FILE is optional when receiving a file by Y-Modem: options -ry or -rg
DST-FILE is the name sent to receiver when using Y-Modem. Default is FILE.
</pre>

Program exit values:
-	0 (zero) if transfer succeeded.
-	1 (one) if the command line is invalid.
-	A negative value from SerialPort::Error, YModem::Error, YModemTx::TxError, YModemRx::RxError, or #Error.

@version 2007-01-17
	- Added <CODE>-le</CODE> option to echo the captured log to STDOUT.
@version 2007-05-28
	- Enabled neither send nor recieve to be specified, this enables the program to
	  be abused to just capture a log from a serial port.
@{
*/


#ifdef LINUX
#define DEFAULT_PORT 0							/**< Default value for #PortNum */
#else
#define DEFAULT_PORT 1
#endif
#define DEFAULT_BAUD 115200						/**< Default value for #PortBaud */
#define DEFAULT_TIMEOUT 30						/**< Default value (in seconds) for #Timeout */
#define DEFAULT_LOG_TIMEOUT 10					/**< Default value (in seconds) for #LogTimeout */
#define MAX_LOG_END_STRING_LENGTH 128			/**< Maximum length for #LogEndString */


const char* ProgramName;						/**< Name of this program */
unsigned PortNum = DEFAULT_PORT;				/**< Serial port number to use */
unsigned PortBaud = DEFAULT_BAUD;				/**< Baud rate for serial port */
unsigned Timeout = DEFAULT_TIMEOUT*1000;		/**< Timeout (in milliseconds) before aborting transfer */
bool XModemFlag = false;						/**< True for X-Modem, false for Y-Modem */
bool CrcFlag = false;							/**< True for X-Modem CRC mode */
bool KModeFlag = false;
bool GModeFlag = false;							/**< True for Y-Modem G mode */
bool SendFlag = false;							/**< True if sending file */
bool ReceiveFlag = false;						/**< True if receiving file */
const char* FileName = 0;						/**< Name of file to be transferred */
const char* DstFileName = 0;					/**< File name to send to receiver in Y-Modem transer */
const char* LogFileName = 0;					/**< Name of file for 'capture log' */
unsigned LogTimeout = DEFAULT_LOG_TIMEOUT*1000;	/**< Timeout (in milliseconds) before aborting 'capture log' */
const char* LogEndString = 0;					/**< String to terminate 'capture log' */
size_t LogEndStringLength = 0;					/**< Length of #LogEndString */
bool LogEcho = false;							/**< True if log should be echoed to STDOUT */


/**
Error values specifiec to this program.
*/
enum Error
	{
	ErrorNoMemory		= -500,					/**< Insufficient memory */
	ErrorLogFileError	= -501					/**< Error occured creating or writing to 'capture log' */
	};

#define STRINGIFY2(a) #a						/**< Helper for #STRINGIFY */
#define STRINGIFY(a) STRINGIFY2(a)				/**< Convert \a a into a quoted string */

/**
Display help message, then exit.
*/
void Help()
	{
	printf(	"Usage: %s [OPTION]... FILE [DST-FILE]\n"
			"Tranfer a file using X-Modem or Y-Modem protocols\n\n"
			"  -s[y|x|k]       Send file.\n"
			"                  If 'y' is specifieied Y-Modem protocol is used. (Default)\n"
			"                  If 'x' is specifieied X-Modem protocol is used.\n"
			"                  If 'k' is specifieied X-Modem 1K protocol is used.\n"
			"  -r[y|g|x|c]     Receive file.\n"
			"                  If 'y' is specifieied Y-Modem protocol is used. (Default)\n"
			"                  If 'g' is specifieied Y-Modem G protocol is used.\n"
			"                  If 'x' is specifieied X-Modem protocol is used.\n"
			"                  If 'c' is specifieied X-Modem CRC protocol is used.\n"
			"  -pPORT          Use communications port PORT. Default is " STRINGIFY(DEFAULT_PORT) "\n"
			"  -bBAUD          Set baud rate for port to BAUD. Default is " STRINGIFY(DEFAULT_BAUD) "\n"
			"  -tTIMEOUT       Set timeout for transfer start (in seconds). Default is " STRINGIFY(DEFAULT_TIMEOUT) "\n"
			"  -lfLOG          Capture log after transfer and write it to file LOG\n"
			"  -le             Echo the captured log to STDOUT.\n"
			"  -ltLOGTIMEOUT   Timeout for log capture (in seconds). Default is " STRINGIFY(DEFAULT_LOG_TIMEOUT) "\n"
			"  -lxSTRING       End log capture when STRING is received\n"
			"\n"
			"FILE is optional when receiving a file by Y-Modem: options -ry or -rg\n"
			"DST-FILE is the name sent to receiver when using Y-Modem. Default is FILE.\n"
			,ProgramName
			);
	exit(1);
	}


/**
Print message when a bad command line is detected.
Then do <code> exit(1); </code>.

@param format	Format string a la printf.
@param ...		Arguments for format sting, a la printf.
*/
void UsageError(const char *format, ...)
	{
	va_list args;
	va_start(args,format);
	vfprintf(stderr,format,args);
	va_end(args);
	fprintf(stderr,"\nTry '%s -h' for more information.\n",ProgramName);
	exit(1);
	}


/**
Print message when a error is detected.
Then do <code> exit(error); </code>.

@param error	The error number.
@param format	Format string a la printf.
@param ...		Arguments for format sting, a la printf.
*/
void Error(int error,const char *format, ...)
	{
	va_list args;
	va_start(args,format);
	vfprintf(stderr,format,args);
	va_end(args);
	if(error<0)
		fprintf(stderr,". Error %d",error);
	fprintf(stderr,"\n");
	exit(error);
	}


/**
@brief Parse an integer command-line argument.

This converts a decimal string into an unsigned integer.

@param argStr	The string to convert.
@param[out] arg	The value of the converted number.

@return True if conversion was successful, false otherwise.
*/
bool GetIntArg(const char* argStr, unsigned& arg)
	{
	char* argEnd;
	unsigned long val = strtoul(argStr,&argEnd,10);
	if(*argEnd)
		return false;
	arg = (unsigned)val;
	return true;
	}


/**
@brief Parse an command-line timeout argument.

This converts a decimal string representing a number of seconds
into an unsigned integer representing milliseconds.

@param argStr	The string to convert.
@param[out] arg	The value of the converted timeout.

@return True if conversion was successful, false otherwise.
*/
bool GetTimeoutArg(const char* argStr, unsigned& arg)
	{
	if(!GetIntArg(argStr,arg))
		return false;
	// convert to milliseconds...
	if(arg<(~(unsigned)0)/1000u)
		arg *= 1000;
	else
		arg = ~(unsigned)0;
	return true;
	}


/**
Parse all of the command-line arguments.

@param argc		The number of arguments.
@param argv		Array of argument strings.
*/
void ParseArgs(int argc, char** argv)
	{
	ProgramName = *argv;

	// parse arguments...
	while(--argc)
		{
		char* arg = *++argv;
		if(arg[0]=='"')
			{
			// remove "" from around argument...
			int len = strlen(arg);
			if(arg[len-1]=='"')
				{
				arg[len-1] = 0;
				++arg;
				*argv = arg;
				}
			}
		if(arg[0]!='-')
			{
			// arg doesn't start with '-' so it must be the name of the file...
			if(DstFileName)
				UsageError("Too many file arguments");
			if(FileName)
				DstFileName = arg;
			else
				FileName = arg;
			continue;
			}

		++arg; // skip initial '-'
		switch(*arg++)
			{
			case 'p':
				if(!GetIntArg(arg,PortNum))
					UsageError("Invalid PORT argument");
				break;

			case 'b':
				if(!GetIntArg(arg,PortBaud))
					UsageError("Invalid BAUD argument");
				break;

			case 't':
				if(!GetTimeoutArg(arg,Timeout))
					UsageError("Invalid TIMEOUT argument");
				break;

			case 'h':
				Help();

			case 's':
				SendFlag = true;
				XModemFlag = false;
				if(arg[0]!=0)
					{
					if(arg[1]!=0)
						goto invalid;
					if(arg[0]=='y')
						; // default
					else if(arg[0]=='x')
						XModemFlag = true, KModeFlag = false;
					else if(arg[0]=='k')
						XModemFlag = true, KModeFlag = true;
					else
						goto invalid;
					}
				break;

			case 'r':
				ReceiveFlag = true;
				XModemFlag = false;
				GModeFlag = false;
				if(arg[0]!=0)
					{
					if(arg[1]!=0)
						goto invalid;
					if(arg[0]=='y')
						; // default
					else if(arg[0]=='g')
						GModeFlag = true;
					else if(arg[0]=='x')
						CrcFlag = false, XModemFlag = true;
					else if(arg[0]=='c')
						CrcFlag = true, XModemFlag = true;
					else
						goto invalid;
					}
				break;

			case 'l':
				switch(*arg++)
					{
					case 'f':
						LogFileName = arg;
						break;
					case 'e':
						LogEcho = true;
						break;
					case 't':
						if(!GetTimeoutArg(arg,LogTimeout))
							UsageError("Invalid LOGTIMEOUT argument");
						break;
					case 'x':
						LogEndStringLength = strlen(arg);
						LogEndString = arg;
						if(LogEndStringLength>MAX_LOG_END_STRING_LENGTH)
							UsageError("String too long in -lx option. Max length is " STRINGIFY(MAX_LOG_END_STRING_LENGTH) );
						break;
					default:
						goto invalid;
					}
				break;

			default:
				goto invalid;
			}
		continue;
		}
	return;

invalid:
	UsageError("Invalid option '%s'",*argv);
	}


/**
Class for presenting a file as a input stream.
*/
class InFile : public YModemTx::InStream
	{
public:
	InFile()
		: File(0)
		{
		}

	/**
	Open stream for reading a file.

	@param fileName		Name of the file to read.

	@return Zero if successful, or a negative error value if failed.
	*/
	int Open(const char* fileName)
		{
		File = fopen(fileName,"rb");
		if(!File)
			return YModemTx::ErrorInputStreamError;
		// find file size...
		if(fseek(File,0,SEEK_END))
			{
			fclose(File);
			return YModemTx::ErrorInputStreamError;
			}
		TotalSize = ftell(File);
		if(fseek(File,0,SEEK_SET))
			{
			fclose(File);
			return YModemTx::ErrorInputStreamError;
			}
		TransferredSize = 0;
		return 0;
		}

	/**
	Close the stream.
	*/
	void Close()
		{
		if(File)
			{
			fclose(File);
			File = 0;
			}
		}

	/**
	Return the size of the file.

	@return File size.
	*/
	inline size_t Size()
		{
		return TotalSize;
		}

	/**
	Read data from the stream.

	@param[out] data	Pointer to buffer to hold data read from stream.
	@param size			Maximum size of data to read.

	@return Zero if successful, or a negative error value if failed.
	*/
	int In(uint8_t* data, size_t size)
		{
		int percent = TotalSize ? ((uint64_t)TransferredSize*(uint64_t)100)/(uint64_t)TotalSize : 0;
		printf("%8d bytes of %d - %3d%%\r",TransferredSize, TotalSize, percent);
		fflush(stdout);
		size=fread(data,sizeof(uint8_t),size,File);
		if(size)
			{
			TransferredSize += size;
			return size;
			}
		if(TransferredSize!=TotalSize)
			return YModemTx::ErrorInputStreamError;
		return 0;
		}
private:
	FILE* File;
	size_t TotalSize;
	size_t TransferredSize;
	};


/**
Class for presenting a file as a output stream.
*/
class OutFile : public YModemRx::OutStream
	{
public:
	OutFile()
		: File(0)
		{
		}

	/**
	Open stream for writing to a file.

	If no data has yet been written to the stream, this function may be
	called a second time in order to update the file size.

	@param fileName		Name of the file to write.
	@param size			Size of data being written to a file.

	@return Zero if successful, or a negative error value if failed.
	*/
	int Open(const char* fileName, size_t size)
		{
		if(File)
			{
			if(TransferredSize)
				return YModemRx::ErrorOutputStreamError;
			}
		else
			{
			File = fopen(fileName,"w+b");
			if(!File)
				return YModemRx::ErrorOutputStreamError;
			printf("Receiving %s...\n",fileName);
			}
		TotalSize = size;
		TransferredSize = 0;
		return 0;
		}

	/**
	Close the stream.
	*/
	void Close()
		{
		if(File)
			{
			fclose(File);
			File = 0;
			}
		}

	/**
	Write data to the stream.

	@param data		Pointer to data to write.
	@param size		Size of data.

	@return Zero if successful, or a negative error value if failed.
	*/
	int Out(const uint8_t* data, size_t size)
		{
		if(!File)
			return YModemRx::ErrorOutputStreamError; // we've not been Open()ed
		// show progress...
		if(TotalSize)
			{
			int percent = TotalSize ? ((uint64_t)TransferredSize*(uint64_t)100)/(uint64_t)TotalSize : 0;
			printf("%8d bytes of %d - %3d%%\r",TransferredSize, TotalSize, percent);
			fflush(stdout);
			}
		else
			{
			printf("%8d bytes\r",TransferredSize);
			fflush(stdout);
			}
		// limit data to size of file...
		if(TotalSize)
			{
			size_t maxSize = TotalSize-TransferredSize;
			if(size>maxSize)
				size = maxSize;
			if(!maxSize)
				return 0; // end
			}
		// write data...
		if(size!=fwrite(data,sizeof(uint8_t),size,File))
			return YModemRx::ErrorOutputStreamError; // write failed
		TransferredSize += size;
		return size;
		}
private:
	FILE* File;
	size_t TotalSize;
	size_t TransferredSize;
	};


FILE* LogFile = 0;	/**< Handle of the 'capture log' file */

/**
Capture data from the serial port and write it to the 'capture log' file.
Log capture ends either when no data has been received for #LogTimeout milliseconds,
or when characters matching #LogEndString are found.

@param port		The serial port to receive data from.
*/
void CaptureLog(SerialPort* port)
	{
	printf("Capturing log to %s...\n",LogFileName);
	fflush(stdout);
	uint8_t logBuffer[1024+MAX_LOG_END_STRING_LENGTH];
	size_t bufferOffset = 0;
	for(;;)
		{
		uint8_t* start = logBuffer+bufferOffset;
		uint8_t* end = logBuffer+sizeof(logBuffer);
		size_t length = end-start;
		int result = port->In(start,length,LogTimeout);
		if(!result)
			{
			fclose(LogFile);
			printf("Log capture timeout.\n");
			return;
			}
		if(result<0)
			{
			fclose(LogFile);
			Error(result,"Error during log capture");
			}

		// save data to log...
		length = result;
		end = start+length;
		if(fwrite(start,1,length,LogFile)!=length)
			{
			fclose(LogFile);
			Error(ErrorLogFileError,"File error during log capture");
			}
		fflush(LogFile);

		if(LogEcho)
			{
#ifndef WIN32
			fwrite(start,1,length,stdout);
#else
			// Hack for windows because it expands LF character to CR+LF
			for(size_t i=0; i<length; ++i)
				if(start[i]!=13) // ignore CR characters
					fwrite(start+i,1,1,stdout);
#endif
			fflush(stdout);
			}

		// check for terminating string...
		if(!LogEndStringLength)
			continue;
		uint8_t* endString = (uint8_t*)LogEndString;
		uint8_t firstEndChar = endString[0];
		size_t endStringLength = LogEndStringLength;
		uint8_t* scanEnd = end-endStringLength;
		uint8_t* scan = logBuffer;
		while(scan<=scanEnd)
			{
match_next:	if(*scan++!=firstEndChar)
				continue;

			uint8_t* ptr = scan;
			size_t endStringMatch = 0;
			while(++endStringMatch<endStringLength)
				if(*ptr++!=endString[endStringMatch])
					goto match_next;

			fclose(LogFile);
			printf("Log capture termination string found.\n");
			return;
			}
		bufferOffset = end-scan;
		memcpy(logBuffer,scan,bufferOffset);
		}
	}


/**
Send the file.

@param port	The serial port to use.
*/
void Send(SerialPort* port)
	{
	if(!FileName)
		UsageError("File argument missing");

	if(!DstFileName)
		DstFileName = FileName;

	InFile source;
	int error = source.Open(FileName);
	if(error)
		Error(error,"Can't open file '%s'",FileName);

	printf("Sending %s...\n",FileName);
	YModemTx ymodem(*port);
	if(XModemFlag)
		error = ymodem.SendX(source,Timeout,KModeFlag);
	else
		error = ymodem.SendY(DstFileName,source.Size(),source,Timeout);
	if(error)
		Error(error,"Error during file transfer");

	printf("\nSent OK\n");
	source.Close();
	}


/**
Receive a file.

@param port	The serial port to use.
*/
void Receive(SerialPort* port)
	{
	OutFile destination;
	YModemRx ymodem(*port);
	int error;
	if(XModemFlag)
		{
		if(!FileName)
			UsageError("File argument missing");
		error = destination.Open(FileName,0);
		if(error)
			Error(error,"Can't create file '%s'",FileName);
		error = ymodem.ReceiveX(destination,Timeout,CrcFlag);
		}
	else
		{
		if(FileName)
			{
			error = destination.Open(FileName,0);
			if(error)
				Error(error,"Can't create file '%s'",FileName);
			}
		error = ymodem.ReceiveY(destination,Timeout,GModeFlag);
		}
	if(error)
		Error(error,"Error during file transfer");

	printf("\nReceived OK\n");
	destination.Close();
	}


/**
Main function.

@param argc		The number of arguments.
@param argv		Array of argument strings.

@return Zero if transfer succeeded.
*/
int main(int argc, char** argv)
	{
	ParseArgs(argc,argv);

	SerialPort* port = SerialPort::New();
	if(!port)
		Error(ErrorNoMemory,"Can't create port");

	int error = port->Open(PortNum);
	if(error)
		Error(error,"Can't open port");

	error = port->Initialise(PortBaud);
	if(error)
		Error(error,"Can't initialise port");

	if(ReceiveFlag)
		Receive(port);
	else
		{
		if(LogFileName)
			{
			LogFile = fopen(LogFileName,"w+b");
			if(!LogFile)
				Error(ErrorLogFileError,"Can't create LogFile file '%s'",LogFileName);
			}
		if(SendFlag || FileName)
			Send(port);
		if(LogFileName)
			CaptureLog(port);
		}

	port->Close();
	delete port;

	return 0;
	}


/** @} */ // End of group

