/**
 * \file   premake.c
 * \brief  Program entry point.
 * \author Copyright (c) 2002-2017 Jason Perkins and the Premake project
 */

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "premake.h"
#include "lua_shimtable.h"

#if PLATFORM_MACOSX
#include <CoreFoundation/CFBundle.h>
#endif

#if PLATFORM_BSD
#include <sys/types.h>
#include <sys/sysctl.h>
#endif

#define ERROR_MESSAGE  "Error: %s\n"


static void build_premake_path(lua_State* L);
static int process_arguments(lua_State* L, int argc, const char** argv);
static int run_premake_main(lua_State* L, const char* script);


/* A search path for script files */
const char* scripts_path = NULL;


/* Built-in functions */
static const luaL_Reg criteria_functions[] = {
	{ "_compile", criteria_compile },
	{ "_delete", criteria_delete },
	{ "matches", criteria_matches },
	{ NULL, NULL }
};

static const luaL_Reg debug_functions[] = {
	{ "prompt", debug_prompt },
	{ NULL, NULL }
};

static const luaL_Reg path_functions[] = {
	{ "getabsolute", path_getabsolute },
	{ "getrelative", path_getrelative },
	{ "isabsolute",  path_isabsolute },
	{ "join", path_join },
	{ "deferredjoin", path_deferred_join },
	{ "hasdeferredjoin", path_has_deferred_join },
	{ "resolvedeferredjoin", path_resolve_deferred_join },
	{ "normalize", path_normalize },
	{ "translate", path_translate },
	{ "wildcards", path_wildcards },
	{ NULL, NULL }
};

static const luaL_Reg os_functions[] = {
	{ "chdir",                  os_chdir                },
	{ "chmod",                  os_chmod                },
	{ "comparefiles",           os_comparefiles         },
	{ "copyfile",               os_copyfile             },
	{ "_is64bit",               os_is64bit              },
	{ "isdir",                  os_isdir                },
	{ "getcwd",                 os_getcwd               },
	{ "getpass",                os_getpass              },
	{ "getWindowsRegistry",     os_getWindowsRegistry   },
	{ "listWindowsRegistry",    os_listWindowsRegistry  },
	{ "getversion",             os_getversion           },
	{ "host",                   os_host                 },
	{ "isfile",                 os_isfile               },
	{ "islink",                 os_islink               },
	{ "locate",                 os_locate               },
	{ "matchdone",              os_matchdone            },
	{ "matchisfile",            os_matchisfile          },
	{ "matchname",              os_matchname            },
	{ "matchnext",              os_matchnext            },
	{ "matchstart",             os_matchstart           },
	{ "mkdir",                  os_mkdir                },
#if PLATFORM_WINDOWS
	// utf8 functions for Windows (assuming posix already handle utf8)
	{"remove",                  os_remove               },
	{"rename",                  os_rename               },
#endif
	{ "pathsearch",             os_pathsearch           },
	{ "realpath",               os_realpath             },
	{ "rmdir",                  os_rmdir                },
	{ "stat",                   os_stat                 },
	{ "uuid",                   os_uuid                 },
	{ "writefile_ifnotequal",   os_writefile_ifnotequal },
	{ "touchfile",              os_touchfile            },
	{ "compile",                os_compile              },
	{ NULL, NULL }
};

static const luaL_Reg premake_functions[] = {
	{ "getEmbeddedResource", premake_getEmbeddedResource },
	{ NULL, NULL }
};

static const luaL_Reg string_functions[] = {
	{ "endswith",  string_endswith },
	{ "hash", string_hash },
	{ "sha1", string_sha1 },
	{ "startswith", string_startswith },
	{ NULL, NULL }
};

static const luaL_Reg buffered_functions[] = {
	{ "new", buffered_new },
	{ "write", buffered_write },
	{ "writeln", buffered_writeln },
	{ "tostring", buffered_tostring },
	{ "close", buffered_close },
	{ NULL, NULL }
};

static const luaL_Reg term_functions[] = {
	{ "getTextColor",  term_getTextColor },
	{ "setTextColor",  term_setTextColor },
	{ NULL, NULL }
};

#ifdef PREMAKE_CURL
static const luaL_Reg http_functions[] = {
	{ "get",       http_get },
	{ "post",      http_post },
	{ "download",  http_download },
	{ NULL, NULL }
};
#endif

