/*
	Commandline interpreter interface.
	Copyright (c) 2003-2006 stefan kersten.
	Copyright (c) 2013 tim blechmann.

	====================================================================

	SuperCollider real time audio synthesis system
	Copyright (c) 2002 James McCartney. All rights reserved.
	http://www.audiosynth.com

	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 2 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, write to the Free Software
	Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/

#include "SC_TerminalClient.h"
#ifdef SC_QT
#include "../../QtCollider/LanguageClient.h"
#endif

#include <boost/bind.hpp>

#ifdef _WIN32
# define __GNU_LIBRARY__
# include "getopt.h"
# include "SC_Win32Utils.h"
# include <io.h>
# include <windows.h>
# include <ioapiset.h>
# include <iostream> // for cerr
#endif

#ifdef __APPLE__
#include "../../common/SC_Apple.hpp"
#endif

#ifdef HAVE_READLINE
# include <readline/readline.h>
# include <readline/history.h>
# include <signal.h>
#endif

#include "GC.h"
#include "PyrKernel.h"
#include "PyrPrimitive.h"
#include "PyrLexer.h"
#include "PyrSlot.h"
#include "VMGlobals.h"
#include "SC_Filesystem.hpp"
#include "SC_LanguageConfig.hpp"
#include "SC_Version.hpp"

#include <boost/filesystem/operations.hpp>

static FILE* gPostDest = stdout;

#ifdef _WIN32
static UINT gOldCodePage; // for remembering the old codepage when we switch to UTF-8
#endif

SC_TerminalClient::SC_TerminalClient(const char* name)
	: SC_LanguageClient(name),
	  mReturnCode(0),
	  mUseReadline(false),
      mWork(mIoService),
	  mTimer(mIoService),
#ifndef _WIN32
	  mStdIn(mInputService, STDIN_FILENO)
#else
	  mStdIn(mInputService, GetStdHandle(STD_INPUT_HANDLE))
#endif
{}

SC_TerminalClient::~SC_TerminalClient()
{}

void SC_TerminalClient::postText(const char* str, size_t len)
{
	fwrite(str, sizeof(char), len, gPostDest);
}

void SC_TerminalClient::postFlush(const char* str, size_t len)
{
	fwrite(str, sizeof(char), len, gPostDest);
	fflush(gPostDest);
}

void SC_TerminalClient::postError(const char* str, size_t len)
{
	fprintf(gPostDest, "ERROR: ");
	fwrite(str, sizeof(char), len, gPostDest);
}

void SC_TerminalClient::flush()
{
	fflush(gPostDest);
}

void SC_TerminalClient::printUsage()
{
	Options opt;

	const size_t bufSize = 128;
	char memGrowBuf[bufSize];
	char memSpaceBuf[bufSize];

	snprintMemArg(memGrowBuf, bufSize, opt.mMemGrow);
	snprintMemArg(memSpaceBuf, bufSize, opt.mMemSpace);

	fprintf(stdout, "Usage:\n   %s [options] [file..] [-]\n\n", getName());
	fprintf(stdout,
			"Options:\n"
			"   -v                             Print supercollider version and exit\n"
			"   -d <path>                      Set runtime directory\n"
			"   -D                             Enter daemon mode (no input)\n"
			"   -g <memory-growth>[km]         Set heap growth (default %s)\n"
			"   -h                             Display this message and exit\n"
			"   -l <path>                      Set library configuration file\n"
			"   -m <memory-space>[km]          Set initial heap size (default %s)\n"
			"   -r                             Call Main.run on startup\n"
			"   -s                             Call Main.stop on shutdown\n"
			"   -u <network-port-number>       Set UDP listening port (default %d)\n"
			"   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default \"%s\")\n"
			"   -a                             Standalone mode (exclude SCClassLibrary and user and system Extensions folders from search path)\n",
			memGrowBuf,
			memSpaceBuf,
			opt.mPort,
			SC_Filesystem::instance().getIdeName().c_str()
		);
}

bool SC_TerminalClient::parseOptions(int& argc, char**& argv, Options& opt)
{
	const char* optstr = ":d:Dg:hl:m:rsu:i:av";
	int c;

	// inhibit error reporting
	opterr = 0;

	while ((c = getopt(argc, argv, optstr)) != -1) {
		switch (c) {
			case 'd':
				opt.mRuntimeDir = optarg;
				break;
			case 'D':
				opt.mDaemon = true;
				break;
			case 'g':
				if (!parseMemArg(optarg, &opt.mMemGrow)) {
					optopt = c;
					goto optArgInvalid;
				}
				break;
			case 'h':
				goto help;
			case 'l':
				opt.mLibraryConfigFile = optarg;
				break;
			case 'm':
				if (!parseMemArg(optarg, &opt.mMemSpace)) {
					optopt = c;
					goto optArgInvalid;
				}
				break;
			case 'r':
				opt.mCallRun = true;
				break;
			case 'v':
				fprintf(stdout, "sclang %s (%s)\n", SC_VersionString().c_str(), SC_BuildString().c_str());
				quit(0);
				return false;
				break;
			case 's':
				opt.mCallStop = true;
				break;
			case 'u':
				if (!parsePortArg(optarg, &opt.mPort)) {
					optopt = c;
					goto optArgInvalid;
				}
				break;
			case '?':
				goto optInvalid;
				break;
			case ':':
				goto optArgExpected;
				break;
			case 'i':
				SC_Filesystem::instance().setIdeName(optarg);
				break;
			case 'a':
				opt.mStandalone = true;
				break;
			default:
				::post("%s: unknown error (getopt)\n", getName());
				quit(255);
				return false;
		}
	}

	argv += optind;
	argc -= optind;

	return true;

 help:
	printUsage();
	quit(0);
	return false;

 optInvalid:
	::post("%s: invalid option -%c\n", getName(), optopt);
	quit(1);
	return false;

 optArgExpected:
	::post("%s: missing argument for option -%c\n", getName(), optopt);
	quit(1);
	return false;

 optArgInvalid:
	::post("%s: invalid argument for option -%c -- %s\n", getName(), optopt, optarg);
	quit(1);
	return false;
}

int SC_TerminalClient::run(int argc, char** argv)
{
	Options& opt = mOptions;

	if (!parseOptions(argc, argv, opt)) {
		return mReturnCode;
	}

	// finish argv processing
	const char* codeFile = 0;

	if (argc > 0) {
		codeFile = argv[0];
		opt.mDaemon = true;
		argv++; argc--;
	}

	opt.mArgc = argc;
	opt.mArgv = argv;

	// read library configuration file
	if (opt.mLibraryConfigFile)
		SC_LanguageConfig::setConfigPath(opt.mLibraryConfigFile);
	SC_LanguageConfig::readLibraryConfig(opt.mStandalone);

	// initialize runtime
	initRuntime(opt);

	// Create config directory so that it can be used by Quarks, etc. See #2919.
	if (!opt.mStandalone && !opt.mLibraryConfigFile)
		boost::filesystem::create_directories(
			SC_Filesystem::instance().getDirectory(SC_Filesystem::DirName::UserConfig));

	// startup library
	compileLibrary(opt.mStandalone);

	// enter main loop
	if (codeFile) executeFile(codeFile);
	if (opt.mCallRun) runMain();

	if (opt.mDaemon) {
		daemonLoop();
	}
	else {
		initInput();
		startInput();
		commandLoop();
		endInput();
		cleanupInput();
	}

	if (opt.mCallStop) stopMain();

	// shutdown library
	shutdownLibrary();
	flush();

	shutdownRuntime();

	return mReturnCode;
}

void SC_TerminalClient::recompileLibrary()
{
    SC_LanguageClient::recompileLibrary(mOptions.mStandalone);
}

void SC_TerminalClient::quit(int code)
{
	mReturnCode = code;
}

static PyrSymbol * resolveMethodSymbol(bool silent)
{
	if (silent)
		return s_interpretCmdLine;
	else
		return s_interpretPrintCmdLine;
}

void SC_TerminalClient::interpretCmdLine(const char* cmdLine, bool silent)
{
	setCmdLine(cmdLine);
	runLibrary(resolveMethodSymbol(silent));
	flush();
}


void SC_TerminalClient::interpretCmdLine(const char *cmdLine, size_t size, bool silent)
{
	setCmdLine(cmdLine, size);
	runLibrary(resolveMethodSymbol(silent));
	flush();
}

// Note: called only if the input thread does not perform an asynchronous read operation
void SC_TerminalClient::interpretInput()
{
	char *data = mInputBuf.getData();
	int c = mInputBuf.getSize();
	int i = 0;
	while( i < c ) {
		switch (data[i]) {
		case kInterpretCmdLine:
			interpretCmdLine(data, i, true);
			break;
		case kInterpretPrintCmdLine:
			interpretCmdLine(data, i, false);
			break;

		case kRecompileLibrary:
			recompileLibrary();
			break;

		default:
			++i;
			continue;
		}

		data += i+1;
		c -= i+1;
		i = 0;
	}
	mInputBuf.reset();

	if (mUseReadline)
		mReadlineSem.post();
	else
		startInputRead();
}

void SC_TerminalClient::onLibraryStartup()
{
	SC_LanguageClient::onLibraryStartup();
	int base, index = 0;
	base = nextPrimitiveIndex();
	definePrimitive(base, index++, "_Argv", &SC_TerminalClient::prArgv, 1, 0);
	definePrimitive(base, index++, "_Exit", &SC_TerminalClient::prExit, 1, 0);
	definePrimitive(base, index++, "_AppClock_SchedNotify",
		&SC_TerminalClient::prScheduleChanged, 1, 0);
	definePrimitive(base, index++, "_Recompile", &SC_TerminalClient::prRecompile, 1, 0);
}

void SC_TerminalClient::sendSignal( Signal sig )
{
	switch (sig) {
	case sig_input:
		mIoService.post( boost::bind(&SC_TerminalClient::interpretInput, this) );
		break;

	case sig_recompile:
		mIoService.post( boost::bind(&SC_TerminalClient::recompileLibrary, this) );
		break;

	case sig_sched:
		mIoService.post( boost::bind(&SC_TerminalClient::tick, this, boost::system::error_code()) );
		break;

	case sig_stop:
		mIoService.post( boost::bind(&SC_TerminalClient::stopMain, this) );
		break;
	}
}

void SC_TerminalClient::onQuit( int exitCode )
{
	postfl("main: quit request %i\n", exitCode);
	quit( exitCode );
	stop();
}

extern void ElapsedTimeToChrono(double elapsed, std::chrono::system_clock::time_point & out_time_point);

/**
 * \brief Repeatedly calls the main AppClock loop.
 *
 * This method does the following:
 *
 * - Wraps \c tickLocked (locking \c gLangMutex before and after), which in
 *   turn calls \c runLibrary.
 * - Schedules to call itself again using a \c boost::asio::basic_waitable_timer
 *   (\c mTimer).
 *
 * Since it calls itself, a single call to \c tick will cause the method to
 * keep calling itself autonomously.
 *
 * If \c tick is called again externally while a timer is running, any
 * previously scheduled \c tick call is canceled. This forces a premature \c
 * tick, which will schedule itself again, and so on.
 *
 * The timed calls to \c tick are used for events scheduled on the AppClock.
 * The interruption feature is used when sclang receives unanticipated events
 * such as inbound OSC messages.
 */
