//-----------------------------------------------------------------------------
// AScript os module
//-----------------------------------------------------------------------------
#include <ascript.h>

#if defined(HAVE_WINDOWS_H)
#else
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#endif

AScript_BeginModule(os)

static Environment *_pEnvSelf = NULL;

void SetupStream(Stream **ppStreamStdout, Stream **ppStreamStderr);
int ExecProgram(Environment &env, Signal sig, const char *pathName,
		const ValueList &valList, Stream *pStreamStdout, Stream *pStreamStderr);

//-----------------------------------------------------------------------------
// AScript module functions: os
//-----------------------------------------------------------------------------
// os.exec(pathname:string, args*:string):map
AScript_DeclareFunction(exec)
{
	SetMode(RSLTMODE_Normal, FLAG_Map);
	DeclareArg(env, "pathname", VTYPE_String);
	DeclareArg(env, "args", VTYPE_String, OCCUR_ZeroOrMore);
	SetHelp("Executes the specified executable file.");
}

AScript_ImplementFunction(exec)
{
	Stream *pStreamStdout = NULL, *pStreamStderr = NULL;
	SetupStream(&pStreamStdout, &pStreamStderr);
	int rtn = ExecProgram(env, sig, args.GetString(0),
						args.GetList(1), pStreamStdout, pStreamStderr);
	return Value(rtn);
}

// os.getenv(name:string):map
AScript_DeclareFunction(getenv)
{
	SetMode(RSLTMODE_Normal, FLAG_Map);
	DeclareArg(env, "name", VTYPE_String);
	SetHelp("Returns the value of a environment variable.");
}

AScript_ImplementFunction(getenv)
{
	String str = OAL::GetEnv(args.GetString(0));
	return Value(env, str.c_str());
}

// os.putenv(name:string, value:string):void
AScript_DeclareFunction(putenv)
{
	SetMode(RSLTMODE_Void, FLAG_None);
	DeclareArg(env, "name", VTYPE_String);
	DeclareArg(env, "value", VTYPE_String);
	SetHelp("Set the value of a environment variable.");
}

AScript_ImplementFunction(putenv)
{
	OAL::PutEnv(args.GetString(0), args.GetString(1));
	return Value::Null;
}

// Module entry
AScript_ModuleEntry()
{
	_pEnvSelf = &env;
	// value assignment
	Module *pModuleSys = env.GetModule_sys();
	do {
		const Value *pValue = pModuleSys->LookupValue(AScript_Symbol(stdin), false);
		if (pValue == NULL) pValue = &Value::Null;
		AScript_AssignValue(stdin, *pValue);
	} while (0);
	do {
		const Value *pValue = pModuleSys->LookupValue(AScript_Symbol(stdout), false);
		if (pValue == NULL) pValue = &Value::Null;
		AScript_AssignValue(stdout, *pValue);
	} while (0);
	do {
		const Value *pValue = pModuleSys->LookupValue(AScript_Symbol(stderr), false);
		if (pValue == NULL) pValue = &Value::Null;
		AScript_AssignValue(stderr, *pValue);
	} while (0);
	// function assignment
	AScript_AssignFunction(exec);
	AScript_AssignFunction(getenv);
	AScript_AssignFunction(putenv);
}

AScript_ModuleTerminate()
{
}

void SetupStream(Stream **ppStreamStdout, Stream **ppStreamStderr)
{
	do {
		const Value *pValue = _pEnvSelf->LookupValue(AScript_Symbol(stdout), false);
		if (pValue != NULL && pValue->IsStream()) {
			*ppStreamStdout = &pValue->GetStream();
		}
	} while (0);
	do {
		const Value *pValue = _pEnvSelf->LookupValue(AScript_Symbol(stderr), false);
		if (pValue != NULL && pValue->IsStream()) {
			*ppStreamStderr = &pValue->GetStream();
		}
	} while (0);
}

#if defined(HAVE_WINDOWS_H)

static void AppendCmdLine(String &cmdLine, const char *arg)
{
	bool quoteFlag = (::strchr(arg, ' ') != NULL);
	if (quoteFlag) {
		cmdLine += '"';
		cmdLine += arg;
		cmdLine += '"';
	} else {
		cmdLine += arg;
	}
}

