#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <unistd.h>
#include <fcntl.h> // open(..) O_APPEND
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <vector>
#include <memory>

#ifdef __DEBUG__
#define DEBUG(x) printf("%s\n", (x))
#else
#define DEBUG(x)
#endif

#define LINE_BUFFER_SIZE 512
#define TOK_BUFFER_SIZE 64

int prog_should_close = 0;
void msg_err(const char * msg);

struct Tokenizer {
public:
	static char* get_redirected_file(char* cmd, char sp[]);
	static int get_cmd_args(char *line, char sp[], char** tokens);
};

struct Invoker {
public:
	int cmd_cd(char** argv);
	int cmd_pwd(char** argv);
	int cmd_quit(char** argv);
	int cmd_externel(char** argv);
};

struct Command {
	Invoker invoker;

public:
	int execute(char* cmd);
	virtual void execute_mode(char** cmds, int n_cmd) {}
};

struct SequentialCommands : public Command {
public:
	void execute_mode(char** cmds, int n_cmd);
};

struct ParallelCommands : public Command {
public:
	void execute_mode(char** cmds, int n_cmd);
};

struct Receiver {
	std::vector<std::string> log;

public:
	void loop(FILE* file);
};

int main(int argc, char **argv) {

	int		i;
	FILE	*file;
	Receiver receiver;

	if (argc > 1) {
		for (i=1; i<argc; i++) {
			file = fopen(argv[i], "r");
			if (file) {
				receiver.loop(file);
				fclose(file);
			} else {
				fprintf(stderr, "Cannot open file : %s\n", file);
			}
		}
	} else receiver.loop(stdin);

	return 0;
}

void msg_err(const char * msg) {
	write(STDERR_FILENO, msg, strlen(msg));
}

int Invoker::cmd_cd(char** argv) {
	if (argv[1] == NULL) {
		if (chdir(getenv("HOME")) != 0)
			msg_err("cd: failed to change dir to home\n");
	} else {
		if (chdir(argv[1]) != 0)
			msg_err("cd: no such directory\n");
	} return 1;
}

int Invoker::cmd_pwd(char** argv) {
	char cPwd[LINE_BUFFER_SIZE];
	getcwd(cPwd, sizeof(cPwd));
	int len = strlen(cPwd);
	cPwd[len] = '\n';
	cPwd[len + 1] = '\0';
	write(STDOUT_FILENO, cPwd, strlen(cPwd));
	return 1;
}

int Invoker::cmd_quit(char** argv) {
	prog_should_close = 1;
	return 0;
}

int Invoker::cmd_externel(char** argv) {
	DEBUG("cmd_externel");
	int		status;
	pid_t	pid;

	pid = fork();
	if (pid == 0) {
		if (execvp(argv[0], argv) < 0)
			msg_err("An error has occurred\n");
		exit(1);
	} else if (pid < 0) {
		exit(1);
	} else do {
		waitpid(pid, &status, WUNTRACED);
	} while (!WIFEXITED(status) && !WIFSIGNALED(status));

	return 1;
}

int Command::execute(char* cmd) {
	DEBUG("execute");
	int		i, n;

	char cmd_cp[strlen(cmd) + 1];
	strcpy(cmd_cp, cmd);
	n = Tokenizer::get_cmd_args(cmd_cp, " \n\t\r\a\"", NULL);
	char* argv[n + 1];
	Tokenizer::get_cmd_args(cmd, " \n\t\r\a\"", argv);

	if (argv[0] == NULL) // null command
		return 1;
	if (strcmp(argv[0], "cd") == 0)
		return invoker.cmd_cd(argv);
	else if (strcmp(argv[0], "pwd") == 0)
		return invoker.cmd_pwd(argv);
	else if (strcmp(argv[0], "quit") == 0)
		return invoker.cmd_quit(argv);
	else return invoker.cmd_externel(argv);
}

void SequentialCommands::execute_mode(char** cmds, int n_cmd) {
	DEBUG("exec_sequential");
	int		i;

	for (i=0; i<n_cmd; i++) {
		execute(cmds[i]);
		if (prog_should_close) return;
	}
}

void ParallelCommands::execute_mode(char** cmds, int n_cmd) {
	DEBUG("exec_parallel");
	int		i, status;
	pid_t	pid;

	for (i=0; i<n_cmd; i++) {
		pid = fork();
		if (pid == 0) { // child process
			execute(cmds[i]);
			exit(0);
		}
	}

	while ((pid = wait(&status)) > 0); // wait till all children finish their jobs
}

char* Tokenizer::get_redirected_file(char* cmd, char sp[]) {
	DEBUG("get_redirected_file");
	char	*cmark, *crestore;

	cmark = strstr(cmd, sp);
	if (cmark) {
		crestore = cmark;
		cmark += strlen(sp);
		*crestore = '\0';
		return cmark;
	} else return NULL;
}

int Tokenizer::get_cmd_args(char *line, char *sp, char* tokens[]) {
	DEBUG("get_cmd_args");
	int		i;
	char	*token;

	i = 0;
	token = strtok(line, sp);
	while (token) {
		if (tokens) {
			tokens[i] = token;
		} i++;
		token = strtok(NULL, sp);
	} if (tokens) tokens[i] = NULL;

	return i;
}

void Receiver::loop(FILE* file) {
	DEBUG("loop");
	int		i, n, status;
	char	line[LINE_BUFFER_SIZE], line_cp[LINE_BUFFER_SIZE];
	char	*token, **cmd;
	Command* command = nullptr;

	do {
		if (file == stdin) {
			write(STDOUT_FILENO, "> ", strlen("> "));
		}
		if (fgets(line, LINE_BUFFER_SIZE, file) == NULL) {
			prog_should_close = 1; // non-sense
			break;
		}

		log.push_back(std::string(line));

		strcpy(line_cp, line);
		if (strstr(line_cp, "+") && strstr(line_cp, ";")) {
			msg_err("Hybrid execution not supported\n");
			continue;
		}

		n = Tokenizer::get_cmd_args(line_cp, "+", NULL);
		if (n > 1) {
			char* cmds[n + 1];
			Tokenizer::get_cmd_args(line, "+", cmds);
			command = new ParallelCommands();
			command->execute_mode(cmds, n);
			delete command; command = nullptr;
		} else {
			n = Tokenizer::get_cmd_args(line_cp, ";", NULL);
			char* cmds[n + 1];
			Tokenizer::get_cmd_args(line, ";", cmds);
			command = new SequentialCommands();
			command->execute_mode(cmds, n);
			delete command; command = nullptr;
		}
	} while (!prog_should_close);
}