void SC_TerminalClient::tick( const boost::system::error_code& error )
{
	/*
	Even if the timeout is canceled, this callback will always fire (see docs
	for boost::asio::basic_waitable_timer).

	Distinguishing between a canceled and successful callback is done by
	inspecting the error. If it turns out this method was called because of a
	cancelled timer, we need to bail out and let the tick call that
	interrupted us take over.

	If we aren't the result of a canceled timer call, we're either the result
	of a scheduled timer expiry, or we *are* the interruption and we need to
	cancel any scheduled tick calls. Calling mTimer.cancel() if the error is
	a success error code handles both cases.

	Previously, instead of this if/else block, this was just a call to
	mTimer.cancel(). This was causing this method to rapidly call itself
	excessively, hogging the CPU. See discussion at #2144.
	*/
	if (error == boost::system::errc::success) {
		mTimer.cancel();
	} else {
		return;
	}

	double secs;
	lock();
	bool haveNext = tickLocked( &secs );
	unlock();

	flush();

	std::chrono::system_clock::time_point nextAbsTime;
	ElapsedTimeToChrono( secs, nextAbsTime );

	if (haveNext) {
		mTimer.expires_at(nextAbsTime);
		mTimer.async_wait(boost::bind(&SC_TerminalClient::tick, this, _1));
	}
}

