#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#ifndef COMMAND_H_
#define COMMAND_H_
#define BUFF_SIZE 40
#define LS_FILENAME "lstext"
#define BASE_PATH "./"
#define CONN_TIMEOUT 30
#define CONN_WAIT_TIME 1000000

/******************
 * These are the commands that the clients use to communicate
 * with the server.  The communications between client and
 * server are governed by these command types
 ******************/
typedef enum {
	CMD_INVALID,
	CMD_LS,
	CMD_CD,
	CMD_GET,
	CMD_GET_ENCRYPT,
	CMD_PUT,
	CMD_PUT_ENCRYPT,
	CMD_QUIT,
	CMD_EMPTY
} command_type;

// define response types
typedef enum {
	RES_PROCEED,
	RES_CEASE
} response_type;

command_type command_type_parse(char*, int);
command_type command_parse(char*, char*, int);
void fputnc(char*, FILE*, int);

// parse command and argument from cmd_str and return the command
command_type command_parse(char* cmd_str, char* arg, int encrypted) {
	char* cmd_arg_str;
	char* cmd_type_str;
	command_type type;
	cmd_type_str = strtok(cmd_str, "\n\r ");
	cmd_arg_str = strtok(NULL, "\n\r ");
	type = command_type_parse(cmd_type_str, encrypted);
	if (type == CMD_QUIT || type == CMD_INVALID || type == CMD_EMPTY
			|| type == CMD_LS) {
		arg = NULL;
		return type;
	}
	// if encryption is turned on, set command accordingly
	if (type == CMD_GET && encrypted == 1)
		type = CMD_GET_ENCRYPT;
	else if (type == CMD_PUT && encrypted == 1)
		type = CMD_PUT_ENCRYPT;

	if (cmd_arg_str == NULL) {
		arg = NULL;
		return (type != CMD_CD) ? CMD_INVALID : CMD_CD;
	} else {
		bzero(arg, BUFF_SIZE);
		strcpy(arg, cmd_arg_str);
		return type;
	}
}

// parse cmd and return the corresponding command type
command_type command_type_parse(char* cmd, int encrypted) {
	if(cmd == NULL || cmd[0] == '\0'){
		return CMD_EMPTY;
	} else if (strcmp(cmd, "ls") == 0) {
		return CMD_LS;
	} else if (strcmp(cmd, "cd") == 0) {
		return CMD_CD;
	} else if (strcmp(cmd, "get") == 0) {
		return (encrypted == 0) ? CMD_GET : CMD_GET_ENCRYPT;
	} else if (strcmp(cmd, "put") == 0) {
		return (encrypted == 0) ? CMD_PUT : CMD_PUT_ENCRYPT;
	} else if (strcmp(cmd, "quit") == 0) {
		return CMD_QUIT;
	}
	return CMD_INVALID;

}

// write dir tree to file
void cmd_call_ls(char* location) {
	DIR *dp;
	struct dirent *entptr;
	FILE* file;

	file = fopen(LS_FILENAME, "w");
	dp = opendir(location);
	// if directory is empty, add a message to the file
	if (dp != NULL) {
	} else {
		fputs("*****************\n DIRECTORY EMPTY\n*****************\n", file);
		fclose(file);
		return;
	}
	// iterate through all directories and files and add their names to a file
	while ((entptr = readdir(dp)) != NULL) {
		if (entptr->d_type == DT_DIR) {
			fputs("+ ", file);
		} else {
			fputs("  ", file);
		}
		fputs(entptr->d_name, file);
		fputs("\n", file);
	}

	fclose(file);
	return;
}

