#include "application.h"
#include "process.h"
#include <io/io.h>
#include <cf/cf.h>
#include <text/format.h>
#include <oak/compat.h>
#include <oak/debug.h>

OAK_DEBUG_VAR(Application);

namespace oak
{
	static std::string _app_name      = NULL_STR;
	static std::string _app_path      = NULL_STR;
	static std::string _full_app_path = NULL_STR;
	static std::string _pid_path      = NULL_STR;
	static std::string _support_path  = NULL_STR;

	static std::string process_name (pid_t pid)
	{
		struct kinfo_proc procInfo;
		size_t length = sizeof(procInfo);
		int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid };
		if(sysctl(mib, 4, &procInfo, &length, NULL, 0) == 0 && length > 0)
			return procInfo.kp_proc.p_comm;
		return NULL_STR;
	}

	static void disable_interactive_input ()
	{
		if(int fd = strtol(getenv("TM_INTERACTIVE_INPUT_ECHO_FD") ?: "0", NULL, 10))
		{
			close(fd);
			unsetenv("TM_INTERACTIVE_INPUT_ECHO_FD");
		}
		if(int fd = strtol(getenv("TM_ERROR_FD") ?: "0", NULL, 10))
		{
			close(fd);
			unsetenv("TM_ERROR_FD");
		}
	}

	application_t::application_t (int argc, char const* argv[])
	{
		disable_interactive_input(); // this is only relevant when we launch TM from a TM command that uses interactive input (like make) — each relaunch will allocate two new file descriptors, so eventually we’ll run out of them

		// When upgrading from r9110 or earlier.
		static char const* LegacyVariables[] = { "RELAUNCH", "ACTIVATE", "DISABLE_UNTITLED_FILE" };
		iterate(variable, LegacyVariables)
		{
			if(getenv(*variable))
			{
				setenv((std::string("OAK_") + *variable).c_str(), getenv(*variable), 1);
				unsetenv(*variable);
			}
		}

		_app_name      = getenv("OAK_APP_NAME") ?: path::name(argv[0]);
		_full_app_path = path::join(path::cwd(), argv[0]);
		_app_path      = _full_app_path;
		_pid_path      = support(((CFBundleGetMainBundle() && CFBundleGetIdentifier(CFBundleGetMainBundle())) ? cf::to_s(CFBundleGetIdentifier(CFBundleGetMainBundle())) : _app_name) + ".pid");

		std::string const appBinary = path::join("Contents/MacOS", _app_name);
		if(_app_path.size() > appBinary.size() && _app_path.find(appBinary) == _app_path.size() - appBinary.size())
			_app_path.erase(_app_path.end() - appBinary.size(), _app_path.end());

		std::string content = path::content(_pid_path);
		if(content == NULL_STR && strcmp(getenv("OAK_RELAUNCH") ?: "0", "1") == 0)
			content = path::content(support(_app_name + ".pid"));

		long pid = content != NULL_STR ? strtol(content.c_str(), NULL, 10) : 0;
		if(pid != 0 && process_name(pid) == _app_name)
		{
			int event_queue = kqueue();

			struct kevent changeList = { (uintptr_t)pid, EVFILT_PROC, EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_EXIT, 0, NULL };
			int res = kevent(event_queue, &changeList, 1, NULL /* event list */, 0 /* number of events */, NULL);
			if(res == -1 && errno == ESRCH)
			{
				D(DBF_Application, bug("old instance no longer running\n"););
			}
			else if(res == 0)
			{
				if(char const* relaunch = getenv("OAK_RELAUNCH"))
				{
					D(DBF_Application, bug("%s is already running (pid %ld), OAK_RELAUNCH = %s\n", _app_name.c_str(), pid, relaunch););
					kill(pid, strcmp(relaunch, "QUICK") == 0 ? SIGTERM : SIGINT);

					struct kevent changed;
					struct timespec timeout = { 30, 0 };
					if(kevent(event_queue, NULL /* change list */, 0 /* number of changes */, &changed /* event list */, 1 /* number of events */, &timeout) == 1)
					{
						D(DBF_Application, bug("other instance terminated\n"););
						ASSERTF((pid_t)changed.ident == pid, "pid %d, changed %d", pid, (pid_t)changed.ident);
						CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
					}
					else
					{
						D(DBF_Application, bug("timeout expired, terminate\n"););
						exit(1);
					}
				}
				else
				{
					D(DBF_Application, bug("%s is already running (pid %ld), OAK_RELAUNCH = NO\n", name().c_str(), pid););
					ProcessSerialNumber psn;
					if(noErr == GetProcessForPID(pid, &psn))
					{
						SetFrontProcess(&psn);
						exit(0);
					}
					// procNotFound
				}
			}
			close(event_queue);
		}

		create_pid_file();

		if(getenv("OAK_ACTIVATE") || getenv("OAK_RELAUNCH"))
		{
			D(DBF_Application, bug("bring new instance to front\n"););
			SetFrontProcess(&(ProcessSerialNumber){ 0, kCurrentProcess });
			unsetenv("OAK_ACTIVATE");
			unsetenv("OAK_RELAUNCH");
		}
	}

	void application_t::create_pid_file ()
	{
		if(path::set_content(_pid_path, text::format("%d", getpid())))
		{
			atexit(&remove_pid_file);
		}
		else
		{
			D(DBF_Application, bug("open(\"%s\", O_WRONLY): %s\n", _pid_path.c_str(), strerror(errno)););
		}
	}

	void application_t::remove_pid_file ()
	{
		D(DBF_Application, bug("unlink %s\n", _pid_path.c_str()););
		unlink(_pid_path.c_str());
	}

	std::string application_t::name ()
	{
		if(_app_name == NULL_STR)
		{
			if(CFBundleRef mainBundle = CFBundleGetMainBundle())
			{
				if(CFTypeRef appName = CFBundleGetValueForInfoDictionaryKey(mainBundle, CFSTR("CFBundleName")))
				{
					if(CFGetTypeID(appName) == CFStringGetTypeID())
						_app_name = cf::to_s((CFStringRef)appName);
				}
			}
		}
		return _app_name;
	}

	std::string application_t::path (std::string const& relativePath)
	{
		if(_app_path == NULL_STR)
		{
			if(CFBundleRef mainBundle = CFBundleGetMainBundle())
			{
				if(CFURLRef bundleURL = CFBundleCopyBundleURL(mainBundle))
				{
					if(CFStringRef bundlePath = CFURLCopyFileSystemPath(bundleURL, kCFURLPOSIXPathStyle))
					{
						_app_path = cf::to_s(bundlePath);
						CFRelease(bundlePath);
					}
					CFRelease(bundleURL);
				}
			}
		}
		return path::join(_app_path, relativePath);
	}

	void application_t::set_name (std::string const& newName)
	{
		_app_name = newName;
	}

	void application_t::set_path (std::string const& newPath)
	{
		_app_path = newPath;
	}

	void application_t::set_support (std::string const& newPath)
	{
		_support_path = newPath;
	}

	std::string application_t::support (std::string const& relativePath)
	{
		if(_support_path == NULL_STR)
			_support_path = path::join(path::join(path::home(), "Library/Application Support"), name());
		return path::join(_support_path, relativePath);
	}

	std::string application_t::revision ()
	{
		if(CFBundleRef mainBundle = CFBundleGetMainBundle())
		{
			if(CFTypeRef bundleVersion = CFBundleGetValueForInfoDictionaryKey(mainBundle, CFSTR("CFBundleVersion")))
			{
				if(CFGetTypeID(bundleVersion) == CFStringGetTypeID())
					return cf::to_s((CFStringRef)bundleVersion);
			}
		}
		return "???";
	}

	static void* relaunch_thread (void* userdata)
	{
		oak::set_thread_name("application_t::relaunch");
		pid_t pid = (pid_t)userdata;

		int status = 0;
		if(waitpid(pid, &status, 0) != pid)
			fprintf(stderr, "*** relaunch failed: no process for pid %d.\n", pid);
		else if(WIFSIGNALED(status))
			fprintf(stderr, "*** relaunch failed: process terminated, %s.\n", strsignal(WTERMSIG(status)));
		else if(!WIFEXITED(status))
			fprintf(stderr, "*** relaunch failed: process terminated abnormally %d.\n", status);
		else
			fprintf(stderr, "*** relaunch failed: process terminated with status %d.\n", status);

		return NULL;
	}

	void application_t::relaunch ()
	{
		ASSERT(_full_app_path != NULL_STR);
		D(DBF_Application, bug("%s\n", _full_app_path.c_str()););

		create_pid_file(); // we create this during startup, but incase there was no support folder it would have failed

		std::map<std::string, std::string> envMap = oak::basic_environment();
		envMap["OAK_RELAUNCH"] = "1";
		oak::c_array env(envMap);

		pid_t pid = vfork();
		if(pid == 0)
		{
			char const* argv[] = { _full_app_path.c_str(), NULL };
			execve(argv[0], (char* const*)argv, env);
			perror("relaunch");
			_exit(-1);
		}
		else
		{
			pthread_t thread;
			if(pthread_create(&thread, NULL, &relaunch_thread, (void*)pid) == 0)
					pthread_detach(thread);
			else	perror("pthread_create()");
		}
	}

} /* oak */
