#include "stdafx.h"
#include "Open.h"
#include "Error.h"
#include "Core/Str.h"
#include "Core/Convert.h"
#include "Core/SystemError.h"

#if defined(WINDOWS)

#include <shellapi.h>
#pragma comment (lib, "Shell32.lib")

namespace storm {

	void open(Url *file) {
		DWORD error = (DWORD)ShellExecute(NULL, L"open", file->format()->c_str(), NULL, NULL, SW_SHOWDEFAULT);
		const wchar *msg = null;
		switch (error) {
		case 0:
		case SE_ERR_OOM:
			msg = S("Out of memory.");
			break;
		case ERROR_FILE_NOT_FOUND:
			msg = S("File not found.");
			break;
		case ERROR_PATH_NOT_FOUND:
			msg = S("Path not found.");
			break;
		case SE_ERR_ACCESSDENIED:
			msg = S("Access denied.");
			break;
		case SE_ERR_ASSOCINCOMPLETE:
		case SE_ERR_NOASSOC:
			msg = S("Unknown or incomplete file association.");
			break;
		default:
			if (error <= 32)
				msg = S("Unknown error.");
		}

		if (msg)
			throw new (file) SystemError(TO_S(file, S("Failed to open ") << file << S(": ") << msg));
	}

	// Escape the string so that it can be read back by most C programs on Windows. Adds quotes around it.
	static void escapeStr(StrBuf *out, Str *input) {
		*out << Char('"');

		Nat slashes = 0;
		for (Str::Iter i = input->begin(); i != input->end(); ++i) {
			if (i.v().codepoint() == '\\') {
				slashes++;
			} else if (i.v().codepoint() == '"') {
				// If we have slashes right before the ", then we need to double those since they
				// are interpreted as an escape character in this particular context.
				for (Nat i = 0; i < slashes; i++)
					*out << Char('\\');
				*out << Char('"');
				slashes = 0;
			} else {
				slashes = 0;
			}

			*out << i.v();
		}

		// If we have slashes here, we need to double them once more.
		for (Nat i = 0; i < slashes; i++)
			*out << Char('\\');
		*out << Char('"');
	}

	// Convert parameters to a string.
	static Str *strParams(Str *program, Array<Str *> *params) {
		StrBuf *out = new (program) StrBuf();
		escapeStr(out, program);

		for (Nat i = 0; i < params->count(); i++) {
			*out << Char(' ');
			escapeStr(out, params->at(i));
		}

		return out->toS();
	}

	void platformExecute(Execute *exec) {
		STARTUPINFO info;
		zeroMem(info);
		info.cb = sizeof(info);

		PROCESS_INFORMATION out;

		Str *fileName;
		const wchar *fileNameCStr;
		if (exec->program->relative() && exec->program->count() == 1) {
			fileName = exec->program->at(0);
			fileNameCStr = NULL;
		} else {
			fileName = exec->program->toS();
			fileNameCStr = fileName->c_str();
		}

		const wchar *cwd = null;
		if (exec->workingDir)
			cwd = exec->workingDir->format()->c_str();

		DWORD flags = NORMAL_PRIORITY_CLASS;
		if (exec->terminal)
			flags |= CREATE_NEW_CONSOLE;
		if (exec->group)
			flags |= CREATE_NEW_PROCESS_GROUP;

		const wchar *p = strParams(fileName, exec->args)->c_str();
		if (!CreateProcess(fileNameCStr, (LPWSTR)p, NULL, NULL, FALSE, flags, NULL, cwd, &info, &out)) {
			const wchar *error = S("Unknown error.");
			switch (GetLastError()) {
			case ERROR_FILE_NOT_FOUND:
				error = S("File not found.");
				break;
			case ERROR_PATH_NOT_FOUND:
				error = S("Path not found.");
				break;
			case ERROR_ACCESS_DENIED:
				error = S("Access denied.");
				break;
			}

			throw new (exec) SystemError(TO_S(exec, S("Failed to launch '") << fileName << S("': ") << error));
		}

		CloseHandle(out.hProcess);
		CloseHandle(out.hThread);
	}

}

#elif defined(POSIX)

#include <unistd.h>
#include <sys/wait.h>
#include <sys/fcntl.h>

namespace storm {

	enum ExecError {
		errorNone,
		errorChdir,
		errorFork,
		errorExec,
		errorInternal,

		exitCode,
		exitSignal,
	};

	struct ExecStatus {
		ExecError status;
		int error;

		ExecStatus() : status(errorNone), error(0) {}
		ExecStatus(ExecError s) : status(s), error(errno) {}
		ExecStatus(ExecError s, int val) : status(s), error(val) {}
	};

	static void errnoError(Engine &e, const wchar *message, const char *str, int errno_val) {
		StrBuf *msg = new (e) StrBuf();
		*msg << message;
		if (str) {
			*msg << S(" ");
			*msg << toWChar(e, str)->v;
		}
		*msg << S(": ");
		*msg << systemErrorMessage(e, errno_val);
		throw new (e) SystemError(msg->toS());
	}