// set new client location if it exists
void cmd_call_cd(char* basePath, char* relativePath) {
	DIR *dp;
	char fullPath[BUFF_SIZE];
	char* prevDir, *head, tempPath[BUFF_SIZE];
	bzero(fullPath, BUFF_SIZE);
	if (strncmp(relativePath, "..", 2) == 0) {
		// prevent exiting root directory
		if(strcmp(basePath, "./") == 0)
			return;
		strcpy(tempPath, basePath);
		head = strtok(tempPath, "/");
		prevDir = head;
		while((head = strtok(NULL, "/")) != NULL){
			prevDir = head;
		}
		// clear last directory in path
		bzero(&basePath[(prevDir - tempPath)/sizeof(char)], strlen(prevDir));
		return;
	} else if (strncmp(relativePath, ".", 1) == 0) {
		return;
	}

	// concatenate full path from base path and relative path
	sprintf(fullPath, "%s%s/", basePath, relativePath);
	if (relativePath == NULL || relativePath[0] == '\0') {
		bzero(basePath, BUFF_SIZE);
		strcpy(basePath, BASE_PATH);
		return;
	}

	// if path exists, store as new base path
	if ((dp = opendir(fullPath)) != NULL) {
		bzero(basePath, BUFF_SIZE);
		strcpy(basePath, fullPath);
	}
}

// create checksum byte for data in buffer
char getChecksum(char* buff) {
	int i = 0, sum = 0;
	while (buff[i] != '\0')
		sum += buff[i++];
	return (char) (sum % 256);
}

// create random encryption key
void generateEncryptionKey(char* keyBuff, int size) {
	int i = 0;
	bzero(keyBuff, size);
	while (i < size - 1) {
		keyBuff[i] = rand() % 255 + 1;
		i++;
	}
}

// xor buffer with encryption key to encrypt or decrypt
void XORcrypt(char* key, char* buff, int len) {
	int i = 0;
	while (i < len) {
		buff[i] ^= key[i];
		i++;
	}
}

// checks for the existence of filename at basepath.  Returns 1 if true
int doesFileExist(char* basePath, char *filename) {
	char fullPath[BUFF_SIZE*2];
	bzero(fullPath, BUFF_SIZE*2);
	sprintf(fullPath, "%s%s", basePath, filename);
    struct stat file_stat;
    int result = stat(fullPath, &file_stat);
    return (result == 0) ? 1 : 0;
}

/******************
 * This is the function that sends files.
 * The data is read from a file, then encrypted, then
 * metadata is appended, and it is sent across the network
 ******************/
void sendFile(int sockfd, char* filename, char* buff, int useEncryption,
		int useChecksum, int printInfo) {
	FILE* file;
	struct stat file_stat;
	char encryptionKey[BUFF_SIZE];
	int n;
	ulong id = 0;

	// if encryption is on, create a key and send
	if (useEncryption == 1) {
		generateEncryptionKey(encryptionKey, BUFF_SIZE);
		bzero(buff, BUFF_SIZE);
		if ((n = write(sockfd, encryptionKey, BUFF_SIZE)) < 0) {
			perror("sendFile : write key");
		}
	}
	if ((file = fopen(filename, "r")) == NULL) {
		perror("sendFile : file open");
		return;
	}
	if (stat(filename, &file_stat) != 0)
		perror("fstat");
	// send size of file across network
	bzero(buff, BUFF_SIZE);
	if(printInfo == 1)
		printf("size: %d bytes\n", file_stat.st_size);
	sprintf(buff, "%d", file_stat.st_size);
	if ((n = write(sockfd, buff, BUFF_SIZE)) <= 0)
		perror("sendFile : write file size");
	if(printInfo == 1)
		printf("transmitting...\n");
	// send file across network in chunks
	while (!feof(file)) {
		bzero(buff, BUFF_SIZE);
		// read data from file into buffer, leaving room for ID and checksum
		if (fread(buff, sizeof(char),
				BUFF_SIZE - sizeof(char) * 2 - sizeof(ulong), file) == 0)
			perror("sendFile : fread");
		// if encryption on, encrypt the buffer
		if (useEncryption == 1)
			XORcrypt(encryptionKey, buff,
					BUFF_SIZE - sizeof(char) * 2 - sizeof(ulong));
		// if checksum on, append ID and checksum to buffer
		if (useChecksum == 1) {
			buff[BUFF_SIZE - sizeof(char) * 2] = getChecksum(buff);
			buff[BUFF_SIZE - sizeof(char) * 2 - sizeof(ulong)] = id++;
		}
		// send the data across the network
		if ((n = write(sockfd, buff, BUFF_SIZE)) <= 0)
			perror("write");
	}
	if(printInfo == 1)
		printf("transmission complete\n");
	fclose(file);
}

