/*
 * synergy -- mouse and keyboard sharing utility
 * Copyright (C) 2002 Chris Schoeneman
 * 
 * This package is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * found in the file COPYING that should have accompanied this file.
 * 
 * This package 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.
 */

#include "CClient.h"
#include "ISecondaryScreenFactory.h"
#include "CPlatform.h"
#include "ProtocolTypes.h"
#include "Version.h"
#include "XScreen.h"
#include "CNetwork.h"
#include "CNetworkAddress.h"
#include "CTCPSocketFactory.h"
#include "XSocket.h"
#include "CCondVar.h"
#include "CLock.h"
#include "CMutex.h"
#include "CThread.h"
#include "XThread.h"
#include "CLog.h"
#include "CString.h"
#include <cstring>

#if WINDOWS_LIKE
#include "CMSWindowsSecondaryScreen.h"
#include "resource.h"
#elif UNIX_LIKE
#include "CXWindowsSecondaryScreen.h"
#endif

// platform dependent name of a daemon
#if WINDOWS_LIKE
#define DAEMON_NAME "Synergy Client"
#elif UNIX_LIKE
#define DAEMON_NAME "synergyc"
#endif

//
// program arguments
//

static const char*		pname         = NULL;
static bool				s_backend     = false;
static bool				s_restartable = true;
static bool				s_daemon      = true;
static bool				s_camp        = true;
static bool				s_install     = false;
static bool				s_uninstall   = false;
static const char*		s_logFilter   = NULL;
static CString			s_name;
static CNetworkAddress	s_serverAddress;


//
// logging thread safety
//

static CMutex*			s_logMutex = NULL;

static
void
logLock(bool lock)
{
	assert(s_logMutex != NULL);

	if (lock) {
		s_logMutex->lock();
	}
	else {
		s_logMutex->unlock();
	}
}


//
// platform dependent factories
//

class CSecondaryScreenFactory : public ISecondaryScreenFactory {
public:
	CSecondaryScreenFactory() { }
	virtual ~CSecondaryScreenFactory() { }

	// ISecondaryScreenFactory overrides
	virtual CSecondaryScreen*
						create(IScreenReceiver*);
};

CSecondaryScreen*
CSecondaryScreenFactory::create(IScreenReceiver* receiver)
{
#if WINDOWS_LIKE
	return new CMSWindowsSecondaryScreen(receiver);
#elif UNIX_LIKE
	return new CXWindowsSecondaryScreen(receiver);
#endif
}


//
// platform independent main
//

static CClient*			s_client = NULL;

static
int
realMain(CMutex* mutex)
{
	// caller should have mutex locked on entry

	// initialize threading library
	CThread::init();

	// make logging thread safe
	CMutex logMutex;
	s_logMutex = &logMutex;
	CLog::setLock(&logLock);

	int result = kExitSuccess;
	do {
		bool opened = false;
		bool locked = true;
		try {
			// create client
			s_client = new CClient(s_name);
			s_client->camp(s_camp);
			s_client->setAddress(s_serverAddress);
			s_client->setScreenFactory(new CSecondaryScreenFactory);
			s_client->setSocketFactory(new CTCPSocketFactory);
			s_client->setStreamFilterFactory(NULL);

			// open client
			try {
				s_client->open();
				opened = true;

				// run client
				if (mutex != NULL) {
					mutex->unlock();
				}
				locked = false;
				s_client->mainLoop();
				locked = true;

				// get client status
				if (s_client->wasRejected()) {
					// try again later.  we don't want to bother
					// the server very often if it doesn't want us.
					throw XScreenUnavailable(60.0);
				}

				// clean up
#define FINALLY do {								\
				if (!locked && mutex != NULL) {		\
					mutex->lock();					\
				}									\
				if (s_client != NULL) {				\
					if (opened) {					\
						s_client->close();			\
					}								\
				}									\
				delete s_client;					\
				s_client = NULL;					\
				} while (false)
				FINALLY;
			}
			catch (XScreenUnavailable& e) {
				// wait before retrying if we're going to retry
				if (s_restartable) {
					CThread::sleep(e.getRetryTime());
				}
				else {
					result = kExitFailed;
				}
				FINALLY;
			}
			catch (XThread&) {
				FINALLY;
				throw;
			}
			catch (...) {
				// don't try to restart and fail
				s_restartable = false;
				result        = kExitFailed;
				FINALLY;
			}
#undef FINALLY
		}
		catch (XBase& e) {
			log((CLOG_CRIT "failed: %s", e.what()));
		}
		catch (XThread&) {
			// terminated
			s_restartable = false;
			result        = kExitTerminated;
		}
		catch (...) {
			CLog::setLock(NULL);
			s_logMutex = NULL;
			throw;
		}
	} while (s_restartable);

	// clean up
	CLog::setLock(NULL);
	s_logMutex = NULL;

	return result;
}