#ifdef PREMAKE_COMPRESSION
static const luaL_Reg zip_functions[] = {
	{ "extract",  zip_extract },
	{ NULL, NULL }
};
#endif


static void lua_getorcreate_table(lua_State *L, const char *modname)
{
	luaL_getsubtable(L, LUA_REGISTRYINDEX, "_LOADED");      // get _LOADED table            stack = {_LOADED}
	lua_getfield(L, -1, modname);                           // get _LOADED[modname]         stack = { _LOADED, result }
	if (!lua_istable(L, -1))                                // not found?                   stack = { _LOADED, result }
	{
		lua_pop(L, 1);                                      // remove previous result       stack = { _LOADED }
		lua_pushglobaltable(L);                             // push _G onto stack           stack = { _LOADED, _G }
		lua_createtable(L, 0, 0);                           // new table for field          stack = { _LOADED, _G, result }
		lua_pushlstring(L, modname, strlen(modname));       //                              stack = { _LOADED, _G, result, modname }
		lua_pushvalue(L, -2);                               //                              stack = { _LOADED, _G, result, modname, result }
		lua_settable(L, -4);                                // _G[modname] = result         stack = { _LOADED, _G, result }
		lua_remove(L, -2);                                  // remove _G from stack         stack = { _LOADED, result }
		lua_pushvalue(L, -1);                               // duplicate result             stack = { _LOADED, result, result }
		lua_setfield(L, -3, modname);                       // _LOADED[modname] = result    stack = { _LOADED, result }
	}

	lua_remove(L, -2);                                      // remove _LOADED from stack    stack = { result }
}


void luaL_register(lua_State *L, const char *libname, const luaL_Reg *l)
{
	lua_getorcreate_table(L, libname);
	luaL_setfuncs(L, l, 0);
	lua_pop(L, 1);
}


/**
 * Initialize the Premake Lua environment.
 */
int premake_init(lua_State* L)
{
	const char* value;

	luaL_register(L, "premake",  premake_functions);
	luaL_register(L, "criteria", criteria_functions);
	luaL_register(L, "debug",    debug_functions);
	luaL_register(L, "path",     path_functions);
	luaL_register(L, "os",       os_functions);
	luaL_register(L, "string",   string_functions);
	luaL_register(L, "buffered", buffered_functions);
	luaL_register(L, "term",     term_functions);

#ifdef PREMAKE_CURL
	luaL_register(L, "http",     http_functions);
#endif

#ifdef PREMAKE_COMPRESSION
	luaL_register(L, "zip",     zip_functions);
#endif

	lua_pushlightuserdata(L, &s_shimTable);
	lua_rawseti(L, LUA_REGISTRYINDEX, 0x5348494D); // equal to 'SHIM'

	/* push the application metadata */
	lua_pushstring(L, LUA_COPYRIGHT);
	lua_setglobal(L, "_COPYRIGHT");

	lua_pushstring(L, PREMAKE_VERSION);
	lua_setglobal(L, "_PREMAKE_VERSION");

	lua_pushstring(L, PREMAKE_COPYRIGHT);
	lua_setglobal(L, "_PREMAKE_COPYRIGHT");

	lua_pushstring(L, PREMAKE_PROJECT_URL);
	lua_setglobal(L, "_PREMAKE_URL");

	/* set the OS platform variable */
	lua_pushstring(L, PLATFORM_STRING);
	lua_setglobal(L, "_TARGET_OS");

	/* find the user's home directory */
	value = getenv("HOME");
	if (!value) value = getenv("USERPROFILE");
	if (!value) value = "~";
	lua_pushstring(L, value);
	lua_setglobal(L, "_USER_HOME_DIR");

	/* publish the initial working directory */
	os_getcwd(L);
	lua_setglobal(L, "_WORKING_DIR");

#if !defined(PREMAKE_NO_BUILTIN_SCRIPTS)
	/* let native modules initialize themselves */
	registerModules(L);
#endif

	return OKAY;
}


static void setErrorColor(lua_State* L)
{
	int errorColor = 12;

	lua_getglobal(L, "term");
	lua_pushstring(L, "errorColor");
	lua_gettable(L, -2);

	if (!lua_isnil(L, -1))
		errorColor = (int)luaL_checkinteger(L, -1);

	term_doSetTextColor(errorColor);

	lua_pop(L, 2);
}



void printLastError(lua_State* L)
{
	const char* message = lua_tostring(L, -1);
	int oldColor = term_doGetTextColor();
	setErrorColor(L);
	printf(ERROR_MESSAGE, message);
	term_doSetTextColor(oldColor);
}

