#include <pthread.h>
#include <assert.h>
#include <getopt.h>
#include "common.h"
#include <signal.h>
#include <unistd.h>

const char *USAGE="client [-v | -V logfile ] [ -k keylog ] [-c context_list] [-s server] [-m middlebox_list | -M middlebox_list_file]\n"
	"\tcontext list must be comma-separated list of context names, defaults to \"header,body\"\n"
	"\tserver must be url including port, default 127.0.0.1:" xstr(DEFAULT_SERVER_PORT) "\n"
	"\tmiddlebox list must semi-colon delimited urls including port,"
	"\tmiddlebox list_file must be a file with middlebox urls on separate lines,"
	"\t\tdefault \"127.0.0.1:" xstr(DEFAULT_MBOX_PORT) "\n"
	"Input to send to the server is taken from stdin in lines of the form context:command\n";

const char *ARG_TYPES="v:V:k:c:s:m:M";

typedef struct ClientConfigST {
	FILE *vlog;
	FILE *fIn;
	FILE *fOut;
	SSL *pSSL;
	int numContexts;
	const char *contextDescs[50];
	int numMiddleboxes;
	const char *MiddleboxURLs[50];
	const char *serverURL;
	char myKeyFile[1024];
	char myPassword[1024];
	char caFile[1024];
} ClientConfig;

static int
client_ParseArgs(int iArgC, char *papsArgV[], ClientConfig *ptCConf);

void
client_SetDefaults(ClientConfig *ptC)
{
	memset(ptC,0,sizeof(*ptC));
	ptC->fIn=stdin;
	ptC->fOut=stdout;
	strcpy(ptC->caFile,"ca.pem");
	strcpy(ptC->myKeyFile,"client.pem");
	strcpy(ptC->myPassword,"password");
	ptC->serverURL="127.0.0.1:" xstr(DEFAULT_SERVER_PORT);
	ptC->numMiddleboxes=1;
	ptC->MiddleboxURLs[0]="127.0.0.1:" xstr(DEFAULT_MBOX_PORT);
}

extern char *optarg;
//the next two are available but currently ununsed,
//commented out to prevent compiler complaining
//extern int optind;
//extern int optopt;
static int
client_ParseArgs(int iArgC, char *papsArgV[], ClientConfig *ptCConf)
{
	int iStatus=0;
	char carg=0;

	while( iStatus==0 && (carg=getopt(iArgC,papsArgV,ARG_TYPES)) != -1) {
		switch(carg) {
		case 'v':
		case 'V':
			if(ptCConf->vlog!=NULL) {
				COMMON_LogErrorAndExit(iStatus=INVALID_COMMAND_LINE_ARGS,stderr,"verbose logging specified multiple times\n%s",USAGE);
			}
			if(carg=='v') {
				ptCConf->vlog=stdout;
			} else { /*carg=='V'*/
				ptCConf->vlog=fopen(optarg,"w");
				if(ptCConf->vlog==NULL) {
					COMMON_LogErrorAndExit(iStatus=FILE_NOT_WRITABLE,stderr,"Unable to open log file %s",optarg);
				}
			}
			break;
		case 's':
			/*todo*/
			assert(0);
			break;
		case 'k':
			/*todo*/
			assert(0);
			break;
		case 'c':
			/*todo*/
			assert(0);
			break;
		case 'm':
			/*todo*/
			assert(0);
			break;
		case 'M':
			/*todo*/
			assert(0);
			break;
		default:
			COMMON_LogErrorAndExit(iStatus=INVALID_COMMAND_LINE_ARGS,stderr,"Unrecognised command line option -%c",carg);
			break; /*never reached - keeps compiler happy*/
		}
	}

	return iStatus;
}

ERROR_STATUS
client_CreateContext(SSL_CTX **pptSSLctx,const ClientConfig *ptConf)
{
	ERROR_STATUS iStatus;
	iStatus=COMMON_InitializeSSLCtx(pptSSLctx,
			ptConf->myKeyFile, ptConf->myPassword,
			ptConf->caFile,
			ID_CLIENT);
	return iStatus;

}