//
// command line parsing
//

#define BYE "\nTry `%s --help' for more information."

static void				(*bye)(int) = &exit;

static
void
version()
{
	log((CLOG_PRINT
"%s %s, protocol version %d.%d\n"
"%s",
								pname,
								kVersion,
								kProtocolMajorVersion,
								kProtocolMinorVersion,
								kCopyright));
}

static
void
help()
{
#if WINDOWS_LIKE

#  define PLATFORM_ARGS												\
" [--install]"														\
" <server-address>\n"												\
"or\n"																\
" --uninstall\n"
#  define PLATFORM_DESC												\
"      --install            install server as a service.\n"			\
"      --uninstall          uninstall server service.\n"

#else

#  define PLATFORM_ARGS												\
" <server-address>\n"
#  define PLATFORM_DESC

#endif

	log((CLOG_PRINT
"Usage: %s"
" [--camp|--no-camp]"
" [--daemon|--no-daemon]"
" [--debug <level>]"
" [--name <screen-name>]"
" [--restart|--no-restart]"
PLATFORM_ARGS
"\n"
"Start the synergy mouse/keyboard sharing server.\n"
"\n"
"*     --camp               keep attempting to connect to the server until\n"
"                           successful.\n"
"      --no-camp            do not camp.\n"
"  -d, --debug <level>      filter out log messages with priorty below level.\n"
"                           level may be: FATAL, ERROR, WARNING, NOTE, INFO,\n"
"                           DEBUG, DEBUG1, DEBUG2.\n"
"  -f, --no-daemon          run the client in the foreground.\n"
"*     --daemon             run the client as a daemon.\n"
"  -n, --name <screen-name> use screen-name instead the hostname to identify\n"
"                           ourself to the server.\n"
"  -1, --no-restart         do not try to restart the client if it fails for\n"
"                           some reason.\n"
"*     --restart            restart the client automatically if it fails.\n"
PLATFORM_DESC
"  -h, --help               display this help and exit.\n"
"      --version            display version information and exit.\n"
"\n"
"* marks defaults.\n"
"\n"
"The server address is of the form: [<hostname>][:<port>].  The hostname\n"
"must be the address or hostname of the server.  The port overrides the\n"
"default port, %d.\n"
"\n"
"Where log messages go depends on the platform and whether or not the\n"
"client is running as a daemon.",
								pname, kDefaultPort));

}

static
bool
isArg(int argi, int argc, const char** argv,
				const char* name1, const char* name2,
				int minRequiredParameters = 0)
{
	if ((name1 != NULL && strcmp(argv[argi], name1) == 0) ||
		(name2 != NULL && strcmp(argv[argi], name2) == 0)) {
		// match.  check args left.
		if (argi + minRequiredParameters >= argc) {
			log((CLOG_PRINT "%s: missing arguments for `%s'" BYE,
								pname, argv[argi], pname));
			bye(kExitArgs);
		}
		return true;
	}

	// no match
	return false;
}