static int lua_error_handler(lua_State* L)
{
	// in debug mode, show full traceback on all errors
#if !defined(NDEBUG)
	lua_getglobal(L, "debug");
	lua_getfield(L, -1, "traceback");
	lua_remove(L, -2);     // remove debug table
	lua_insert(L, -2);     // insert traceback function before error message
	lua_pushinteger(L, 3); // push level
	lua_call(L, 2, 1);     // call traceback
#else
	(void) L;
#endif

	return 1;
}

int premake_pcall(lua_State* L, int nargs, int nresults)
{
	lua_pushcfunction(L, lua_error_handler);

	int error_handler_index = lua_gettop(L) - nargs - 1;
	lua_insert(L, error_handler_index); // insert lua_error_handler before call parameters
	int result = lua_pcall(L, nargs, nresults, error_handler_index);
	lua_remove(L, error_handler_index); // remove lua_error_handler from stack
	return result;
}

int premake_execute(lua_State* L, int argc, const char** argv, const char* script)
{
	/* push the absolute path to the Premake executable */
	lua_pushcfunction(L, path_getabsolute);
	premake_locate_executable(L, argv[0]);
	lua_call(L, 1, 1);
	lua_setglobal(L, "_PREMAKE_COMMAND");

	/* Parse the command line arguments */
	if (process_arguments(L, argc, argv) != OKAY) {
		return !OKAY;
	}

	/* Use --scripts and PREMAKE_PATH to populate premake.path */
	build_premake_path(L);

	/* Find and run the main Premake bootstrapping script */
	if (run_premake_main(L, script) != OKAY) {
		printLastError(L);
		return !OKAY;
	}

	/* and call the main entry point */
	lua_getglobal(L, "_premake_main");
	if (premake_pcall(L, 0, 1) != OKAY) {
		printLastError(L);
		return !OKAY;
	}
	else {
		int exitCode = (int)lua_tonumber(L, -1);
		return exitCode;
	}
}



/**
 * Locate the Premake executable, and push its full path to the Lua stack.
 * Based on:
 * http://sourceforge.net/tracker/index.php?func=detail&aid=3351583&group_id=71616&atid=531880
 * http://stackoverflow.com/questions/933850/how-to-find-the-location-of-the-executable-in-c
 * http://stackoverflow.com/questions/1023306/finding-current-executables-path-without-proc-self-exe
 */
int premake_locate_executable(lua_State* L, const char* argv0)
{
	char buffer[PATH_MAX];
	const char* path = NULL;

#if PLATFORM_WINDOWS
	wchar_t widebuffer[PATH_MAX];

	DWORD len = GetModuleFileNameW(NULL, widebuffer, PATH_MAX);
	if (len > 0)
	{
		WideCharToMultiByte(CP_UTF8, 0, widebuffer, len, buffer, PATH_MAX, NULL, NULL);

		buffer[len] = 0;
		path = buffer;
	}
#endif

#if PLATFORM_MACOSX
	CFURLRef bundleURL = CFBundleCopyExecutableURL(CFBundleGetMainBundle());
	CFStringRef pathRef = CFURLCopyFileSystemPath(bundleURL, kCFURLPOSIXPathStyle);
	if (CFStringGetCString(pathRef, buffer, PATH_MAX - 1, kCFStringEncodingUTF8))
		path = buffer;
#endif

#if PLATFORM_LINUX
	int len = readlink("/proc/self/exe", buffer, PATH_MAX - 1);
	if (len > 0)
	{
		buffer[len] = 0;
		path = buffer;
	}
#endif

#if PLATFORM_BSD && !defined(__OpenBSD__)
	int len = readlink("/proc/curproc/file", buffer, PATH_MAX - 1);
	if (len < 0)
		len = readlink("/proc/curproc/exe", buffer, PATH_MAX - 1);
	if (len < 0)
	{
		int mib[4];
		mib[0] = CTL_KERN;
		mib[1] = KERN_PROC;
		mib[2] = KERN_PROC_PATHNAME;
		mib[3] = -1;
		size_t cb = sizeof(buffer);
		sysctl(mib, 4, buffer, &cb, NULL, 0);
		len = (int)cb;
	}
	if (len > 0)
	{
		buffer[len] = 0;
		path = buffer;
	}
#endif

#if PLATFORM_SOLARIS
	int len = readlink("/proc/self/path/a.out", buffer, PATH_MAX - 1);
	if (len > 0)
	{
		buffer[len] = 0;
		path = buffer;
	}
#endif

	/* As a fallback, search the PATH with argv[0] */
	if (!path)
	{
		lua_pushcfunction(L, os_pathsearch);
		lua_pushstring(L, argv0);
		lua_pushstring(L, getenv("PATH"));
		if (lua_pcall(L, 2, 1, 0) == OKAY && !lua_isnil(L, -1))
		{
			lua_pushstring(L, "/");
			lua_pushstring(L, argv0);
			lua_concat(L, 3);
			path = lua_tostring(L, -1);
		}
		lua_pop(L, 1);
	}

	/* If all else fails, use argv[0] as-is and hope for the best */
	if (!path)
	{
		/* make it absolute, if needed */
		os_getcwd(L);
		lua_pushstring(L, "/");
		lua_pushstring(L, argv0);

		if (!do_isabsolute(argv0)) {
			lua_concat(L, 3);
		}
		else {
			lua_pop(L, 3);
			lua_pushstring(L, argv0);
		}

		path = lua_tostring(L, -1);
		lua_pop(L, 1);
	}

	lua_pushstring(L, path);
	return 1;
}