void SC_TerminalClient::commandLoop()
{
	mIoService.run();
}

void SC_TerminalClient::daemonLoop()
{
	commandLoop();
}

#ifdef HAVE_READLINE

static void sc_rl_cleanlf(void)
{
	rl_reset_line_state();
	rl_crlf();
	rl_redisplay();
}

static void sc_rl_signalhandler(int sig)
{
	// ensure ctrl-C clears line rather than quitting (ctrl-D will quit nicely)
	rl_replace_line("", 0);
	sc_rl_cleanlf();
}

static int sc_rl_mainstop(int i1, int i2)
{
	static_cast<SC_TerminalClient*>(SC_LanguageClient::instance())
		->sendSignal( SC_TerminalClient::sig_stop );
	sc_rl_cleanlf(); // We also push a newline so that there's some UI feedback
	return 0;
}

/*
// Completion from sclang dictionary TODO
char ** sc_rl_completion (const char *text, int start, int end);
char ** sc_rl_completion (const char *text, int start, int end){
	char **matches = (char **)NULL;
	printf("sc_rl_completion(%s, %i, %i)\n", text, start, end);
	return matches;
}
*/

int SC_TerminalClient::readlineRecompile(int i1, int i2)
{
	static_cast<SC_TerminalClient*>(SC_LanguageClient::instance())->sendSignal(sig_recompile);
	sc_rl_cleanlf();
	return 0;
}