static
void
parse(int argc, const char** argv)
{
	assert(pname != NULL);
	assert(argv  != NULL);
	assert(argc  >= 1);

	// set defaults
	char hostname[256];
	if (CNetwork::gethostname(hostname, sizeof(hostname)) != CNetwork::Error) {
		s_name = hostname;
	}

	// parse options
	int i;
	for (i = 1; i < argc; ++i) {
		if (isArg(i, argc, argv, "-d", "--debug", 1)) {
			// change logging level
			s_logFilter = argv[++i];
		}

		else if (isArg(i, argc, argv, "-n", "--name", 1)) {
			// save screen name
			s_name = argv[++i];
		}

		else if (isArg(i, argc, argv, NULL, "--camp")) {
			// enable camping
			s_camp = true;
		}

		else if (isArg(i, argc, argv, NULL, "--no-camp")) {
			// disable camping
			s_camp = false;
		}

		else if (isArg(i, argc, argv, "-f", "--no-daemon")) {
			// not a daemon
			s_daemon = false;
		}

		else if (isArg(i, argc, argv, NULL, "--daemon")) {
			// daemonize
			s_daemon = true;
		}

		else if (isArg(i, argc, argv, "-1", "--no-restart")) {
			// don't try to restart
			s_restartable = false;
		}

		else if (isArg(i, argc, argv, NULL, "--restart")) {
			// try to restart
			s_restartable = true;
		}

		else if (isArg(i, argc, argv, "-z", NULL)) {
			s_backend = true;
		}

		else if (isArg(i, argc, argv, "-h", "--help")) {
			help();
			bye(kExitSuccess);
		}

		else if (isArg(i, argc, argv, NULL, "--version")) {
			version();
			bye(kExitSuccess);
		}

#if WINDOWS_LIKE
		else if (isArg(i, argc, argv, NULL, "--install")) {
			s_install = true;
			if (s_uninstall) {
				log((CLOG_PRINT "%s: `--install' and `--uninstall'"
								" are mutually exclusive" BYE,
								pname, argv[i], pname));
				bye(kExitArgs);
			}
		}
#endif

#if WINDOWS_LIKE
		else if (isArg(i, argc, argv, NULL, "--uninstall")) {
			s_uninstall = true;
			if (s_install) {
				log((CLOG_PRINT "%s: `--install' and `--uninstall'"
								" are mutually exclusive" BYE,
								pname, argv[i], pname));
				bye(kExitArgs);
			}
		}
#endif

		else if (isArg(i, argc, argv, "--", NULL)) {
			// remaining arguments are not options
			++i;
			break;
		}

		else if (argv[i][0] == '-') {
			log((CLOG_PRINT "%s: unrecognized option `%s'" BYE,
								pname, argv[i], pname));
			bye(kExitArgs);
		}

		else {
			// this and remaining arguments are not options
			break;
		}
	}

	// exactly one non-option argument (server-address) unless using
	// --uninstall.
	if (s_uninstall) {
		if (i != argc) {
			log((CLOG_PRINT "%s: unrecognized option `%s' to `%s'" BYE,
								pname, argv[i], pname,
								s_install ? "--install" : "--uninstall"));
			bye(kExitArgs);
		}
	}
	else {
		if (i == argc) {
			log((CLOG_PRINT "%s: a server address or name is required" BYE,
								pname, pname));
			bye(kExitArgs);
		}
		if (i + 1 != argc) {
			log((CLOG_PRINT "%s: unrecognized option `%s'" BYE,
								pname, argv[i], pname));
			bye(kExitArgs);
		}

		// save server address
		try {
			s_serverAddress = CNetworkAddress(argv[i], kDefaultPort);
		}
		catch (XSocketAddress& e) {
			log((CLOG_PRINT "%s: %s" BYE,
								pname, e.what(), pname));
			bye(kExitFailed);
		}
	}

	// increase default filter level for daemon.  the user must
	// explicitly request another level for a daemon.
	if (s_daemon && s_logFilter == NULL) {
#if WINDOWS_LIKE
		if (CPlatform::isWindows95Family()) {
			// windows 95 has no place for logging so avoid showing
			// the log console window.
			s_logFilter = "FATAL";
		}
		else
#endif
		{
			s_logFilter = "NOTE";
		}
	}

	// set log filter
	if (!CLog::setFilter(s_logFilter)) {
		log((CLOG_PRINT "%s: unrecognized log level `%s'" BYE,
								pname, s_logFilter, pname));
		bye(kExitArgs);
	}
}


//
// platform dependent entry points
//

#if WINDOWS_LIKE

#include "CMSWindowsScreen.h"

static bool				s_errors = false;

static
bool
logMessageBox(int priority, const char* msg)
{
	if (priority <= CLog::kERROR) {
		s_errors = true;
	}
	if (s_backend) {
		return true;
	}
	if (priority <= CLog::kFATAL) {
		MessageBox(NULL, msg, pname, MB_OK | MB_ICONWARNING);
		return true;
	}
	else {
		return false;
	}
}

static
void
byeThrow(int x)
{
	throw CWin32Platform::CDaemonFailed(x);
}

static
void
daemonStop(void)
{
	s_client->exitMainLoop();
}

static
int
daemonStartup(IPlatform* iplatform, int argc, const char** argv)
{
	// get platform pointer
	CWin32Platform* platform = static_cast<CWin32Platform*>(iplatform);

	// catch errors that would normally exit
	bye = &byeThrow;

	// parse command line
	s_install   = false;
	s_uninstall = false;
	parse(argc, argv);
	if (s_install || s_uninstall) {
		// not allowed to install/uninstall from service
		throw CWin32Platform::CDaemonFailed(kExitArgs);
	}

	// run as a service
	return platform->runDaemon(realMain, daemonStop);
}

static
int
daemonStartup95(IPlatform*, int, const char**)
{
	return realMain(NULL);
}