/**
 * Checks one or more of the standard script search locations to locate the
 * specified file. If found, returns the discovered path to the script on
 * the top of the Lua stack.
 */
int premake_test_file(lua_State* L, const char* filename, int searchMask)
{
	if (searchMask & TEST_LOCAL) {
		if (do_isfile(L, filename)) {
			lua_pushcfunction(L, path_getabsolute);
			lua_pushstring(L, filename);
			lua_call(L, 1, 1);
			return OKAY;
		}
	}

	if (scripts_path && (searchMask & TEST_SCRIPTS)) {
		if (do_locate(L, filename, scripts_path)) return OKAY;
	}

	if (searchMask & TEST_PATH) {
		const char* path = getenv("PREMAKE_PATH");
		if (path && do_locate(L, filename, path)) return OKAY;
	}

#if !defined(PREMAKE_NO_BUILTIN_SCRIPTS)
	if ((searchMask & TEST_EMBEDDED) != 0) {
		/* Try to locate a record matching the filename */
		if (premake_find_embedded_script(filename) != NULL) {
			lua_pushstring(L, "$/");
			lua_pushstring(L, filename);
			lua_concat(L, 2);
			return OKAY;
		}
	}
#endif

	return !OKAY;
}



static const char* set_scripts_path(const char* relativePath)
{
	char* path = (char*)malloc(PATH_MAX);
	do_getabsolute(path, relativePath, NULL);
	scripts_path = path;
	return scripts_path;
}



/**
 * Set the premake.path variable, pulling from the --scripts argument
 * and PREMAKE_PATH environment variable if present.
 */
static void build_premake_path(lua_State* L)
{
	int top;
	const char* value;

	lua_getglobal(L, "premake");
	top = lua_gettop(L);

	/* Start by searching the current working directory */
	lua_pushstring(L, ".");

	/* The --scripts argument goes next, if present */
	if (scripts_path) {
		lua_pushstring(L, ";");
		lua_pushstring(L, scripts_path);
	}

	/* Then the PREMAKE_PATH environment variable */
	value = getenv("PREMAKE_PATH");
	if (value) {
		lua_pushstring(L, ";");
		lua_pushstring(L, value);
	}

	/* Then in ~/.premake */
	lua_pushstring(L, ";");
	lua_getglobal(L, "_USER_HOME_DIR");
	lua_pushstring(L, "/.premake");

	/* In the user's Application Support folder */
#if defined(PLATFORM_MACOSX)
	lua_pushstring(L, ";");
	lua_getglobal(L, "_USER_HOME_DIR");
	lua_pushstring(L, "/Library/Application Support/Premake");
#endif

	/* In the /usr tree */
	lua_pushstring(L, ";/usr/local/share/premake;/usr/share/premake");

	/* Put it all together */
	lua_concat(L, lua_gettop(L) - top);

	/* Match Lua's package.path; use semicolon separators */
#if !defined(PLATFORM_WINDOWS)
	lua_getglobal(L, "string");
	lua_getfield(L, -1, "gsub");
	lua_pushvalue(L, -3);
	lua_pushstring(L, ":");
	lua_pushstring(L, ";");
	lua_call(L, 3, 1);
	/* remove the string global table */
	lua_remove(L, -2);
	/* remove the previously concatonated result */
	lua_remove(L, -2);
#endif

	/* Store it in premake.path */
	lua_setfield(L, -2, "path");

	/* Remove the premake namespace table */
	lua_pop(L, 1);
}