	static void errnoError(Engine &e, const wchar *message, int errno_val) {
		errnoError(e, message, null, errno_val);
	}

	// Throw an appropriate error indicated by ExecStatus.
	static void statusError(Engine &e, ExecStatus status, const char *file, const char *cwd) {
		switch (status.status) {
		case errorChdir:
			if (cwd)
				errnoError(e, S("Failed to change directory to"), cwd, status.error);
			else
				errnoError(e, S("Failed to change directory"), status.error);
			break;
		case errorFork:
			errnoError(e, S("Failed to fork"), status.error);
			break;
		case errorExec:
			if (file)
				errnoError(e, S("Failed to execute"), file, status.error);
			else
				errnoError(e, S("Failed to exec"), status.error);
			break;
		case errorInternal:
			errnoError(e, S("Internal error"), status.error);
			break;
		}
	}

	// Spawn a new process. Properly indicates failures from "execlp" to the parent process.
	// 'params' is an array of parameters as one would give the execvp() function.
	// Makes sure we don't have to wait for the child process.
	// If "detach" is set, then we will place the process in a new process group.
	// If "monitorExit" is nonzero, then we wait (maximum of that many seconds) until the process was terminated and
	// return the exit code. Returns - SIGALRM if the timeout expired.
	static ExecStatus execute(Engine &e, const vector<std::string> params, const char *cdTo,
							bool detach, nat monitorExit = 0) {
		// Prepare parameters array.
		vector<const char *> execParams(params.size() + 1, null);
		for (size_t i = 0; i < params.size(); i++)
			execParams[i] = params[i].c_str();

		// To detect errors in the child, we create a new pipe with the CLOEXEC flag set. As such,
		// we can wait until the write end is closed. If nothing was written, then we know that
		// everything went well (at least up to starting the process). Otherwise we write an error
		// message to the pipe before exiting.
		int pipefds[2];
		if (pipe(pipefds) != 0)
			errnoError(e, S("Failed to create a pipe for spawning a process"), errno);

		int pipeRead = pipefds[0];
		int pipeWrite = pipefds[1];
		fcntl(pipeWrite, F_SETFD, FD_CLOEXEC);

		pid_t pid = fork();
		if (pid < 0) {
			int error = errno;
			close(pipeRead);
			close(pipeWrite);
			errnoError(e, S("Failed to fork a child process"), error);
		}

		ExecStatus status;

		if (pid == 0) {
			// Child.
			// Close the read end of the pipe.
			close(pipeRead);

			// Note: We need to use _exit here, otherwise we might crash in an exit cleanup handler somewhere...

			// CD to the correct directory if we were asked to do so.
			if (cdTo) {
				if (chdir(cdTo) < 0) {
					status = ExecStatus(errorExec);
					(void)!write(pipeWrite, &status, sizeof(status));
					_exit(1);
				}
			}

			// Fork once more to avoid zombies.
			pid = fork();
			if (pid < 0) {
				status = ExecStatus(errorFork);
				(void)!write(pipeWrite, &status, sizeof(status));
				_exit(1);
			}

			if (pid == 0) {
				if (detach) {
					// Create a new process group if we were asked to detach.
					setpgid(0, 0);
				}

				// Child. Now we can call exec!
				execvp(execParams[0], (char **)&execParams[0]);

				// If we get here, the call failed.
				status = ExecStatus(errorExec);
				(void)!write(pipeWrite, &status, sizeof(status));
				_exit(1);
			}

			// Parent. See if we should wait for the exit code from the child.
			if (monitorExit) {
				alarm(monitorExit);

				int status = 0;
				waitpid(pid, &status, 0);

				// Forward it.
				if (WIFEXITED(status))
					_exit(WEXITSTATUS(status));
				if (WIFSIGNALED(status))
					raise(WTERMSIG(status));
			}

			// Success.
			_exit(0);
		}

		// Close the write end from here.
		close(pipeWrite);

		// Wait for the first child.
		int waitStatus = 0;
		waitpid(pid, &waitStatus, 0);

		// Read status from the pipe.
		while (true) {
			ssize_t result = read(pipeRead, &status, sizeof(status));
			if (result < 0) {
				if (errno == EINTR)
					continue;

				return ExecError(errorInternal);
			} else if (result == 0) {
				// No more data. The socket was closed.
				break;
			} else if (result == sizeof(status)) {
				// All is well, exit.
				break;
			} else {
				// This should not happen - we write to the pipe in one chunk.
				STORM_FAIL(S("Got an unexpected size from 'read'."));
				break;
			}
		}

		if (status.status != errorNone)
			return status;

		if (WIFEXITED(waitStatus))
			status = ExecStatus(exitCode, WEXITSTATUS(waitStatus));
		else if (WIFSIGNALED(waitStatus))
			status = ExecStatus(exitSignal, WTERMSIG(waitStatus));
		return status;
	}

