/*
    libmaus2
    Copyright (C) 2017 German Tischler

    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/util/Command.hpp>
#include <libmaus2/util/WriteableString.hpp>
#include <libmaus2/util/GetFileSize.hpp>
#include <libmaus2/timing/RealTimeClock.hpp>
#include <libmaus2/aio/InputOutputStreamInstance.hpp>

#if defined(__APPLE__)
#include <crt_externs.h>
#endif

#if defined(__NetBSD__) || defined(__FreeBSD__)
extern char **environ;
#endif

static char ** getEnviron()
{
	#if defined(__APPLE__)
	return *_NSGetEnviron();
	#else
	return environ;
	#endif
}

int libmaus2::util::Command::dispatch(std::string const & fn) const
{
	{
		libmaus2::aio::OutputStreamInstance OSI(fn);
		OSI << script;
		OSI.flush();

		if ( ! OSI )
		{
			libmaus2::exception::LibMausException lme;
			lme.getStream() << "[E] failed to write script " << fn << std::endl;
			lme.finish();
			throw lme;
		}
	}

	std::vector<std::string> args;
	args.push_back(shell);
	args.push_back(fn);

	libmaus2::autoarray::AutoArray < libmaus2::util::WriteableString::unique_ptr_type > AW(args.size());
	for ( uint64_t i = 0; i < args.size(); ++i )
	{
		libmaus2::util::WriteableString::unique_ptr_type tptr(new libmaus2::util::WriteableString(args[i]));
		AW[i] = std::move(tptr);
	}
	libmaus2::autoarray::AutoArray < char * > AA(args.size()+1);
	for ( uint64_t i = 0; i < args.size(); ++i )
		AA[i] = AW[i]->A.begin();
	AA[args.size()] = 0;

	std::string const com = args[0];

	libmaus2::timing::RealTimeClock rtc;
	rtc.start();

	libmaus2::autoarray::AutoArray<unsigned char> Apid(libmaus2::posix::PosixFunctions::getPidTSize());
	libmaus2::autoarray::AutoArray<unsigned char> Opid(libmaus2::posix::PosixFunctions::getPidTSize());
	libmaus2::posix::PosixFunctions::fork(Apid.begin());

	if ( libmaus2::posix::PosixFunctions::pidIsMinusOne(Apid.begin()) )
	{
		int const error = errno;
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "[E] fork failed: " << strerror(error) << std::endl;
		lme.finish();
		throw lme;
	}
	else if ( libmaus2::posix::PosixFunctions::pidIsZero(Apid.begin()) )
	{
		libmaus2::autoarray::AutoArray<unsigned char> Amode(libmaus2::posix::PosixFunctions::getOpenModeSize());
		libmaus2::posix::PosixFunctions::encode(0600,Amode.begin(),Amode.size());

		int const fdout = libmaus2::posix::PosixFunctions::open(out.c_str(),libmaus2::posix::PosixFunctions::get_O_CREAT()|libmaus2::posix::PosixFunctions::get_O_TRUNC()|libmaus2::posix::PosixFunctions::get_O_RDWR(),Amode.begin());
		if ( fdout == -1 )
		{
			int const error = errno;
			std::cerr << "[E] failed to open " << out << ":" << strerror(error) << std::endl;
			_exit(EXIT_FAILURE);
		}
		int const fderr = libmaus2::posix::PosixFunctions::open(err.c_str(),libmaus2::posix::PosixFunctions::get_O_CREAT()|libmaus2::posix::PosixFunctions::get_O_TRUNC()|libmaus2::posix::PosixFunctions::get_O_RDWR(),Amode.begin());
		if ( fderr == -1 )
		{
			int const error = errno;
			std::cerr << "[E] failed to open " << err << ":" << strerror(error) << std::endl;
			_exit(EXIT_FAILURE);
		}
		int const fdin = libmaus2::posix::PosixFunctions::open(in.c_str(),libmaus2::posix::PosixFunctions::get_O_RDONLY());
		if ( fdin == -1 )
		{
			int const error = errno;
			std::cerr << "[E] failed to open " << in << ":" << strerror(error) << std::endl;
			_exit(EXIT_FAILURE);
		}

		if ( libmaus2::posix::PosixFunctions::close(STDOUT_FILENO) == -1 )
			_exit(EXIT_FAILURE);
		if ( libmaus2::posix::PosixFunctions::close(STDERR_FILENO) == -1 )
			_exit(EXIT_FAILURE);
		if ( libmaus2::posix::PosixFunctions::close(STDIN_FILENO) == -1 )
			_exit(EXIT_FAILURE);

		if ( dup2 ( fdout, STDOUT_FILENO ) == -1 )
			_exit(EXIT_FAILURE);
		if ( dup2 ( fderr, STDERR_FILENO ) == -1 )
			_exit(EXIT_FAILURE);
		if ( dup2 ( fdin, STDIN_FILENO ) == -1 )
			_exit(EXIT_FAILURE);

		execve(com.c_str(),AA.begin(),getEnviron());
		_exit(EXIT_FAILURE);
	}

	int status = 0;
	libmaus2::posix::PosixFunctions::waitpid(Opid.begin(),Apid.begin(),&status,0/*options */);
	if ( libmaus2::posix::PosixFunctions::pidIsMinusOne(Opid.begin()) )
	{
		int const error = errno;
		libmaus2::exception::LibMausException lme;
		lme.getStream() << "[E] waitpid failed: " << strerror(error) << std::endl;
		lme.finish();
		throw lme;
	}

	double const etime = rtc.getElapsedSeconds();

	{
		libmaus2::aio::InputOutputStreamInstance IOSI(err,std::ios::in|std::ios::out|std::ios::binary);
		uint64_t const fs = libmaus2::util::GetFileSize::getFileSize(IOSI);
		IOSI.clear();
		IOSI.seekp(fs);

		IOSI << etime << "\t" << libmaus2::timing::RealTimeClock::formatTime(etime) << std::endl;
	}

	if ( ! ( libmaus2::posix::PosixFunctions::posix_WIFEXITED(status) && (libmaus2::posix::PosixFunctions::posix_WEXITSTATUS(status) == 0) ) )
	{
		libmaus2::exception::LibMausException lme;
		std::ostream & errstr = lme.getStream();

		errstr << "[E] " << args[0] << " failed" << std::endl;

		if ( libmaus2::posix::PosixFunctions::posix_WIFEXITED(status) )
		{
			errstr << "[E] exit code " << libmaus2::posix::PosixFunctions::posix_WEXITSTATUS(status) << std::endl;
		}

		lme.finish();

		throw lme;
	}

	return EXIT_SUCCESS;
}

std::ostream & libmaus2::util::operator<<(std::ostream & out, Command const & C)
{
	out << "Command("
		<< "in=" << C.in
		<< ",out=" << C.out
		<< ",err=" << C.err
		<< ",numattempts=" << C.numattempts
		<< ",maxattempts=" << C.maxattempts
		<< ",completed=" << C.completed
		<< ",ignorefail=" << C.ignorefail
		<< ",deepsleep=" << C.deepsleep
		<< ",modcall=" << C.modcall
		<< ",shell=" << C.shell
		<< ",script=" << C.script
		<< "])";
	return out;
}