/**
 * Copy all command line arguments into the script-side _ARGV global, and
 * check for the presence of a /scripts=<path> argument to help locate
 * the manifest if needed.
 * \returns OKAY if successful.
 */
static int process_arguments(lua_State* L, int argc, const char** argv)
{
	int i;

	/* Copy all arguments in the _ARGV global */
	lua_newtable(L);
	for (i = 1; i < argc; ++i)
	{
		lua_pushstring(L, argv[i]);
		lua_rawseti(L, -2, luaL_len(L, -2) + 1);

		/* The /scripts option gets picked up here; used later to find the
		 * manifest and scripts later if necessary */
		if (strncmp(argv[i], "/scripts=", 9) == 0)
		{
			argv[i] = set_scripts_path(argv[i] + 9);
		}
		else if (strncmp(argv[i], "--scripts=", 10) == 0)
		{
			argv[i] = set_scripts_path(argv[i] + 10);
		}
	}
	lua_setglobal(L, "_ARGV");

	return OKAY;
}



/**
 * Find and run the main Premake bootstrapping script. The loading of the
 * bootstrap and the other core scripts use a limited set of search paths
 * to avoid mismatches between the native host code and the scripts
 * themselves.
 */
static int run_premake_main(lua_State* L, const char* script)
{
	/* Release builds want to load the embedded scripts, with --scripts
	 * argument allowed as an override. Debug builds will look at the
	 * local file system first, then fall back to embedded. */
#if defined(NDEBUG)
	int z = premake_test_file(L, script,
		TEST_SCRIPTS | TEST_EMBEDDED);
#else
	int z = premake_test_file(L, script,
		TEST_LOCAL | TEST_SCRIPTS | TEST_PATH | TEST_EMBEDDED);
#endif

	/* If no embedded script can be found, release builds will then
	 * try to fall back to the local file system, just in case */
#if defined(NDEBUG)
	if (z != OKAY) {
		z = premake_test_file(L, script, TEST_LOCAL | TEST_PATH);
	}
#endif

	if (z == OKAY) {
		const char* filename = lua_tostring(L, -1);
		z = luaL_dofile(L, filename);
	}

	return z;
}



/**
 * Locate a file in the embedded script index. If found, returns the
 * contents of the file's script.
 */

const buildin_mapping* premake_find_embedded_script(const char* filename)
{
#if !defined(PREMAKE_NO_BUILTIN_SCRIPTS)
	int i;
	for (i = 0; builtin_scripts[i].name != NULL; ++i) {
		if (strcmp(builtin_scripts[i].name, filename) == 0) {
			return builtin_scripts + i;
		}
	}
#endif
	return NULL;
}



/**
 * Load a script that was previously embedded into the executable. If
 * successful, a function containing the new script chunk is pushed to
 * the stack, just like luaL_loadfile would do had the chunk been loaded
 * from a file.
 */

int premake_load_embedded_script(lua_State* L, const char* filename)
{
#if !defined(NDEBUG)
	static int warned = 0;
#endif

	const buildin_mapping* chunk = premake_find_embedded_script(filename);
	if (chunk == NULL) {
		return LUA_ERRFILE;
	}

	/* Debug builds probably want to be loading scripts from the disk */
#if !defined(NDEBUG)
	if (!warned) {
		warned = 1;
		printf("** warning: using embedded script '%s'; use /scripts argument to load from files\n", filename);
	}
#endif

	/* "Fully qualify" the filename by turning it into the form $/filename */
	lua_pushstring(L, "$/");
	lua_pushstring(L, filename);
	lua_concat(L, 2);

	/* Load the chunk */
	return luaL_loadbuffer(L, (const char*)chunk->bytecode, chunk->length, filename);
}


/**
 * Give the lua runtime raw access to embedded files.
 */
int premake_getEmbeddedResource(lua_State* L)
{
	const char* filename = luaL_checkstring(L, 1);
	const buildin_mapping* chunk = premake_find_embedded_script(filename);
	if (chunk == NULL)
	{
		return 0;
	}

	lua_pushlstring(L, (const char*)chunk->bytecode, chunk->length);
	return 1;
}