	void open(Url *file) {
		vector<std::string> params = {
			"xdg-open",
			file->format()->utf8_str(),
		};
		// PVAR(file->format());
		// Note: xdg-open does not always terminate immediately, so we cannot really look at its exit code.
		ExecStatus status = execute(file->engine(), params, null, true, 1);
		if (status.status == exitCode) {
			if (status.error == 2)
				throw new (file) SystemError(TO_S(file, S("The file ") << file << S(" does not exist.")));
			if (status.error == 3)
				throw new (file) SystemError(TO_S(file, S("Unable to find a tool for opening ") << file));
			if (status.error != 0)
				throw new (file) SystemError(TO_S(file, S("Failed to open: ") << file));
		} else if (status.status == exitSignal) {
			// If it exits on timeout, it just means that xdg-open exec'd into the new process.
			if (status.error != SIGALRM)
				throw new (file) SystemError(TO_S(file, S("Failed to open: ") << file));
		} else {
			statusError(file->engine(), status, params[0].c_str(), null);
		}
	}

	static const char *terminalPrefix[] = {
		"xdg-terminal-exec", "--"
	};

	static const char *skipSpaces(const char *ptr) {
		while (*ptr != '\0' && *ptr == ' ')
			ptr++;
		return ptr;
	}

	static void addFallback(Engine &e, vector<std::string> &addTo) {
		vector<std::string>::iterator insertAt = addTo.begin();
		const char *cmdline = getenv("STORM_TERM_COMMAND");
		if (!cmdline)
			cmdline = "xterm -e";

		const char *start = cmdline;
		while (*start) {
			start = skipSpaces(start);
			const char *end = start;
			while (*end != '\0' && *end != ' ')
				end++;

			if (start != end) {
				insertAt = addTo.insert(insertAt, std::string(start, end)) + 1;
			}

			start = end;
		}
	}

	void platformExecute(Execute *exec) {
		Engine &e = exec->engine();

		// We need to copy parameters out of the GC. This makes it safer to use them in the forked
		// process, and also makes sure to keep them alive in the heap allocation we need for the C
		// array.
		vector<std::string> paramList;
		paramList.reserve(exec->args->count() + 4);

		// Note: We copy a bit too much here. Would be nice to have "to_c++_string" in the Str class.

		// If "file" is relative and only has a single part, then don't output "./" in front of
		// it. That would probably inhibit the ability to look in PATH.
		Url *file = exec->program;
		if (file->relative() && file->count() == 1) {
			paramList.push_back(file->at(0)->utf8_str());
		} else {
			paramList.push_back(file->format()->utf8_str());
		}

		for (Nat i = 0; i < exec->args->count(); i++)
			paramList.push_back(exec->args->at(i)->utf8_str());

		const char *cwd = null;
		if (exec->workingDir)
			cwd = exec->workingDir->format()->utf8_str();

		if (!exec->terminal) {
			ExecStatus status = execute(e, paramList, cwd, exec->group);
			statusError(e, status, paramList[0].c_str(), cwd);
		} else {
			// If executing in a new terminal we need some logic to try multiple options.
			for (size_t i = 0; i < ARRAY_COUNT(terminalPrefix); i++)
				paramList.insert(paramList.begin() + i, terminalPrefix[i]);

			// Try to execute the first one:
			ExecStatus status = execute(e, paramList, cwd, exec->group);
			if (status.status == errorExec && status.error == ENOENT) {
				// File not found, this means we want to try with the second option!
				paramList.erase(paramList.begin(), paramList.begin() + ARRAY_COUNT(terminalPrefix));
				addFallback(e, paramList);

				status = execute(e, paramList, cwd, exec->group);
			}

			// If we still failed, produce a more useful error message.
			if (status.status == errorExec && status.error == ENOENT) {
				StrBuf *message = new (e) StrBuf();
				*message << S("Failed to launch a new terminal for ") << file << S(".\n");
				*message << S("The command 'xdg-terminal-exec' was attempted, but it was not ");
				*message << S("found.\nAs a fallback, the command '");
				*message << toWChar(e, paramList[0].c_str())->v;
				*message << S("' was used, but it also failed.\nYou can either install ");
				*message << S("xdg-terminal-exec, or set the environment\nvariable STORM_TERM_COMMAND ");
				*message << S("to tell Storm how to open a terminal.");
				throw new (e) SystemError(message->toS());
			}

			// Throw any errors.
			statusError(e, status, paramList[0].c_str(), cwd);
		}
	}
}

#endif

/**
 * The platform-agnostic part:
 */

namespace storm {

	void execute(Url *program, Array<Str *> *args) {
		(new (program) Execute(program, args))->detach();
	}

	Execute::Execute(Url *program, Array<Str *> *args)
		: program(program), args(args),
		  workingDir(null), terminal(false), group(false) {}

	Execute::Execute(Str *program, Array<Str *> *args)
		: program(new (program) Url(new (program) Array<Str *>(1, program))), args(args),
		  workingDir(null), terminal(false), group(false) {}

	Execute *Execute::workingDirectory(MAYBE(Url *) dir) {
		workingDir = dir;
		return this;
	}

	Execute *Execute::newTerminal() {
		terminal = true;
		group = true;
		return this;
	}

	Execute *Execute::newGroup() {
		group = true;
		return this;
	}

	void Execute::detach() {
		// Forward to the platform-specific code.
		platformExecute(this);
	}

}