int ExecProgram(Environment &env, Signal sig, const char *pathName,
		const ValueList &valList, Stream *pStreamStdout, Stream *pStreamStderr)
{
	String cmdLine;
	AppendCmdLine(cmdLine, pathName);
	foreach_const (ValueList, pValue, valList) {
		cmdLine += " ";
		AppendCmdLine(cmdLine, pValue->GetString());
	}
	HANDLE hStdout = INVALID_HANDLE_VALUE;
	HANDLE hStderr = INVALID_HANDLE_VALUE;
	HANDLE hStdoutWatch = INVALID_HANDLE_VALUE;
	HANDLE hStderrWatch = INVALID_HANDLE_VALUE;
	do {
		SECURITY_ATTRIBUTES sa;
		::memset(&sa, 0x00, sizeof(sa));
		sa.nLength = sizeof(sa);
		sa.lpSecurityDescriptor = NULL;
		sa.bInheritHandle = TRUE;
		if (pStreamStdout != NULL && !::CreatePipe(&hStdoutWatch, &hStdout, &sa, 0)) {
			sig.SetError(ERR_IOError, "failed to create a pipe");
			return -1;
		}
		if (pStreamStderr != NULL && !::CreatePipe(&hStderrWatch, &hStderr, &sa, 0)) {
			sig.SetError(ERR_IOError, "failed to create a pipe");
			return -1;
		}
	} while (0);
	STARTUPINFO si;
	PROCESS_INFORMATION ps;
	::memset(&si, 0x00, sizeof(si));
	si.cb = sizeof(STARTUPINFO);
	si.dwFlags = STARTF_USESTDHANDLES;
	si.hStdInput = ::GetStdHandle(STD_INPUT_HANDLE);
	si.hStdOutput = hStdout;
	si.hStdError = hStderr;
	BOOL rtn = ::CreateProcess(NULL, const_cast<char *>(cmdLine.c_str()),
							NULL, NULL, TRUE, 0, NULL, NULL, &si, &ps);
	if (!rtn) {
		sig.SetError(ERR_IOError, "failed to execute %s", pathName);
		return -1;
	}
	::WaitForInputIdle(ps.hProcess, INFINITE);
	OAL::Memory memory(32768);
	DWORD exitCode = 0;
	for (;;) {
		bool dataAvailFlag = false;
		if (pStreamStdout != NULL) {
			DWORD bytesAvail;
			::PeekNamedPipe(hStdoutWatch, NULL, NULL, NULL, &bytesAvail, NULL);
			if (bytesAvail > 0) {
				dataAvailFlag = true;
				void *buff = memory.GetPointer();
				DWORD bytesRead;
				::ReadFile(hStdoutWatch, buff,
							static_cast<DWORD>(memory.GetSize()), &bytesRead, NULL);
				pStreamStdout->Write(sig, buff, bytesRead);
			}
		}
		if (pStreamStderr != NULL) {
			DWORD bytesAvail;
			::PeekNamedPipe(hStderrWatch, NULL, NULL, NULL, &bytesAvail, NULL);
			if (bytesAvail > 0) {
				dataAvailFlag = true;
				void *buff = memory.GetPointer();
				DWORD bytesRead;
				::ReadFile(hStderrWatch, buff,
							static_cast<DWORD>(memory.GetSize()), &bytesRead, NULL);
				pStreamStderr->Write(sig, buff, bytesRead);
			}
		}
		if (!dataAvailFlag) {
			::GetExitCodeProcess(ps.hProcess, &exitCode);
			if (exitCode != STILL_ACTIVE) break;
			::Sleep(100);
		}
	}
	::CloseHandle(ps.hProcess);
	::CloseHandle(ps.hThread);
	::CloseHandle(hStdout);
	::CloseHandle(hStderr);
	::CloseHandle(hStdoutWatch);
	::CloseHandle(hStderrWatch);
	return static_cast<int>(exitCode);
}

#else

int ExecProgram(Environment &env, Signal sig, const char *pathName,
		const ValueList &valList, Stream *pStreamStdout, Stream *pStreamStderr)
{
	char **argv = new char *[2 + 1 + valList.size() + 1];
	int argc = 0;
	argv[argc++] = ::strdup("/bin/sh");
	argv[argc++] = ::strdup("-c");
	String cmdLine;
	cmdLine = pathName;
	foreach_const (ValueList, pValue, valList) {
		cmdLine += ' ';
		cmdLine += pValue->GetString();
	}
	argv[argc++] = ::strdup(cmdLine.c_str());
	argv[argc++] = NULL;
	int fdsStdout[2];
	int fdsStderr[2];
	::pipe(fdsStdout);
	::pipe(fdsStderr);
	pid_t pid = ::fork();
	if (pid == 0) {
		::dup2(fdsStdout[1], 1);
		::dup2(fdsStderr[1], 2);
		::close(fdsStdout[1]);
		::close(fdsStderr[1]);
		::execv("/bin/sh", argv);
		::exit(1);
	}
	fd_set fdsRead;
	OAL::Memory memory(32768);
	int exitCode = 0;
	for (;;) {
		timeval tv;
		::memset(&tv, 0x00, sizeof(tv));
		tv.tv_sec = 0;
		tv.tv_usec = 100 * 1000;
		FD_ZERO(&fdsRead);
		if (pStreamStdout != NULL) FD_SET(fdsStdout[0], &fdsRead);
		if (pStreamStderr != NULL) FD_SET(fdsStderr[0], &fdsRead);
		::select(ChooseMax(fdsStdout[0], fdsStderr[0]) + 1, &fdsRead, NULL, NULL, &tv);
		bool idleFlag = true;
		if (FD_ISSET(fdsStdout[0], &fdsRead)) {
			idleFlag = false;
			void *buff = memory.GetPointer();
			ssize_t bytesRead = ::read(fdsStdout[0], buff, memory.GetSize()); 
			pStreamStdout->Write(sig, buff, bytesRead);
			if (sig.IsSignalled()) return -1;
		}
		if (FD_ISSET(fdsStderr[0], &fdsRead)) {
			idleFlag = false;
			void *buff = memory.GetPointer();
			ssize_t bytesRead = ::read(fdsStderr[0], buff, memory.GetSize()); 
			pStreamStderr->Write(sig, buff, bytesRead);
			if (sig.IsSignalled()) return -1;
		}
		if (idleFlag) {
			int status;
			int rtn = waitpid(pid, &status, WNOHANG);
			if (rtn > 0 && WIFEXITED(status)) {
				exitCode = WEXITSTATUS(status);
				break;
			}
		}
	}
	for (int i = 0; i < 2; i++) {
		::close(fdsStdout[i]);
		::close(fdsStderr[i]);
	}
	for (int i = 0; i < argc; i++) ::free(argv[i]);
	delete[] argv;
	return exitCode;
}

#endif

AScript_EndModule(os, os)

AScript_RegisterModule(os)