void SC_TerminalClient::readlineCmdLine( char *cmdLine )
{
	SC_TerminalClient *client = static_cast<SC_TerminalClient*>(instance());

	if( cmdLine == NULL ) {
		postfl("\nExiting sclang (ctrl-D)\n");
		client->onQuit(0);
		return;
	}

	if( *cmdLine != 0 ) {
		// If line wasn't empty, store it so that uparrow retrieves it
		add_history(cmdLine);
		int len = strlen(cmdLine);

		client->mInputBuf.append(cmdLine, len);
		client->mInputBuf.append(kInterpretPrintCmdLine);
		client->sendSignal(sig_input);
		client->mReadlineSem.wait();
	}
}

void SC_TerminalClient::readlineInit()
{
	// Setup readline
	rl_readline_name = "sclang";
	rl_basic_word_break_characters = " \t\n\"\\'`@><=;|&{}().";
	//rl_attempted_completion_function = sc_rl_completion;
	rl_bind_key(CTRL('t'), &sc_rl_mainstop);
	rl_bind_key(CTRL('x'), &readlineRecompile);
	rl_callback_handler_install( "sc3> ", &readlineCmdLine );

	// FIXME: Implement the code below on Windows
#ifndef _WIN32
	// Set our handler for SIGINT that will clear the line instead of terminating.
	// NOTE: We prevent readline from setting its own signal handlers,
	// to not override ours.
	rl_catch_signals = 0;
	struct sigaction sact;
	memset( &sact, 0, sizeof(struct sigaction) );
	sact.sa_handler = &sc_rl_signalhandler;
	sigaction( SIGINT, &sact, 0 );
#endif
}

#endif // HAVE_READLINE

void SC_TerminalClient::startInputRead()
{
#ifndef _WIN32
	if (mUseReadline)
		mStdIn.async_read_some(boost::asio::null_buffers(), boost::bind(&SC_TerminalClient::onInputRead, this, _1, _2));
	else
		mStdIn.async_read_some(boost::asio::buffer(inputBuffer), boost::bind(&SC_TerminalClient::onInputRead, this, _1, _2));
#else
	mStdIn.async_wait( [&] (const boost::system::error_code & error) {
		if(error)
			onInputRead(error, 0);
		else {
			DWORD bytes_transferred;

			::ReadFile(GetStdHandle(STD_INPUT_HANDLE),
					   inputBuffer.data(),
					   inputBuffer.size(),
					   &bytes_transferred,
					   nullptr);

			onInputRead(error, bytes_transferred);
		}
	});
#endif
}

void SC_TerminalClient::onInputRead(const boost::system::error_code &error, std::size_t bytes_transferred)
{
	if (error == boost::asio::error::operation_aborted) {
		postfl("SCLang Input: Quit requested\n");
		return;
	}

	if (error == boost::asio::error::eof) {
		postfl("SCLang Input: EOF. Will quit.\n");
		onQuit(0);
		return;
	}

	if (error) {
		postfl("SCLang Input: %s.\n", error.message().c_str());
		onQuit(1);
		return;
	}

	if (!error) {
#if HAVE_READLINE
		if (mUseReadline) {
			rl_callback_read_char();
			startInputRead();
			return;
		}
#endif
		pushCmdLine( inputBuffer.data(), bytes_transferred );
	}
}