ERROR_STATUS
client_Connect(SSL_CTX *ptSSLctx, ClientConfig *ptConf)
{
	ERROR_STATUS iStatus=SUCCESS;
	int i,j,p;
	BIO *ptSBio;

	/*step 0 - create SSL structure*/
	/*the SSL_CTX already has out keys set up*/
	ptConf->pSSL=SSL_new(ptSSLctx);
	/*todo - check for NULL return*/
	assert(ptConf->pSSL!=NULL);

	/*step 1 - create the contexts*/
	iStatus=COMMON_InitMulticontextSet(ptConf->pSSL);
	assert(iStatus==SUCCESS);
	if(ptConf->numContexts==0) {
		iStatus=COMMON_AppendContext(ptConf->pSSL,"default");
	} else {
		/*Iterate through the context descriptions and create each one*/
		for(i=0;i!=ptConf->numContexts && iStatus==SUCCESS;i++) {
			iStatus=COMMON_AppendContext(ptConf->pSSL,ptConf->contextDescs[i]);
		}

	}
	if(iStatus!=SUCCESS) {
		/*TODO*/
		assert(0);
	}

	/*step 2 - create the middlebox list*/
	iStatus=COMMON_InitProxySet(ptConf->pSSL);
	assert(iStatus==SUCCESS);
	/*step 2.0? - do we need to insert client as middlebox 0? - TODO confirm*/
	/*step 2.1 - iterate through middlebox list */
	for(i=0;i!=ptConf->numMiddleboxes;i++) {
		iStatus=COMMON_AppendProxy(ptConf->pSSL,ptConf->MiddleboxURLs[i]);
	}
	/*step 2.2 - add server url*/
	iStatus=COMMON_AppendClientServer(ptConf->pSSL,ptConf->serverURL,1);

	/*step 3 - set the middlebox access permissions*/
	/*for demonstration purposed, we will set by the following rule:
	 * Middleboxes with an even port number get read-only access
	 * Middleboxes with an odd port number get write access
	 * This is for demonstration only - there is no sensible
	 * reason why this rule would ever be real-world implemented!
	 */
	for(j=0;j!=ptConf->numContexts;j++) {
		/*grant client if included*/
		/*grant server*/
		iStatus=COMMON_SetProxyAccessPermissionByID(ptConf->pSSL,ptConf->pSSL->slices[j]->slice_id, ptConf->numMiddleboxes-1,
				1,1);
		for(i=0;i!=ptConf->numMiddleboxes-1;i++) { /*change to i=1 if client included*/
			char *sMBoxUrl=ptConf->pSSL->proxies[i]->address;
			char *sPort=strchr(sMBoxUrl,':');
			assert(sPort);
			sPort++;
			int iPort=atoi(sPort);
			iStatus=COMMON_SetProxyAccessPermissionByID(ptConf->pSSL,ptConf->pSSL->slices[j]->slice_id, i,
					1,iPort%2);
		}
	}

	/*step 4? - do we need to create a socket to first middlebox?*/
	// TCP Connect
	char* sAddress = (char*)malloc(strlen(ptConf->pSSL->proxies[0]->address) + 1); // Large enough for string+\0
	memcpy(sAddress, ptConf->pSSL->proxies[0]->address, strlen(ptConf->pSSL->proxies[0]->address) + 1);
	char *sHost = strtok(sAddress, ":");
	int iPort = atoi(strtok(NULL, ":"));
	int iSock;
	COMMON_TcpConnect(&iSock, sHost, iPort);
	// Connect TCP socket to SSL socket
	ptSBio = BIO_new_socket(iSock, BIO_NOCLOSE);
    SSL_set_bio(ptConf->pSSL, ptSBio, ptSBio);

	/*step 5 - call the connect method*/
	/*SPP connect calls this method but also adds some checks,
	 * in particular it checks that there is at least one context (fine)
	 * but also checks that there is at least one proxy (which seems
	 * unnecassary but the server is included so there must be one)
	 */
	if (SSL_connect(ptConf->pSSL) <= 0) {
		iStatus=NETWORK_CONNECT_FAIL;
	}
	return SUCCESS;
}

int giSignal=0;
int giDone=0;
static pthread_t gtParent_thread;
static pthread_t gtSending_thread;
static pthread_t gtReceiving_thread;

static void
client_sighandler(int sig) {
	if(giSignal==0) giSignal=sig;
	giDone=1;
	/*note: pthread_kill is a signal sending method - not a
	 * "Die now" method. Unfortunate naming
	 */
	pthread_kill(gtSending_thread,sig);
	pthread_kill(gtReceiving_thread,sig);
}

struct iobuffer {
	int iUsed;
	union {
		unsigned char aucBuffer[1024];
		char stext[1024];
	};
	char z;/*force null termination of string*/
};