int WINAPI
WinMain(HINSTANCE instance, HINSTANCE, LPSTR, int)
{
	CPlatform platform;

	// save instance
	CMSWindowsScreen::init(instance);

	// get program name
	pname = platform.getBasename(__argv[0]);

	// initialize network library
	CNetwork::init();

	// send PRINT and FATAL output to a message box
	CLog::setOutputter(&logMessageBox);

	// windows NT family starts services using no command line options.
	// since i'm not sure how to tell the difference between that and
	// a user providing no options we'll assume that if there are no
	// arguments and we're on NT then we're being invoked as a service.
	// users on NT can use `--daemon' or `--no-daemon' to force us out
	// of the service code path.
	if (__argc <= 1 && !CWin32Platform::isWindows95Family()) {
		int result = platform.daemonize(DAEMON_NAME, &daemonStartup);
		if (result == -1) {
			log((CLOG_CRIT "failed to start as a service" BYE, pname));
			return kExitFailed;
		}
		return result;
	}

	// parse command line
	parse(__argc, const_cast<const char**>(__argv));

	// install/uninstall
	if (s_install) {
		// get the full path to this program
		TCHAR path[MAX_PATH];
		if (GetModuleFileName(NULL, path,
								sizeof(path) / sizeof(path[0])) == 0) {
			log((CLOG_CRIT "cannot determine absolute path to program"));
			return kExitFailed;
		}

		// construct the command line to start the service with
		CString commandLine = "--daemon";
		if (s_restartable) {
			commandLine += " --restart";
		}
		else {
			commandLine += " --no-restart";
		}
		if (s_logFilter != NULL) {
			commandLine += " --debug ";
			commandLine += s_logFilter;
		}
		commandLine += " ";
		commandLine += s_serverAddress.getHostname().c_str();

		// install
		if (!platform.installDaemon(DAEMON_NAME,
					"Shares this system's mouse and keyboard with others.",
					path, commandLine.c_str())) {
			log((CLOG_CRIT "failed to install service"));
			return kExitFailed;
		}
		log((CLOG_PRINT "installed successfully"));
		return kExitSuccess;
	}
	else if (s_uninstall) {
		switch (platform.uninstallDaemon(DAEMON_NAME)) {
		case IPlatform::kSuccess:
			log((CLOG_PRINT "uninstalled successfully"));
			return kExitSuccess;

		case IPlatform::kFailed:
			log((CLOG_CRIT "failed to uninstall service"));
			return kExitFailed;

		case IPlatform::kAlready:
			log((CLOG_CRIT "service isn't installed"));
			// return success since service is uninstalled
			return kExitSuccess;
		}
	}

	// daemonize if requested
	int result;
	if (s_daemon) {
		// redirect log messages
		platform.installDaemonLogger(DAEMON_NAME);

		// start as a daemon
		if (CWin32Platform::isWindows95Family()) {
			result = platform.daemonize(DAEMON_NAME, &daemonStartup95);
			if (result == -1) {
				log((CLOG_CRIT "failed to start as a service" BYE, pname));
				result = kExitFailed;
			}
		}
		else {
			// cannot start a service from the command line so just
			// run normally (except with log messages redirected).
			result = realMain(NULL);
		}
	}
	else {
		// run
		result = realMain(NULL);
	}

	CNetwork::cleanup();

	// if running as a non-daemon backend and there was an error then
	// wait for the user to click okay so he can see the error messages.
	if (s_backend && !s_daemon && (result == kExitFailed || s_errors)) {
		char msg[1024];
		msg[0] = '\0';
		LoadString(instance, IDS_FAILED, msg, sizeof(msg) / sizeof(msg[0]));
		MessageBox(NULL, msg, pname, MB_OK | MB_ICONWARNING);
	}

	return result;
}

#elif UNIX_LIKE

static
int
daemonStartup(IPlatform*, int, const char**)
{
	return realMain(NULL);
}

int
main(int argc, char** argv)
{
	CPlatform platform;

	// get program name
	pname = platform.getBasename(argv[0]);

	// initialize network library
	CNetwork::init();

	// parse command line
	parse(argc, const_cast<const char**>(argv));

	// daemonize if requested
	int result;
	if (s_daemon) {
		result = platform.daemonize(DAEMON_NAME, &daemonStartup);
		if (result == -1) {
			log((CLOG_CRIT "failed to daemonize"));
			return kExitFailed;
		}
	}
	else {
		result = realMain(NULL);
	}

	CNetwork::cleanup();

	return result;
}

#else

#error no main() for platform

#endif