void SC_TerminalClient::inputThreadFn()
{
#if HAVE_READLINE
	if (mUseReadline)
		readlineInit();
#endif

	startInputRead();

	boost::asio::io_service::work work(mInputService);
	mInputService.run();
}


void SC_TerminalClient::pushCmdLine( const char *newData, size_t size)
{
	bool signal = false;
	while (size--) {
		char c = *newData++;
		switch (c) {
		case kRecompileLibrary:
		case kInterpretCmdLine:
		case kInterpretPrintCmdLine:
			mInputBuf.append( mInputThrdBuf.getData(), mInputThrdBuf.getSize() );
			mInputBuf.append(c);
			signal = true;
			mInputThrdBuf.reset();
			break;

		default:
			mInputThrdBuf.append(c);
		}
	}

	if (signal)
		sendSignal(sig_input);
	else
		startInputRead();
}


void SC_TerminalClient::initInput()
{
#ifdef HAVE_READLINE
	if (!SC_Filesystem::instance().usingIde()) {
		// Other clients (emacs, vim, ...) won't want to interact through rl
		mUseReadline = true;
		return;
	}
#endif
}


void SC_TerminalClient::startInput()
{
	SC_Thread thread(std::bind(&SC_TerminalClient::inputThreadFn, this));
	mInputThread = std::move(thread);
}

void SC_TerminalClient::endInput()
{
	mInputService.stop();
	mStdIn.cancel();
#ifdef _WIN32
	// Note this breaks Windows XP compatibility, since this function is only defined in Vista and later
	::CancelIoEx(GetStdHandle(STD_INPUT_HANDLE), nullptr);
#endif
	postfl("main: waiting for input thread to join...\n");
	mInputThread.join();
	postfl("main: quitting...\n");
}

void SC_TerminalClient::cleanupInput()
{
#ifdef HAVE_READLINE
	if( mUseReadline ) rl_callback_handler_remove();
#endif
}

int SC_TerminalClient::prArgv(struct VMGlobals* g, int)
{
	int argc = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgc;
	char** argv = ((SC_TerminalClient*)SC_TerminalClient::instance())->options().mArgv;

	PyrSlot* argvSlot = g->sp;

	PyrObject* argvObj = newPyrArray(g->gc, argc * sizeof(PyrObject), 0, true);
	SetObject(argvSlot, argvObj);

	for (int i=0; i < argc; i++) {
		PyrString* str = newPyrString(g->gc, argv[i], 0, true);
		SetObject(argvObj->slots+i, str);
		argvObj->size++;
		g->gc->GCWriteNew(argvObj, (PyrObject*)str); // we know str is white so we can use GCWriteNew
	}

	return errNone;
}

int SC_TerminalClient::prExit(struct VMGlobals* g, int)
{
	int code;

	int err = slotIntVal(g->sp, &code);
	if (err) return err;

	((SC_TerminalClient*)SC_LanguageClient::instance())->onQuit( code );

	return errNone;
}

int SC_TerminalClient::prScheduleChanged( struct VMGlobals *g, int numArgsPushed)
{
	static_cast<SC_TerminalClient*>(instance())->sendSignal(sig_sched);
	return errNone;
}

int SC_TerminalClient::prRecompile(struct VMGlobals *, int)
{
	static_cast<SC_TerminalClient*>(instance())->sendSignal(sig_recompile);
	return errNone;
}

SCLANG_DLLEXPORT SC_LanguageClient * createLanguageClient(const char * name)
{
	if (SC_LanguageClient::instance())
		return NULL;

#ifdef __APPLE__
	SC::Apple::disableAppNap();
#endif

#ifdef _WIN32
	// set codepage to UTF-8
	gOldCodePage = GetConsoleOutputCP();
	if (!SetConsoleOutputCP(65001))
		std::cerr << "WARNING: could not set codepage to UTF-8" << std::endl;
#endif

#ifdef SC_QT
	return new QtCollider::LangClient(name);
#else
	return new SC_TerminalClient(name);
#endif
}

SCLANG_DLLEXPORT void destroyLanguageClient(class SC_LanguageClient * languageClient)
{
#ifdef _WIN32
	// reset codepage from UTF-8
	SetConsoleOutputCP(gOldCodePage);
#endif
	delete languageClient;
}