static void *
client_sendThread(void *pvArg)
{
	int iNewLine=0;
	int iCurrentSlice=0;
	ERROR_STATUS iStatus=SUCCESS;
	int iBytesRead;
	int iBytesToSend;
	int iBytesSent;
	struct iobuffer tInBuff;
	const ClientConfig *ptCConf=(const ClientConfig *)pvArg;
	char *sCurrent,*sEnd,sChunk;
	char sContext[MAX_CONTEXT_LEN];
	int iFIn=fileno(ptCConf->fIn);

	memset(&tInBuff,0,sizeof(tInBuff));

	while(!giDone && iStatus==SUCCESS) {
		/*wait until we have data to read*/
		/*we should always have an empty buffer at this point*/
		tInBuff.iUsed=0;
		memset(tInBuff.aucBuffer,0,sizeof(tInBuff.aucBuffer));
		iStatus=COMMON_ReadWaitFD(iFIn);
		/*Read as much data as possible*/
		iBytesRead=read(iFIn,tInBuff.aucBuffer,sizeof(tInBuff.aucBuffer));
		assert(iBytesRead>0); /*should be as we waited for it*/
		iBytesToSend=iBytesRead;

		iNewLine=(tInBuff.aucBuffer[0]=='\n');

		while(iStatus==SUCCESS && iBytesToSend>0) {
			if(iNewLine) {
				/*we hit a new line - advance to next context*/
				iCurrentSlice=(iCurrentSlice+1)%ptCConf->pSSL->slices_len;
				ptCConf->pSSL->write_slice=SPP_get_slice_by_id(ptCConf->pSSL,ptCConf->pSSL->slices[iCurrentSlice]->slice_id);
				tInBuff.iUsed++;
				iBytesToSend--;
				iNewLine=(tInBuff.aucBuffer[tInBuff.iUsed]=='\n'); /*should only happen on \n\n*/
				/*note - line above may overflow if \n happens to be last byte read
				 * this is guaranteed safe due to the additional null terminator char
				 * and the next iteration of the top loop will reset correctly*/
			} else if((sEnd=strchr(tInBuff.stext+tInBuff.iUsed,'\n'))==NULL) {
				/*entire buffer to output in this context*/
				iBytesSent=SSL_write(ptCConf->pSSL,tInBuff.stext+tInBuff.iUsed,iBytesToSend);
				/*todo - handle error sending*/
				assert(iBytesSent>0);
				tInBuff.iUsed+=iBytesSent;
				iBytesToSend-=iBytesSent;
			} else {
				/*send entire buffer up to and including new line*/
				/*advance buffer up to new line only - context change on next iteration
				 * of this loop*/
				iBytesSent=SSL_write(ptCConf->pSSL,tInBuff.stext+tInBuff.iUsed,sEnd-(tInBuff.stext+tInBuff.iUsed)+1);
				/*todo - handle error sending*/
				tInBuff.iUsed=sEnd-tInBuff.stext;
				iBytesToSend-=sEnd-(tInBuff.stext+tInBuff.iUsed);
				iNewLine=1;
				assert(iBytesSent==(sEnd-(tInBuff.stext+tInBuff.iUsed))+1);
			}
		}
	}
	/*signal to the parent so all other threads are woken*/
	pthread_kill(gtParent_thread,SIGINT);
	pthread_exit(NULL);
	return NULL;

}

static void *
client_receiveThread(void *pvArg)
{
	ERROR_STATUS iStatus=SUCCESS;
	struct iobuffer tInBuff;
	int iBytesRead=1;
	const ClientConfig *ptCConf=(const ClientConfig *)pvArg;
	BIO *sock_bio=ptCConf->pSSL->rbio;
	int rfd;
	SPP_SLICE *pSlice;
	SPP_CTX *pCtx;

	BIO_get_fd(sock_bio,&rfd);
	memset(&tInBuff,0,sizeof(tInBuff));

	while(iBytesRead>0 && iStatus==SUCCESS) {
		/*wait until we have data to read*/
		iStatus=COMMON_ReadWaitFD(rfd);
	    iBytesRead = SSL_read(ptCConf->pSSL,tInBuff.aucBuffer,sizeof(tInBuff.aucBuffer));
	    pSlice = ptCConf->pSSL->read_slice;
	    pCtx = ptCConf->pSSL->spp_read_ctx;
	    ptCConf->pSSL->read_slice = NULL;
	    ptCConf->pSSL->spp_read_ctx = NULL;

	    if(iBytesRead>0) {
	    	/*print out what we got and from where*/
	    	fprintf(ptCConf->fOut,"<%s(%d)>:%s",pSlice->purpose,pSlice->slice_id,tInBuff.stext);
	    } else {
	    	fprintf(ptCConf->fOut,"zero bytes read - closing");
	    }

	}


	/*signal to the parent so all other threads are woken*/
	pthread_kill(gtParent_thread,SIGINT);
	pthread_exit(NULL);
	return NULL;

}

ERROR_STATUS
client_RunClient(const ClientConfig *ptConf)
{
	void *pvUnused;
	ERROR_STATUS iStatus=SUCCESS;

	/*set an interrupt signal handler to kill the process*/
	signal(SIGINT,client_sighandler);
	signal(SIGTERM,client_sighandler);

	gtParent_thread=pthread_self();
	/*create the send and recioeve threads*/
	pthread_create(&gtReceiving_thread, NULL, client_receiveThread, (void *)ptConf);
	pthread_create(&gtSending_thread, NULL, client_sendThread, (void *)ptConf);

	/*wait for both to finish*/
	pthread_join(gtReceiving_thread,&pvUnused);
	pthread_join(gtSending_thread,&pvUnused);
	return SUCCESS;
}

int
main(const int iArgC, char *papsArgV[])
{
	ClientConfig tConfig;
	SSL_CTX *ptSSLctx=NULL;
	int iStatus;

	client_SetDefaults(&tConfig);

	iStatus=client_ParseArgs(iArgC,papsArgV, &tConfig);
	COMMON_CheckLogErrorAndExit(iStatus,stderr,"%s\n",USAGE);

	iStatus=client_CreateContext(&ptSSLctx,&tConfig);
	iStatus=client_Connect(ptSSLctx,&tConfig);
	iStatus=client_RunClient(&tConfig);

	return iStatus;
}
