/*
    libmaus2
    Copyright (C) 2009-2013 German Tischler
    Copyright (C) 2011-2013 Genome Research Limited

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <libmaus2/posix/PosixFunctions.hpp>
#include <libmaus2/posix/PosixExecute.hpp>
#include <string>
#include <cerrno>
#include <memory>
#include <iostream>
#include <cstring>
#include <vector>
#include <cstdio>

#if defined(LIBMAUS2_HAVE_UNISTD_H)
#include <unistd.h>
#endif

std::mutex libmaus2::posix::PosixExecute::lock;

#if defined(LIBMAUS2_HAVE_FORK)
template<typename _type>
struct LocalAutoArray
{
	typedef _type type;

	size_t const n;
	type * const A;

	LocalAutoArray(size_t const rn)
	: n(rn), A(new type[n])
	{
	}
	~LocalAutoArray()
	{
		delete [] A;
	}

	type * get()
	{
		return A;
	}

	type const * get() const
	{
		return A;
	}

	type * begin()
	{
		return get();
	}

	type const * begin() const
	{
		return get();
	}

	size_t size() const
	{
		return n;
	}

	type * end()
	{
		return begin()+size();
	}

	type const * end() const
	{
		return begin()+size();
	}

	type & operator[](size_t const i)
	{
		return A[i];
	}

	type const & operator[](size_t const i) const
	{
		return A[i];
	}
};

static int doClose(int fd)
{
	while ( true )
	{
		int const r = libmaus2::posix::PosixFunctions::close(fd);

		if ( r < 0 )
		{
			switch ( errno )
			{
				case EINTR:
					break;
				default:
					return r;
			}
		}
		else
		{
			return r;
		}
	}
}

int parseEscapeCode(std::string const & command, size_t const j)
{
	if ( j+1 < command.size() )
	{
		switch ( command[j+1] )
		{
			case '0':
				// extend this to general numbers
				return (0);
				break;
			case 'a':
				return ('\a');
				break;
			case 'b':
				return ('\b');
				break;
			case 't':
				return ('\t');
				break;
			case 'n':
				return ('\n');
				break;
			case 'v':
				return ('\v');
				break;
			case 'f':
				return ('\f');
				break;
			case 'r':
				return ('\r');
				break;
			default:
				return -1;
		}
	}
	else
	{
		return -1;
	}
}

std::vector<std::string> parseCommand(std::string const & command)
{
	uint64_t i = 0;
	std::vector<std::string> V;

	while ( i < command.size() )
	{
		while ( i < command.size() && isspace(command[i]) )
			++i;

		uint64_t j = i;

		std::ostringstream ostr;
		while ( j < command.size() && !isspace(command[j]) )
		{
			if ( command[j] == '\\' )
			{
				int const r = parseEscapeCode(command,j);
				if ( r >= 0 )
					ostr.put(r);
				j += 2;
			}
			else if ( command[j] == '"' )
			{
				++j;
				while ( j < command.size() && command[j] != '"' )
				{
					if ( command[j] == '\\' )
					{
						int const r = parseEscapeCode(command,j);
						if ( r >= 0 )
							ostr.put(r);
						j += 2;
					}
					else
					{
						ostr.put(command[j++]);
					}
				}
				++j;
			}
			else if ( command[j] == '\'' )
			{
				++j;
				while ( j < command.size() && command[j] != '\'' )
					ostr.put(command[j++]);
				++j;
			}
			else
			{
				ostr.put(command[j++]);
			}
		}

		std::string const s = ostr.str();
		if ( s.size() )
			V.push_back(s);

		i = j;
	}

	return V;
}

static uint64_t tmpid = 0;

static void fillId(char * p, uint64_t id, char const sym)
{
	// look for first Z
	while ( *p && *p != sym )
		++p;

	// if any
	if ( *p )
	{
		// this should be Z
		assert ( *p == sym );

		// count number of Z
		uint64_t numz = 0;
		while ( *p && *p == sym )
		{
			++p;
			++numz;
		}

		// fill Z area
		while ( numz-- )
		{
			*(--p) = '0' + (id % 10);
			id /= 10;
		}
	}
}

static void fillId(char * p, std::string const & id, char const sym)
{
	// look for first Z
	while ( *p && *p != sym )
		++p;

	// if any
	if ( *p )
	{
		// this should be Z
		assert ( *p == sym );

		// count number of Z
		uint64_t numz = 0;
		while ( *p && *p == sym )
		{
			++p;
			++numz;
		}

		p -= numz;

		if ( numz >= id.size() )
		{
			uint64_t pad = numz - id.size();

			while ( pad-- )
				*(p++) = '0';

			std::copy(id.begin(),id.end(),p);
		}
		else
		{
			assert ( id.size() > numz );
			uint64_t const idskip = id.size()-numz;

			std::copy(id.begin()+idskip,id.end(),p);
		}
	}
}
#endif

int libmaus2::posix::PosixExecute::execute(
	std::string const &
		#if defined(LIBMAUS2_HAVE_FORK)
		command
		#endif
		,
	std::string &
		#if defined(LIBMAUS2_HAVE_FORK)
		out
		#endif
		,
	std::string &
		#if defined(LIBMAUS2_HAVE_FORK)
		err
		#endif
		,
	std::ostream &
		errchan
		= std::cerr
)
{
	try
	{
		#if defined(LIBMAUS2_HAVE_FORK)
		std::vector<std::string> V = parseCommand(command);
		std::ostringstream debugstr;

		std::lock_guard<std::mutex> slock(libmaus2::posix::PosixExecute::lock);
		char stderrfn[] = "/tmp/libmaus2::posix::PosixExecute::execute_err_IIIIIIIIIIIIIII_ZZZZZZZZZZZZ_XXXXXX";
		char stdoutfn[] = "/tmp/libmaus2::posix::PosixExecute::execute_out_IIIIIIIIIIIIIII_ZZZZZZZZZZZZ_XXXXXX";
		int returncode = EXIT_SUCCESS;
		bool stderrfnvalid = false;
		bool stdoutfnvalid = false;

		int stderrfd = -1;
		int stdoutfd = -1;
		int nullfd = -1;
		char ** argptrs = nullptr;

		char * argmem = nullptr;
		size_t argmemsize = 0;
		char * argmemt = nullptr;
		int error = 0;
		char const * failedsyscall = nullptr;
		char * tempmemerr = nullptr;
		char * tempmemout = nullptr;
		int64_t staterr_st_size = -1;
		int64_t statout_st_size = -1;
		size_t errread = 0;
		size_t outread = 0;
		unsigned char * childpidmem = nullptr;
		unsigned char * childopidmem = nullptr;
		unsigned char * off_t_in_mem = nullptr;
		unsigned char * off_t_out_mem = nullptr;

		uint64_t thistmpid = tmpid++;

		std::string const smypid = libmaus2::posix::PosixFunctions::getPidAsString();

		fillId(&stderrfn[0],thistmpid,'Z');
		fillId(&stdoutfn[0],thistmpid,'Z');
		fillId(&stderrfn[0],smypid,'I');
		fillId(&stdoutfn[0],smypid,'I');

		for ( uint64_t i = 0; i < V.size(); ++i )
		{
			// errchan << "command[" << i << "]=" << V[i] << std::endl;
			argmemsize += V[i].size()+1;
		}

		off_t_in_mem = (unsigned char *)malloc(libmaus2::posix::PosixFunctions::getOffTSize());
		if ( !off_t_in_mem )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}

		off_t_out_mem = (unsigned char *)malloc(libmaus2::posix::PosixFunctions::getOffTSize());
		if ( !off_t_out_mem )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}

		childpidmem = (unsigned char *)malloc(libmaus2::posix::PosixFunctions::getPidTSize());
		if ( !childpidmem )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}

		childopidmem = (unsigned char *)malloc(libmaus2::posix::PosixFunctions::getPidTSize());
		if ( !childopidmem )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}

		argmem = (char *)malloc(argmemsize);

		if ( ! argmem )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}


		memset(argmem,0,argmemsize);

		argptrs = (char **)malloc((V.size()+1) * sizeof(char *));

		if ( ! argptrs )
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "malloc";
			goto cleanup;
		}

		memset(argptrs,0,(V.size()+1) * sizeof(char *));

		argmemt = argmem;
		for ( uint64_t i = 0; i < V.size(); ++i )
		{
			memcpy(argmemt,V[i].c_str(),V[i].size());
			argptrs[i] = argmemt;
			argmemt += V[i].size()+1;
		}

		if ( (stderrfd = mkstemp(&stderrfn[0])) < 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "mkstemp";
			goto cleanup;
		}
		else
		{
			stderrfnvalid = true;
		}

		if ( (stdoutfd = mkstemp(&stdoutfn[0])) < 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "mkstemp";
			goto cleanup;
		}
		else
		{
			stdoutfnvalid = true;
		}

		if ( (nullfd = libmaus2::posix::PosixFunctions::open("/dev/null",libmaus2::posix::PosixFunctions::get_O_RDONLY())) < 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "libmaus2::posix::PosixFunctions::open(/dev/null)";
			goto cleanup;
		}

		// child = fork();
		libmaus2::posix::PosixFunctions::fork(childpidmem);

		if ( libmaus2::posix::PosixFunctions::pidIsMinusOne(childpidmem) )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "fork()";
			goto cleanup;
		}
		else if ( libmaus2::posix::PosixFunctions::pidIsZero(childpidmem) )
		{
			try
			{
				if ( doClose(STDOUT_FILENO) < 0 )
					return EXIT_FAILURE;
				if ( doClose(STDERR_FILENO) < 0 )
					return EXIT_FAILURE;
				if ( dup2(nullfd,STDIN_FILENO) < 0 )
					return EXIT_FAILURE;
				if ( dup2(stdoutfd,STDOUT_FILENO) < 0 )
					return EXIT_FAILURE;
				if ( dup2(stderrfd,STDERR_FILENO) < 0 )
					return EXIT_FAILURE;

				execvp(argptrs[0], argptrs);

				doClose(STDIN_FILENO);
				doClose(STDOUT_FILENO);
				doClose(STDERR_FILENO);

				_exit(EXIT_FAILURE);
			}
			catch(...)
			{
				_exit(EXIT_FAILURE);
			}
		}

		while ( true )
		{
			int status = 0;

			//pid_t const r = waitpid(child, &status, 0);
			libmaus2::posix::PosixFunctions::waitpid(
				childopidmem,
				childpidmem,
				&status,
				0
			);

			if ( libmaus2::posix::PosixFunctions::pidIsMinusOne(childopidmem) )
			{
				int const lerror = errno;

				if ( lerror == EAGAIN || lerror == EINTR )
				{

				}
				else
				{
					returncode = EXIT_FAILURE;
					error = errno;
					failedsyscall = "waitpid()";
					goto cleanup;
				}
			}
			else if ( memcmp(childpidmem,childopidmem,libmaus2::posix::PosixFunctions::getPidTSize()) == 0 )
			{
				if ( WIFEXITED(status) )
				{
					returncode = WEXITSTATUS(status);
				}
				else
				{
					returncode = EXIT_FAILURE;
				}

				break;
			}
		}

		::memset(off_t_in_mem,0,libmaus2::posix::PosixFunctions::getOffTSize());
		libmaus2::posix::PosixFunctions::posix_lseek(off_t_out_mem,stderrfd,off_t_in_mem,libmaus2::posix::PosixFunctions::get_SEEK_SET());

		if ( memcmp(off_t_in_mem,off_t_out_mem,libmaus2::posix::PosixFunctions::getOffTSize()) != 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "lseek()";
			goto cleanup;
		}

		::memset(off_t_in_mem,0,libmaus2::posix::PosixFunctions::getOffTSize());
		libmaus2::posix::PosixFunctions::posix_lseek(off_t_out_mem,stdoutfd,off_t_in_mem,libmaus2::posix::PosixFunctions::get_SEEK_SET());

		if ( memcmp(off_t_in_mem,off_t_out_mem,libmaus2::posix::PosixFunctions::getOffTSize()) != 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "lseek()";
			goto cleanup;
		}


		if ( (staterr_st_size = libmaus2::posix::PosixFunctions::getFileSizeViaFstat(stderrfd)) < 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "fstat()";
			goto cleanup;
		}
		if ( (statout_st_size = libmaus2::posix::PosixFunctions::getFileSizeViaFstat(stdoutfd)) < 0 )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "fstat()";
			goto cleanup;
		}

		if ( (tempmemerr = (char *)malloc(staterr_st_size)) == nullptr )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "malloc()";
			goto cleanup;
		}
		if ( (tempmemout = (char *)malloc(statout_st_size)) == nullptr )
		{
			returncode = EXIT_FAILURE;
			error = errno;
			failedsyscall = "malloc()";
			goto cleanup;
		}

		while ( static_cast< ::libmaus2::ssize_t>(errread) < static_cast< ::libmaus2::ssize_t>(staterr_st_size) )
		{
			size_t toread = staterr_st_size - errread;
			::libmaus2::ssize_t const r = ::libmaus2::posix::PosixFunctions::read(stderrfd,tempmemerr+errread,toread);

			if ( r < 0 )
			{
				if ( errno == EAGAIN || errno == EINTR )
				{

				}
				else
				{
					returncode = EXIT_FAILURE;
					error = errno;
					failedsyscall = "read(stderrfd,failed)";
					goto cleanup;
				}
			}
			else if ( r == 0 )
			{
				returncode = EXIT_FAILURE;
				error = errno;
				failedsyscall = "read(stderrfd,eof)";
				goto cleanup;
			}
			else
			{
				errread += r;
			}
		}
		while ( static_cast< ::libmaus2::ssize_t>(outread) < static_cast< ::libmaus2::ssize_t>(statout_st_size) )
		{
			size_t toread = statout_st_size - outread;
			::libmaus2::ssize_t const r = ::libmaus2::posix::PosixFunctions::read(stdoutfd,tempmemout+outread,toread);

			if ( r < 0 )
			{
				if ( errno == EAGAIN || errno == EINTR )
				{

				}
				else
				{
					returncode = EXIT_FAILURE;
					error = errno;
					failedsyscall = "read(stdoutfd,failed)";
					goto cleanup;
				}
			}
			else if ( r == 0 )
			{
				returncode = EXIT_FAILURE;
				error = errno;
				failedsyscall = "read(stdoutfd,EOF)";
				goto cleanup;
			}
			else
			{
				outread += r;
			}
		}

		try
		{
			out = std::string(statout_st_size,' ');
			err = std::string(staterr_st_size,' ');

			for ( ::libmaus2::ssize_t i = 0; i < statout_st_size; ++i )
				out[i] = tempmemout[i];
			for ( ::libmaus2::ssize_t i = 0; i < staterr_st_size; ++i )
				err[i] = tempmemerr[i];
		}
		catch(...)
		{
			returncode = EXIT_FAILURE;
			error = ENOMEM;
			failedsyscall = "std::string alloc/copy";
			goto cleanup;
		}

		cleanup:
		if ( off_t_in_mem )
		{
			::free(off_t_in_mem);
			off_t_in_mem = nullptr;
		}
		if ( off_t_out_mem )
		{
			::free(off_t_out_mem);
			off_t_out_mem = nullptr;
		}
		if ( childopidmem )
		{
			::free(childopidmem);
			childopidmem = nullptr;
		}
		if ( childpidmem )
		{
			::free(childpidmem);
			childpidmem = nullptr;
		}
		if ( argmem )
		{
			::free(argmem);
			argmem = nullptr;
		}

		if ( argptrs )
		{
			::free(argptrs);
			argptrs = nullptr;
		}

		if ( stderrfd >= 0 )
		{
			doClose(stderrfd);
			stderrfd = -1;
		}
		if ( stdoutfd >= 0 )
		{
			doClose(stdoutfd);
			stdoutfd = -1;
		}
		if ( nullfd >= 0 )
		{
			doClose(nullfd);
			nullfd = -1;
		}

		if ( stderrfnvalid )
			remove(&stderrfn[0]);
		if ( stdoutfnvalid )
			remove(&stdoutfn[0]);

		if ( tempmemerr )
		{
			free(tempmemerr);
			tempmemerr = nullptr;
		}
		if ( tempmemout )
		{
			free(tempmemout);
			tempmemout = nullptr;
		}

		if ( returncode != EXIT_SUCCESS )
		{
			try
			{
				if ( error == 0 )
				{
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow: \"" << command << "\" exited with status " << returncode << std::endl;
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow: failing command was " << command << std::endl;
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow: failed syscall " << failedsyscall << std::endl;
				}
				else
				{
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow, failed: " << strerror(error) << std::endl;
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow: failing command was " << command << std::endl;
					errchan << "libmaus2::posix::PosixExecute::execute() donotthrow: failed syscall " << failedsyscall << std::endl;
					errchan << "&stdoutfn[0]=" << &stdoutfn[0] << std::endl;
					errchan << "debugstr\n" << debugstr.str() << std::endl;
				}
			}
			catch(...)
			{
			}
		}

		return returncode;
		#else
		errchan << "PosixExecute: system is missing support for fork or pipe" << std::endl;
		return EXIT_FAILURE;
		#endif
	}
	catch(std::exception const & ex)
	{
		errchan << "PosixExecute: caught exception " << ex.what() << std::endl;
		return EXIT_FAILURE;
	}
}