/******************
 * This is the function that receives files.
 * The data is read from the network, then the
 * metadata is extracted, then the data is decrypted
 ******************/
void receiveFile(int sockfd, char* filename, char* buff, int useEncryption,
		int useChecksum, int printInfo) {
	FILE* file;
	clock_t t;
	int n, size, metasize = sizeof(char) * 2 + sizeof(ulong);
	ulong id, invalidCount = 0;
	char checksum;
	char encryptionKey[BUFF_SIZE];

	if ((file = fopen(filename, "w")) == NULL) {
		perror("file open");
		return;
	}

	// receive encryption key
	if (useEncryption == 1) {
		bzero(buff, BUFF_SIZE);
		bzero(encryptionKey, BUFF_SIZE);
		if ((n = read(sockfd, buff, BUFF_SIZE)) < 0) {
			perror("receiveFile:read key");
		}
		strcpy(encryptionKey, buff);
	}

	// read size
	bzero(buff, BUFF_SIZE);
	if ((n = read(sockfd, buff, BUFF_SIZE)) < 0) {
		perror("read size");
		printf("errno = %d\n", errno);
		// if connection is broken, wait on it
		if (errno == ENETUNREACH || errno == ENETRESET || errno == ECONNABORTED
				|| errno == ECONNRESET) {
			usleep(CONN_WAIT_TIME);
			receiveFile(sockfd, filename, buff, useEncryption, useChecksum, printInfo);
		}
		return;
	}
	size = strtol(buff, NULL, 10);
	if(printInfo == 1){
		printf("file: %s\nsize: %d bytes\n", filename, size);
	}
	// read file
	t = clock();
	if(printInfo == 1)
		printf("receiving...\n");
	fflush(stdout);
	while (size > 0) {
		bzero(buff, BUFF_SIZE);
		// read data
		if ((n = read(sockfd, buff, BUFF_SIZE)) < 0) {
			printf("errno : %d", errno);
			perror("msg");
			// if connection is broken, wait on it
			if (errno == ENETUNREACH || errno == ENETRESET
					|| errno == ECONNABORTED || errno == ECONNRESET) {
				usleep(CONN_WAIT_TIME);
				printf("waiting...\n");
				continue;
			}
			break;
		}
		// extract checksum and buffer id and compare checksum with a calculated checksum for the buffer contents
		if (useChecksum == 1) {
			id = (ulong) buff[BUFF_SIZE - metasize];
			checksum = (char) buff[BUFF_SIZE - sizeof(char) * 2];
			bzero(&buff[BUFF_SIZE - metasize], metasize);
			if (checksum != getChecksum(buff)){
				printf("invalid checksum on buffer %lu\n", id);
				invalidCount++;
			}
		}
		// decrypt buffer
		if (useEncryption == 1)
			XORcrypt(encryptionKey, buff, BUFF_SIZE - metasize);
		size -= BUFF_SIZE - metasize;
		// write buffer to file
		fputnc(buff, file,
				(size >= 0) ?
						BUFF_SIZE - metasize : BUFF_SIZE - metasize + size);
	}
	t = clock() - t;
	if(printInfo == 1){
		printf("receive complete\n");
		printf("File transfer took %f seconds\n",((float)t)/CLOCKS_PER_SEC);
		printf("%d invalid checksums%s\n", invalidCount, (invalidCount > 0) ? " : consider retransmitting" : "");
	}
	fclose(file);
}

// put n chars into file
void fputnc(char* buff, FILE* file, int n) {
	int i = 0;
	while (i < n) {
		fputc(buff[i], file);
		i++;
	}
}

#endif /* COMMAND_H_ */
