// Copyright (C) 2018-2025, Tellusim Technologies Inc. All rights reserved
// https://tellusim.com/

#include <core/TellusimCore.h>
#include <core/TellusimSystem.h>
#include <core/TellusimDirectory.h>
#include <format/TellusimParser.h>

/*
 */
using namespace Tellusim;

/*
 */
using Arguments = Map<String, String>;
using Variables = Map<const char*, String>;

/*****************************************************************************\
 *
 * Utils
 *
\*****************************************************************************/

/*
 */
static void create_args(Arguments &args) {
	
	// cpp version
	args.append(String("std"), String("11"));
	
	// target binary name
	args.append(String("target"), String("main"));
	
	// target architecture
	#if TS_X64
		args.append(String("arch"), String("x64"));
	#elif TS_ARM64
		args.append(String("arch"), String("arm64"));
	#endif
	
	// Visual Studio toolset version
	args.append(String("toolset"), String("v143"));
	
	// Visual Studio version
	String version = System::getEnvironment("VSVERSION");
	if(!version) version = "170";
	args.append(String("version"), "vs" + version);
	
	// Android SDK version
	args.append(String("minsdk"), String("30"));
}

/*
 */
static String get_dirname(const String &name) {
	String ret = name.dirname();
	if(!ret) ret = "./";
	return ret;
}

/*
 */
static String get_target(const Arguments &args) {
	auto target_it = args.find("target");
	auto postfix_it = args.find("postfix");
	if(!target_it) return String::null;
	String postfix;
	if(postfix_it) postfix = postfix_it->data;
	return target_it->data.replace("$(POSTFIX)", postfix.get());
}

/*
 */
static String get_root(const Arguments &args, const String &name = String::null) {
	auto root_it = args.find("root");
	if(root_it) return root_it->data;
	String ret;
	String path = name.dirname();
	while(path && ret.size() < 3 * 16) {
		String bin_path = (path + ret).pathname();
		if(bin_path.size() == 1 && bin_path[0] == '/') break;
		if(bin_path.size() == 3 && bin_path[1] == ':' && bin_path[2] == '/') break;
		bin_path += "bin/";
		if(File::isFile(bin_path + "ts_root.sh")) return ret;
		if(File::isFile(bin_path + "ts_root.bat")) return ret;
		ret += "../";
	}
	ret = System::getEnvironment("TSROOT");
	if(ret && Directory::isDirectory(ret)) {
		ret += "/";
		if(name) {
			if(name && name[0] == '/') path = name;
			else if(name.size() > 1 && name[1] == ':') path = name;
			else path = Directory::getCurrentDirectory() + name.dirname();
			return String::relname(path, ret);
		}
		return ret;
	}
	TS_LOG(Error, "get_root(): can't find Tellusim root directory\n");
	return String::null;
}

/*
 */
static String get_assets(const Arguments &args, const String &name) {
	auto assets_it = args.find("assets");
	if(!assets_it) return String::null;
	String ret;
	String path = get_dirname(name);
	for(const String &asset : assets_it->data.split(" \n")) {
		if(asset.contains("*")) {
			Directory directory;
			String prefix = asset.dirname();
			if(directory.open(path + prefix)) {
				for(const String &name : directory.getFiles()) {
					if(name.match(asset.get())) {
						ret += prefix + name;
						ret += " ";
					}
				}
			}
		} else {
			ret += asset;
			ret += " ";
		}
	}
	return ret;
}

/*
 */
static String get_hash(const Arguments &args) {
	Blob blob;
	for(const auto &it : args) {
		blob.puts(it.key);
		blob.puts(it.data);
	}
	blob.seek(0);
	return blob.getSHA1();
}

static String get_guid(const String &hash) {
	String guid;
	guid.append(hash.get(), 8); guid += '-';
	guid.append(hash.get() + 8, 4); guid += '-';
	guid.append(hash.get() + 12, 4); guid += '-';
	guid.append(hash.get() + 16, 4); guid += '-';
	guid.append(hash.get() + 20, 12);
	return guid;
}

static String get_uuid(const String &hash, uint32_t index) {
	String uuid = hash.substring(0, 18).upper();
	return String::format("6%s%04XA", uuid.get(), index);
}

/*
 */
static bool check_args(const Arguments &args, const InitializerList<const char*> &names) {
	for(const auto &name : names) {
		if(!args.find(name)) {
			TS_LOGF(Error, "check_args(): \"%s\" is not defined\n", name);
			return false;
		}
	}
	return true;
}

/*
 */
static bool save_template(const String &name, const uint8_t (*blob)[256], const Map<const char*, String> &vars) {
	String ret;
	Blob template_blob(blob);
	String src = template_blob.gets();
	try {
		String token;
		const char *s = src.get();
		while(*s) {
			if(*s == '@' && Parser::isUpper(s[1])) {
				s += Parser::readRegion(s + 1, token, 0, '@') + 1;
				auto it = vars.find(token);
				if(it) ret += it->data;
				else {
					ret += String::tformat("@{0}@", token);
					TS_LOGF(Error, "save_template(): unknown variable \"%s\"\n", token.get());
				}
				if(*s == '@') s++;
				continue;
			}
			ret += *s++;
		}
	}
	catch(const String &error) {
		TS_LOGF(Error, "save_template(): %s\n", error.get());
		return false;
	}
	File file;
	if(!file.open(name, "wb")) {
		TS_LOGF(Error, "save_template(): can't create \"%s\" file\n", name.get());
		return false;
	}
	file.puts(ret);
	return true;
}

/*****************************************************************************\
 *
 * Applications
 *
\*****************************************************************************/

/*
 */
static bool create_app(Arguments &args, Variables &vars, String &root, const String &path) {
	
	// root directory
	bool is_internal = true;
	root = get_root(args, path);
	for(uint32_t i = 0; i < root.size(); i++) {
		if(root[i] == '/' || root[i] == '.') continue;
		is_internal = false;
		break;
	}
	if(!is_internal) {
		root = get_root(args);
	}
	if(!root) {
		TS_LOG(Error, "create_app(): can't get root directory\n");
		return false;
	}
	
	// check application path
	if(!path || path.back() != '/') {
		TS_LOGF(Error, "create_app(): invalid app path \"%s\"\n", path.get());
		return false;
	}
	
	// application name
	String name;
	auto name_it = args.find("name");
	if(name_it) {
		name = name_it->data;
	} else {
		name = path.substring(0, path.size() - 1);
		name = name.basename();
		args.append(String("name"), name);
	}
	if(!name) {
		TS_LOGF(Error, "create_app(): can't create \"%s\" application\n", path.get());
		return false;
	}
	
	// check directory
	if(Directory::isDirectory(path)) {
		TS_LOGF(Error, "create_app(): \"%s\" path is already exist\n", path.get());
		return false;
	}
	
	// create application
	if(!Directory::createDirectory(path, true)) {
		TS_LOGF(Error, "create_app(): can't create \"%s\" directory\n", path.get());
		return false;
	}
	
	// copy libraries
	if(!is_internal) {
		
		// create application
		if(!Directory::createDirectory(path + "bin", true)) {
			TS_LOGF(Error, "create_app(): can't create \"%s\" directory\n", path.get());
			return false;
		}
		
		// copy bin libraries
		const char *library_names[] = {
			"Tellusim_x64.dll", "Tellusim_x64d.dll",
			"Tellusim_arm64.dll", "Tellusim_arm64d.dll",
			"libTellusim_x64.so", "libTellusim_x64d.so",
			"libTellusim_arm64.so", "libTellusim_arm64d.so",
			"libTellusim_x64.dylib", "libTellusim_x64d.dylib",
			"libTellusim_arm64.dylib", "libTellusim_arm64d.dylib",
		};
		for(uint32_t i = 0; i < TS_COUNTOF(library_names); i++) {
			String name = root + "bin/" + library_names[i];
			if(File::isFile(name) && !Directory::copyFile(name, path + "bin/" + library_names[i], true)) {
				TS_LOGF(Error, "create_app(): can't copy \"%s\" library\n", library_names[i]);
				return false;
			}
		}
		
		// copy includes
		if(!Directory::copyDirectory(root + "include", path + "include", true)) {
			TS_LOG(Error, "create_app(): can't copy include directory\n");
			return false;
		}
		
		// copy link libraries
		if(!Directory::copyDirectory(root + "lib", path + "lib", true)) {
			TS_LOG(Error, "create_app(): can't copy library directory\n");
			return false;
		}
		
		// root directory
		args.append(String("root"), String("./"));
	}
	else {
		
		// root directory
		args.append(String("root"), root);
	}
	
	// application variables
	vars.append("NAME", name);
	
	return true;
}

/*
 */
static bool create_core(Arguments &args, const String &app_path) {
	
	// create app
	String root;
	Variables vars;
	if(!create_app(args, vars, root, app_path)) {
		TS_LOG(Error, "create_core(): can't create app\n");
		return false;
	}
	
	// create app template
	#include "templates/app_core.blob"
	String app_name = app_path + "main.cpp";
	if(!save_template(app_name, app_core_blob_src, vars)) {
		TS_LOG(Error, "create_core(): can't save app template\n");
		return false;
	}
	
	// application arguments
	args.append(String("srcs"), String("main.cpp"));
	
	return true;
}

/*
 */
static bool create_engine(Arguments &args, const String &app_path) {
	
	// create app
	String root;
	Variables vars;
	if(!create_app(args, vars, root, app_path)) {
		TS_LOG(Error, "create_engine(): can't create app\n");
		return false;
	}
	
	// copy plugins
	if(root != args["root"]) {
		
		// create plugins
		if(!Directory::createDirectory(app_path + "plugins/render/system/source", true) || !Directory::createDirectory(app_path + "plugins/render/system/include", true)) {
			TS_LOGF(Error, "create_app(): can't create \"%splugins/render/system\" directory\n", app_path.get());
			return false;
		}
		
		// copy plugins
		const char *source_name = "plugins/render/system/source/TellusimRenderSystem.cpp";
		const char *header_name = "plugins/render/system/include/TellusimRenderSystem.h";
		if(!Directory::copyFile(root + source_name, app_path + source_name, true) || !Directory::copyFile(root + header_name, app_path + header_name, true)) {
			TS_LOGF(Error, "create_app(): can't copy \"%splugins/render/system\" plugins\n", app_path.get());
			return false;
		}
	}
	
	// create app template
	#include "templates/app_engine.blob"
	String app_name = app_path + "main.cpp";
	if(!save_template(app_name, app_engine_blob_src, vars)) {
		TS_LOG(Error, "create_engine(): can't save app template\n");
		return false;
	}
	
	// create plugins template
	#include "templates/app_plugins.blob"
	String plugins_name = app_path + "Plugins.cpp";
	if(!save_template(plugins_name, app_plugins_blob_src, vars)) {
		TS_LOG(Error, "create_engine(): can't save plugins template\n");
		return false;
	}
	
	// create plugins template
	#include "templates/app_scene.blob"
	String scene_name = app_path + "scene.graphx";
	if(!save_template(scene_name, app_scene_blob_src, vars)) {
		TS_LOG(Error, "create_engine(): can't save scene template\n");
		return false;
	}
	
	// application arguments
	args.append(String("srcs"), String("main.cpp Plugins.cpp"));
	args.append(String("assets"), String("scene.graphx"));
	
	return true;
}

/*****************************************************************************\
 *
 * Makefile
 *
\*****************************************************************************/

/*
 */
static bool load_makefile(Arguments &args, const String &name) {
	
	// load file
	Source source;
	if(!source.open(name)) {
		TS_LOGF(Error, "load_makefile(): can't open \"%s\" file\n", name.get());
		return false;
	}
	String src = source.gets();
	source.close();
	
	// parse file
	try {
		
		String name;
		String value;
		String token;
		
		const char *s = src.get();
		
		while(*s) {
			s += Parser::readToken(s, name, "=:?+");
			if(name.begins("#")) {
				s -= name.size();
				s += Parser::skipLine(s);
			} else if(name.begins("if") || name.begins("!if")) {
				uint32_t counter = 1;
				while(*s && counter > 0) {
					s += Parser::readToken(s, token);
					if(token.begins("if") || token.begins("!if")) counter++;
					else if(token == "endif" || token == "!endif") counter--;
					else if(token == "else" || token == "!else") s += Parser::readToken(s, token);
				}
			} else if(name == "include" || name == "!include") {
				auto it = args.find(name);
				s += Parser::readToken(s, value);
				if(it) it->data += " " + value;
				else args.append(name, value);
			} else if(name && name.back() == ':') {
				name.removeBack();
				auto it = args.append(name);
				s += Parser::readLine(s, value);
				if(value) it->data += value;
				while(*s == '\n' && (s[1] == ' ' || s[1] == '\t')) {
					s += Parser::readLine(s, value);
					if(value) it->data += "\n" + value;
				}
			} else if(name) {
				s += Parser::skipSpaces(s);
				if(*s == ':') {
					s += 1;
					name = name.lower();
					s += Parser::readLine(s, value);
					auto it = args.append(name, value);
					while(*s == '\n' || *s == '\r') s++;
					while(*s == ' ' || *s == '\t') {
						s += Parser::readLine(s, value);
						while(*s == '\n' || *s == '\r') s++;
						it->data += "\n" + value;
					}
				} else if(*s == '=' || (s[1] == '=' && (*s == ':' || *s == '?'))) {
					s += (*s == '=') ? 1 : 2;
					name = name.lower();
					s += Parser::readLine(s, value, true);
					value = value.replace('\n', ' ').replace('\t', ' ');
					args.append(name, value);
				} else if(s[1] == '=' && *s == '+') {
					s += 2;
					name = name.lower();
					auto it = args.find(name);
					s += Parser::readLine(s, value, true);
					value = value.replace('\n', ' ').replace('\t', ' ');
					if(it) it->data += " " + value;
					else args.append(name, value);
				} else {
					Parser::error("unknown token \"%s\"\n", name.get());
				}
			}
		}
	}
	catch(const String &error) {
		TS_LOGF(Error, "load_makefile(): %s", error.get());
		return false;
	}
	
	return true;
}

/*****************************************************************************\
 *
 * CMake
 *
\*****************************************************************************/

/*
 */
static void create_cmake_vars(Variables &vars, const Arguments &args, const String &root, const String &name) {
	
	// get arguments
	String std = args["std"];
	String arch = args["arch"];
	String version = args["version"];
	
	// set variables
	vars.append("STD", std);
	vars.append("ARCH", arch);
	
	// local path
	String include_path = String::tformat("\n\t{0}include\n\t{0}plugins\n\t{0}samples", root);
	String library_path_windows = String::tformat("\n\t\t{0}lib/windows/{2}{1}", root, arch, version);
	String library_path_linux = String::tformat("\n\t\t{0}lib/linux/{1}", root, arch);
	String library_path_macos = String::tformat("\n\t\t{0}lib/macos/{1}", root, arch);
	
	// extern path
	String path = get_dirname(name);
	if(Directory::isDirectory(path + "extern")) {
		include_path += "\n\textern";
		if(Directory::isDirectory(path + "extern/include")) include_path += "\n\textern/include";
		if(Directory::isDirectory(path + "extern/lib/windows/" + arch)) library_path_windows += "\n\t\textern/lib/windows/" + arch + "";
		if(Directory::isDirectory(path + "extern/lib/linux/" + arch)) library_path_linux += "\n\t\textern/lib/linux/" + arch + "";
		if(Directory::isDirectory(path + "extern/lib/macos/" + arch)) library_path_macos += "\n\t\textern/lib/macos/" + arch + "";
	}
	
	vars.append("INCLUDE_PATH", include_path);
	vars.append("LIBRARY_PATH_WINDOWS", library_path_windows);
	vars.append("LIBRARY_PATH_LINUX", library_path_linux);
	vars.append("LIBRARY_PATH_MACOS", library_path_macos);
	
	// libraries
	String link_library;
	auto libs_it = args.find("libs");
	if(libs_it) {
		for(const String &lib : libs_it->data.split(" ")) {
			if(lib.begins("-l")) {
				link_library += " ";
				link_library += lib.get() + 2;
			}
		}
	}
	vars.append("LINK_LIBRARY", link_library);
}

/*
 */
static bool create_cmake(const Arguments &args, const String &cmake_name) {
	
	// check arguments
	if(!check_args(args, { "std", "name", "target", "version", "arch", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, cmake_name);
	if(!root) {
		TS_LOG(Error, "create_cmake(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String name = args["name"];
	String srcs = args["srcs"];
	String target = get_target(args);
	
	// cmake variables
	Variables vars;
	vars.append("NAME", name);
	vars.append("TARGET", target);
	create_cmake_vars(vars, args, root, cmake_name);
	
	// sources
	String sources;
	for(const String &src : srcs.split(" \n")) {
		sources += "\n\t" + src;
	}
	vars.append("SOURCES", sources);
	
	// project template
	#include "templates/cmake.blob"
	if(!save_template(cmake_name, cmake_blob_src, vars)) {
		TS_LOG(Error, "create_cmake(): can't save project template\n");
		return false;
	}
	
	return true;
}

/*****************************************************************************\
 *
 * Makefile
 *
\*****************************************************************************/

/*
 */
static bool create_makefile(const Arguments &args, const String &makefile_name) {
	
	// check arguments
	if(!check_args(args, { "std", "target", "version", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, makefile_name);
	if(!root) {
		TS_LOG(Error, "create_makefile(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String std = args["std"];
	String srcs = args["srcs"];
	String version = args["version"];
	String target = get_target(args);
	
	// cmake variables
	Variables vars;
	vars.append("STD", std);
	vars.append("ROOT", root);
	vars.append("TARGET", target);
	vars.append("VERSION", version);
	
	// sources
	String sources;
	for(const String &src : srcs.split(" \n")) {
		sources += " " + src;
	}
	vars.append("SOURCES", sources);
	
	// cflags
	String cflags;
	String path = get_dirname(makefile_name);
	if(Directory::isDirectory(path + "extern")) {
		cflags += " -Iextern";
		if(Directory::isDirectory(path + "extern/include")) cflags += " -Iextern/include";
	}
	vars.append("CFLAGS", cflags);
	
	// ldflags
	String ldflags;
	auto libs_it = args.find("libs");
	if(libs_it) ldflags = libs_it->data;
	vars.append("LDFLAGS", ldflags);
	
	// project template
	#include "templates/makefile.blob"
	if(!save_template(makefile_name, makefile_blob_src, vars)) {
		TS_LOG(Error, "create_makefile(): can't save project template\n");
		return false;
	}
	
	return true;
}

/*****************************************************************************\
 *
 * VCXProj
 *
\*****************************************************************************/

/*
 */
static void create_vcxproj_vars(Variables &vars, const Arguments &args, const String &root, const char *prefix, const String &name) {
	
	// get arguments
	String arch = args["arch"];
	String version = args["version"];
	
	// set variables
	vars.append("ARCH", arch);
	vars.append("VERSION", version);
	
	// local path
	String include_path = String::tformat("{0}include;{0}plugins;{0}samples;", root);
	String library_path = String::tformat("{0}lib/{1}/{3}{2};", root, prefix, arch, version);
	
	// extern path
	String path = get_dirname(name);
	if(Directory::isDirectory(path + "extern")) {
		include_path += "extern;";
		String extern_include = String("extern/include");
		String extern_library = String::tformat("extern/lib/windows/{0}", arch);
		if(Directory::isDirectory(path + extern_include)) include_path += extern_include + ";";
		if(Directory::isDirectory(path + extern_library)) library_path += extern_library + ";";
	}
	
	vars.append("INCLUDE_PATH", include_path.replace("/", "\\"));
	vars.append("LIBRARY_PATH", library_path.replace("/", "\\"));
}

/*
 */
static bool create_vcxproj(const Arguments &args, const String &vcxproj_name) {
	
	// check arguments
	if(!check_args(args, { "name", "target", "toolset", "version", "arch", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, vcxproj_name);
	if(!root) {
		TS_LOG(Error, "create_vcxproj(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String name = args["name"];
	String srcs = args["srcs"];
	String hash = get_hash(args);
	String target = get_target(args);
	String toolset = args["toolset"];
	
	// vcxproj variables
	Variables vars;
	vars.append("NAME", name);
	vars.append("GUID", get_guid(hash));
	vars.append("TARGET", target);
	vars.append("TOOLSET", toolset);
	create_vcxproj_vars(vars, args, root, "windows", vcxproj_name);
	
	vars.append("DEBUGGER_PATH", String::tformat("{0}bin;", root).replace("/", "\\"));
	
	// sources
	String sources;
	for(const String &src : srcs.split(" \n")) {
		sources += String::tformat("\n\t\t<ClCompile Include=\"{0}\"/>", src.replace("/", "\\"));
	}
	vars.append("SOURCES", sources);
	
	// resources
	String resources;
	auto resources_it = args.find("resources");
	if(resources_it && resources_it->data) {
		for(const String &resource : resources_it->data.split(" \n")) {
			resources += String::tformat("\n\t\t<ResourceCompile Include=\"{0}\"/>", resource.replace("/", "\\"));
		}
	}
	vars.append("RESOURCES", resources);
	
	// project template
	#include "templates/vcxproj.blob"
	if(!save_template(vcxproj_name, vcxproj_blob_src, vars)) {
		TS_LOG(Error, "create_vcxproj(): can't save project template\n");
		return false;
	}
	
	return true;
}

/*
 */
static bool create_vcxproj_uwp(const Arguments &args, const String &vcxproj_name) {
	
	// check arguments
	if(!check_args(args, { "appx", "name", "target", "toolset", "version", "arch", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, vcxproj_name);
	if(!root) {
		TS_LOG(Error, "create_vcxproj_uwp(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String name = args["name"];
	String appx = args["appx"];
	String srcs = args["srcs"];
	String hash = get_hash(args);
	String target = get_target(args);
	String toolset = args["toolset"];
	
	// vcxproj variables
	Variables vars;
	vars.append("NAME", name);
	vars.append("APPX", appx);
	vars.append("GUID", get_guid(hash));
	vars.append("TARGET", target);
	vars.append("TOOLSET", toolset);
	vars.append("ROOT", root.replace("/", "\\"));
	create_vcxproj_vars(vars, args, root, "winapp", vcxproj_name);
	
	// sources
	String sources;
	for(const String &src : srcs.split(" \n")) {
		sources += String::tformat("\n\t\t<ClCompile Include=\"{0}\"/>", src.replace("/", "\\"));
	}
	vars.append("SOURCES", sources);
	
	// resources
	String resources;
	String path = get_dirname(vcxproj_name);
	String assets = get_assets(args, vcxproj_name);
	if(assets) {
		for(const String &asset : assets.split(" \n")) {
			String name = asset.replace("/", "\\");
			if(Directory::isDirectory(path + asset)) name += "\\*";
			resources += String::tformat(R"(
		<None Include="{0}">
			<DeploymentContent>true</DeploymentContent>
		</None>)", name);
		}
	}
	vars.append("RESOURCES", resources);
	
	// project template
	#include "templates/vcxproj_uwp.blob"
	if(!save_template(vcxproj_name, vcxproj_uwp_blob_src, vars)) {
		TS_LOG(Error, "create_vcxproj_uwp(): can't save project template\n");
		return false;
	}
	
	// manifest template
	#include "templates/vcxproj_appx.blob"
	String manifest_name = path + "AppxManifest.xml";
	if(!save_template(manifest_name, vcxproj_appx_blob_src, vars)) {
		TS_LOG(Error, "create_vcxproj_uwp(): can't save manifest template\n");
		return false;
	}
	
	return true;
}

/*****************************************************************************\
 *
 * Xcodeproj
 *
\*****************************************************************************/

/*
 */
static void create_xcodeproj_vars(Variables &vars, const Arguments &args, const String &root, const char *prefix, const String &arch, const String &name) {
	
	// container name
	vars.append("CONTAINER", name.basename());
	
	// local path
	String include_path = String::tformat(R"(
					"$(PROJECT_DIR)/{0}include",
					"$(PROJECT_DIR)/{0}plugins",
					"$(PROJECT_DIR)/{0}samples",)", root);
	
	String library_path = String::tformat(R"(
					"$(PROJECT_DIR)/{0}lib/{1}/{2}",)", root, prefix, arch);
	
	// extern path
	String path = get_dirname(name);
	if(Directory::isDirectory(path + "extern")) {
		include_path += R"(
					"$(PROJECT_DIR)/extern",)";
		if(Directory::isDirectory(path + "extern/include")) {
			include_path += R"(
					"$(PROJECT_DIR)/extern/include",)";
		}
		if(Directory::isDirectory(String::tformat("{0}extern/lib/{1}/{2}", path, prefix, arch))) {
			library_path += String::tformat(R"(
					"$(PROJECT_DIR)/extern/lib/{0}/{1}",)", prefix, arch);
		}
	}
	
	vars.append("INCLUDE_PATH", include_path);
	vars.append("LIBRARY_PATH", library_path);
	
	// libraries
	String ldflags;
	auto libs_it = args.find("libs");
	if(libs_it) {
		for(const String &lib : libs_it->data.split(" ")) {
			if(lib.begins("-l")) ldflags += String::tformat(R"(
					"{0}",)", lib);
		}
	}
	vars.append("LDFLAGS", ldflags);
}

/*
 */
static bool create_xcodeproj_project(const String &name) {
	
	// create directory
	if(Directory::isDirectory(name) && !Directory::removeDirectory(name, true)) {
		TS_LOGF(Error, "create_xcodeproj_project(): can't remove \"%s\" directory\n", name.get());
		return false;
	}
	if(!Directory::createDirectory(name + "/xcshareddata/xcschemes", true)) {
		TS_LOGF(Error, "create_xcodeproj_project(): can't create \"%s\" directory\n", name.get());
		return false;
	}
	
	return true;
}

/*
 */
static bool create_xcodeproj(const Arguments &args, const String &xcodeproj_name) {
	
	// check arguments
	if(!check_args(args, { "std", "target", "arch", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, xcodeproj_name);
	if(!root) {
		TS_LOG(Error, "create_xcodeproj(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String std = args["std"];
	String arch = args["arch"];
	String srcs = args["srcs"];
	String hash = get_hash(args);
	String target = get_target(args);
	
	// variables
	Variables vars;
	vars.append("STD", std);
	vars.append("TARGET", target);
	vars.append("ARCH", arch);
	create_xcodeproj_vars(vars, args, root, "macos", arch, xcodeproj_name);
	
	vars.append("RUN_PATH", String::tformat(R"(
					"$(PROJECT_DIR)/{0}lib/macos/{1}",)", root, arch));
	
	// sources
	String files;
	String groups;
	String sources;
	uint32_t uuid = 0;
	for(const String &src : srcs.split(" \n")) {
		String uuid_0 = get_uuid(hash, uuid++);
		String uuid_1 = get_uuid(hash, uuid++);
		files += String::tformat(R"(
		{0} = { isa = PBXBuildFile; fileRef = {1}; };
		{1} = { isa = PBXFileReference; path = {2}; sourceTree = "<group>"; };)", uuid_0, uuid_1, src);
		groups += String::tformat(R"(
				{0},)", uuid_1);
		sources += String::tformat(R"(
				{0},)", uuid_0);
	}
	
	vars.append("FILES", files);
	vars.append("GROUPS", groups);
	vars.append("SOURCES", sources);
	
	// create uuid
	Array<String> uuids(32);
	for(uint32_t i = 0; i < uuids.size(); i++) {
		uuids[i] = String::format("ID%02u", i);
		vars.append(uuids[i].get(), get_uuid(hash, uuid++));
	}
	
	// create project
	if(!create_xcodeproj_project(xcodeproj_name)) {
		TS_LOG(Error, "create_xcodeproj(): can't create project\n");
		return false;
	}
	
	// project template
	#include "templates/pbxproj.blob"
	String project_name = xcodeproj_name + "/project.pbxproj";
	if(!save_template(project_name, pbxproj_blob_src, vars)) {
		TS_LOG(Error, "create_xcodeproj(): can't save project template\n");
		return false;
	}
	
	// scheme template
	#include "templates/pbxscheme.blob"
	String scheme_name = String::tformat("{0}/xcshareddata/xcschemes/{1}.{2}.xcscheme", xcodeproj_name, target, arch);
	if(!save_template(scheme_name, pbxscheme_blob_src, vars)) {
		TS_LOG(Error, "create_xcodeproj(): can't save scheme template\n");
		return false;
	}
	
	return true;
}

/*
 */
static bool create_xcodeproj_ios(const Arguments &args, const String &xcodeproj_name) {
	
	// check arguments
	if(!check_args(args, { "std", "ipa", "target", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, xcodeproj_name);
	if(!root) {
		TS_LOG(Error, "create_xcodeproj_ios(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String std = args["std"];
	String ipa = args["ipa"];
	String srcs = args["srcs"];
	String arch = String("arm64");
	String hash = get_hash(args);
	String target = get_target(args);
	
	// variables
	Variables vars;
	vars.append("STD", std);
	vars.append("IPA", ipa);
	vars.append("TARGET", target);
	create_xcodeproj_vars(vars, args, root, "ios", arch, xcodeproj_name);
	
	// sources
	String files;
	String groups;
	String sources;
	uint32_t uuid = 0;
	for(const String &src : srcs.split(" \n")) {
		String uuid_0 = get_uuid(hash, uuid++);
		String uuid_1 = get_uuid(hash, uuid++);
		files += String::tformat(R"(
		{0} = { isa = PBXBuildFile; fileRef = {1}; };
		{1} = { isa = PBXFileReference; path = {2}; sourceTree = "<group>"; };)", uuid_0, uuid_1, src);
		groups += String::tformat(R"(
				{0},)", uuid_1);
		sources += String::tformat(R"(
				{0},)", uuid_0);
	}
	
	// resources
	String resources;
	String assets = get_assets(args, xcodeproj_name);
	if(assets) {
		for(const String &asset : assets.split(" \n")) {
			String uuid_0 = get_uuid(hash, uuid++);
			String uuid_1 = get_uuid(hash, uuid++);
			files += String::tformat(R"(
		{0} = { isa = PBXBuildFile; fileRef = {1}; settings = { ATTRIBUTES = ("--decompress"); }; };
		{1} = { isa = PBXFileReference; path = {2}; sourceTree = "<group>"; };)", uuid_0, uuid_1, asset);
			groups += String::tformat(R"(
				{0},)", uuid_1);
			resources += String::tformat(R"(
				{0},)", uuid_0);
		}
	}
	
	vars.append("FILES", files);
	vars.append("GROUPS", groups);
	vars.append("SOURCES", sources);
	vars.append("RESOURCES", resources);
	
	// create uuid
	Array<String> uuids(32);
	for(uint32_t i = 0; i < uuids.size(); i++) {
		uuids[i] = String::format("ID%02u", i);
		vars.append(uuids[i].get(), get_uuid(hash, uuid++));
	}
	
	// create project
	if(!create_xcodeproj_project(xcodeproj_name)) {
		TS_LOG(Error, "create_xcodeproj_ios(): can't create project\n");
		return false;
	}
	
	// project template
	#include "templates/pbxproj_ios.blob"
	String project_name = xcodeproj_name + "/project.pbxproj";
	if(!save_template(project_name, pbxproj_ios_blob_src, vars)) {
		TS_LOG(Error, "create_xcodeproj_ios(): can't save project template\n");
		return false;
	}
	
	// scheme template
	#include "templates/pbxscheme_ios.blob"
	String scheme_name = String::tformat("{0}/xcshareddata/xcschemes/{1}.{2}.xcscheme", xcodeproj_name, target, arch);
	if(!save_template(scheme_name, pbxscheme_ios_blob_src, vars)) {
		TS_LOG(Error, "create_xcodeproj_ios(): can't save scheme template\n");
		return false;
	}
	
	return true;
}

/*****************************************************************************\
 *
 * Gradle
 *
\*****************************************************************************/

/*
 */
static void create_gradle_vars(Variables &vars, const Arguments &args, const String &root, const String &name) {
	
	// get arguments
	String std = args["std"];
	
	// set variables
	vars.append("STD", std);
	
	// local path
	String include_path = String::tformat("\n\t{0}include\n\t{0}plugins\n\t{0}samples", root);
	String library_path = String::tformat("\n\t{0}lib/android/arm64", root);
	
	// extern path
	String path = get_dirname(name);
	if(Directory::isDirectory(path + "extern")) {
		include_path += "\n\t../extern";
		if(Directory::isDirectory(path + "extern/include")) include_path += "\n\t../extern/include";
		if(Directory::isDirectory(path + "extern/lib/android/arm64")) library_path += "\n\t../extern/lib/android/arm64";
	}
	
	vars.append("INCLUDE_PATH", include_path);
	vars.append("LIBRARY_PATH", library_path);
	
	// libraries
	String link_library;
	auto libs_it = args.find("libs");
	if(libs_it) {
		for(const String &lib : libs_it->data.split(" ")) {
			if(lib.begins("-l")) {
				link_library += " ";
				link_library += lib.get() + 2;
			}
		}
	}
	vars.append("LINK_LIBRARY", link_library);
}

/*
 */
static bool create_gradle(const Arguments &args, const String &resource_path, const String &gradle_name) {
	
	// check arguments
	if(!check_args(args, { "std", "apk", "target", "minsdk", "srcs" })) return false;
	
	// root directory
	String root = get_root(args, gradle_name + "/");
	if(!root) {
		TS_LOG(Error, "create_gradle(): can't get root directory\n");
		return false;
	}
	
	// get arguments
	String apk = args["apk"];
	String srcs = args["srcs"];
	String minsdk = args["minsdk"];
	String target = get_target(args);
	
	// cmake variables
	Variables vars;
	vars.append("APK", apk);
	vars.append("ROOT", root);
	vars.append("TARGET", target);
	vars.append("MINSDK", minsdk);
	create_gradle_vars(vars, args, root, gradle_name);
	
	// sources
	String sources;
	for(const String &src : srcs.split(" \n")) {
		sources += "\n\t../" + src;
	}
	vars.append("SOURCES", sources);
	
	// resources
	String resources;
	String assets = get_assets(args, gradle_name);
	if(assets) {
		for(const String &asset : assets.split(" \n")) {
			if(resources) resources += ", ";
			resources += String::tformat("\"../{0}\"", asset);
		}
	}
	vars.append("RESOURCES", resources);
	
	// create directory
	if(Directory::isDirectory(gradle_name) && !Directory::removeDirectory(gradle_name, true)) {
		TS_LOGF(Error, "create_gradle(): can't remove \"%s\" directory\n", gradle_name.get());
		return false;
	}
	if(!Directory::createDirectory(gradle_name + "/src/main", true)) {
		TS_LOGF(Error, "create_gradle(): can't create \"%s\" directory\n", gradle_name.get());
		return false;
	}
	
	// build template
	#include "templates/gradle_build.blob"
	String build_name = gradle_name + "/build.gradle";
	if(!save_template(build_name, gradle_build_blob_src, vars)) {
		TS_LOG(Error, "create_gradle(): can't save build template\n");
		return false;
	}
	
	// project template
	#include "templates/gradle_cmake.blob"
	String project_name = gradle_name + "/CMakeLists.txt";
	if(!save_template(project_name, gradle_cmake_blob_src, vars)) {
		TS_LOG(Error, "create_gradle(): can't save project template\n");
		return false;
	}
	
	// settings template
	#include "templates/gradle_settings.blob"
	String settings_name = gradle_name + "/settings.gradle";
	if(!save_template(settings_name, gradle_settings_blob_src, vars)) {
		TS_LOG(Error, "create_gradle(): can't save settings template\n");
		return false;
	}
	
	// manifest template
	#include "templates/gradle_apk.blob"
	String manifest_name = gradle_name + "/src/main/AndroidManifest.xml";
	if(!save_template(manifest_name, gradle_apk_blob_src, vars)) {
		TS_LOG(Error, "create_gradle(): can't save manifest template\n");
		return false;
	}
	
	// copy gradle
	if(resource_path) {
		if(!Directory::isDirectory(resource_path + "/gradle")) {
			TS_LOG(Error, "create_gradle(): can't find gradle directory\n");
			return false;
		}
		if(!Directory::copyDirectory(resource_path + "/gradle", gradle_name + "/gradle")) {
			TS_LOG(Error, "create_gradle(): can't copy gradle directory\n");
			return false;
		}
		if(File::isFile(resource_path + "/gradlew")) Directory::copyFile(resource_path + "/gradlew", gradle_name + "/gradlew", true);
		if(File::isFile(resource_path + "/gradlew.bat")) Directory::copyFile(resource_path + "/gradlew.bat", gradle_name + "/gradlew.bat", true);
	}
	
	return true;
}

/*****************************************************************************\
 *
 * main
 *
\*****************************************************************************/

/*
 */
int32_t main(int32_t argc, char **argv) {
	
	// print help
	if(argc < 2 || String(argv[1]) == "-h") {
		Log::printf("Tellusim Project Generation Tool (build " __DATE__ " https://tellusim.com/)\nUsage: %s Makefile -cmake -makefile\n", argv[0]);
		Log::print("  -verbose          Verbose output\n");
		Log::print("  -clear            Clear arguments\n");
		Log::print("  -info             Print arguments\n");
		Log::print("  -core <path>      Create Core App\n");
		Log::print("  -engine <path>    Create Engine App\n");
		Log::print("  -name <name>      Set project name\n");
		Log::print("  -root <path>      Path to Tellusim SDK root\n");
		Log::print("  -target <name>    Binary name (main)\n");
		Log::print("  -postfix <name>   Target postfix\n");
		Log::print("  -arch <name>      Project architecture\n");
		Log::print("  -ipa <name>       iOS IPA name\n");
		Log::print("  -apk <name>       Android APK name\n");
		Log::print("  -appx <name>      Windows UWP APPX name\n");
		Log::print("  -srcs <name>      Source files\n");
		Log::print("  -libs <name>      Library names\n");
		Log::print("  -assets <name>    Resource names\n");
		Log::print("  -std <version>    C++ STD version (11)\n");
		Log::print("  -toolset <ver>    Visual Studio toolset (v143)\n");
		Log::print("  -version <ver>    Visual Studio version (vs170)\n");
		Log::print("  -minsdk <ver>     Android SDK version (30)\n");
		Log::print("  -resource <path>  Path to resource directory\n");
		Log::print("  -check <argument> Quit if the argument not defined\n");
		Log::print("  -cmake (name)     Create CMake project\n");
		Log::print("  -makefile (name)  Create Makefile project\n");
		Log::print("  -vcxproj (name)   Create Visual Studio project\n");
		Log::print("  -xcodeproj (name) Create Xcode project\n");
		Log::print("  -gradle (name)    Create Gradle project\n");
		return 0;
	}
	
	// arguments
	Set<const char*> names = {
		"name", "root", "target", "postfix", "arch", "ipa", "apk", "appx",
		"srcs", "libs", "assets", "std", "toolset", "version", "minsdk",
	};
	Map<const char*, const char*> renames = {
		{ "src", "srcs" },
		{ "lib", "libs" },
		{ "asset", "assets" },
	};
	
	// makefile name
	String makefile_name;
	
	// project path
	String project_name;
	String project_path;
	
	// resource path
	String resource_path;
	
	// project arguments
	Arguments arguments;
	create_args(arguments);
	
	// message log level
	Log::setLevel(Log::Message);
	
	// command line arguments
	for(int32_t i = 1; i < argc; i++) {
		const char *s = argv[i];
		
		// commands
		if(s[0] == '-') {
			while(*s == '-') s++;
			String command = String(s);
			
			// verbose output
			if(command == "verbose" || command == "v") {
				Log::setLevel(Log::Verbose);
			}
			
			// clear arguments
			else if(command == "clear") {
				arguments.clear();
				create_args(arguments);
				makefile_name.clear();
				project_name.clear();
				project_path.clear();
				resource_path.clear();
			}
			
			// print current arguments
			else if(command == "info") {
				for(const auto &it : arguments) {
					const auto &arguments = it.data.split(" \n");
					if(arguments.size() > 1) {
						TS_LOGF(Message, "%s = \n", it.key.get());
						for(const auto &value : arguments) {
							TS_LOGF(Message, " %s\n", value.get());
						}
					} else {
						TS_LOGF(Message, "%s = %s\n", it.key.get(), it.data.get());
					}
				}
			}
			
			// create application
			else if(command == "core" && i + 1 < argc) {
				project_path = String(argv[++i]) + "/";
				TS_LOGF(Verbose, "Creating \"%s\" Core App\n", project_path.get());
				if(!create_core(arguments, project_path)) {
					TS_LOGF(Error, "%s: can't create \"%s\" core app\n", argv[0], project_path.get());
					return 1;
				}
				project_name = arguments["name"];
			}
			else if(command == "engine" && i + 1 < argc) {
				project_path = String(argv[++i]) + "/";
				TS_LOGF(Verbose, "Creating \"%s\" Engine App\n", project_path.get());
				if(!create_engine(arguments, project_path)) {
					TS_LOGF(Error, "%s: can't create \"%s\" engine app\n", argv[0], project_path.get());
					return 1;
				}
				project_name = arguments["name"];
			}
			
			// command line parameters
			else if(names.find(command) && i + 1 < argc) {
				arguments.append(command, String(argv[++i]));
			}
			else if(renames.find(command) && i + 1 < argc) {
				auto rename_it = renames.find(command);
				auto argument_it = arguments.find(rename_it->data);
				String argument = String(argv[++i]);
				if(argument_it) argument_it->data += " " + argument;
				else arguments.append(String(rename_it->data), argument);
			}
			
			// resource path
			else if(command == "resource" && i + 1 < argc) {
				resource_path = String(argv[++i]);
			}
			
			// check argument
			else if(command == "check" && i + 1 < argc) {
				if(!arguments.find(argv[++i])) return 0;
			}
			
			// targets
			else if(command == "cmake") {
				String name = project_path;
				if(i + 1 < argc && argv[i + 1][0] != '-') name += argv[++i];
				else name += "CMakeLists.txt";
				TS_LOGF(Verbose, "Creating \"%s\"\n", name.get());
				if(!create_cmake(arguments, name)) {
					if(makefile_name) TS_LOGF(Error, "%s: can't create \"%s\" from \"%s\"\n", argv[0], name.get(), makefile_name.get());
					else TS_LOGF(Error, "%s: can't create \"%s\"\n", argv[0], name.get());
					return 1;
				}
			}
			else if(command == "makefile") {
				String name = project_path;
				if(i + 1 < argc && argv[i + 1][0] != '-') name += argv[++i];
				else name += "Makefile";
				TS_LOGF(Verbose, "Creating \"%s\"\n", name.get());
				if(!create_makefile(arguments, name)) {
					if(makefile_name) TS_LOGF(Error, "%s: can't create \"%s\" from \"%s\"\n", argv[0], name.get(), makefile_name.get());
					else TS_LOGF(Error, "%s: can't create \"%s\"\n", argv[0], name.get());
					return 1;
				}
			}
			else if(command == "vcxproj") {
				bool ret = false;
				String name = project_path;
				auto appx_it = arguments.find("appx");
				if(i + 1 < argc && argv[i + 1][0] != '-') name += argv[++i];
				else if(project_name && appx_it) name += project_name + "_uwp.vcxproj";
				else if(project_name) name += project_name + ".vcxproj";
				else name.clear();
				if(name) {
					TS_LOGF(Verbose, "Creating \"%s\"\n", name.get());
					if(appx_it) ret = create_vcxproj_uwp(arguments, name);
					else ret = create_vcxproj(arguments, name);
				}
				if(!ret) {
					if(makefile_name) TS_LOGF(Error, "%s: can't create vcxproj from \"%s\"\n", argv[0], makefile_name.get());
					else TS_LOGF(Error, "%s: can't create vcxproj\n", argv[0]);
					return 1;
				}
			}
			else if(command == "xcodeproj") {
				bool ret = false;
				String name = project_path;
				auto ipa_it = arguments.find("ipa");
				if(i + 1 < argc && argv[i + 1][0] != '-') name += argv[++i];
				else if(project_name && ipa_it) name += project_name + "_ios.xcodeproj";
				else if(project_name) name += project_name + ".xcodeproj";
				else name.clear();
				if(name) {
					TS_LOGF(Verbose, "Creating \"%s\"\n", name.get());
					if(ipa_it) ret = create_xcodeproj_ios(arguments, name);
					else ret = create_xcodeproj(arguments, name);
				}
				if(!ret) {
					if(makefile_name) TS_LOGF(Error, "%s: can't create xcodeproj from \"%s\"\n", argv[0], makefile_name.get());
					else TS_LOGF(Error, "%s: can't create xcodeproj\n", argv[0]);
					return 1;
				}
			}
			else if(command == "gradle") {
				bool ret = false;
				String name = project_path;
				if(i + 1 < argc && argv[i + 1][0] != '-') name += argv[++i];
				else if(project_name) name += project_name + ".gradle";
				else name.clear();
				if(name) {
					TS_LOGF(Verbose, "Creating \"%s\"\n", name.get());
					ret = create_gradle(arguments, resource_path, name);
				}
				if(!ret) {
					if(makefile_name) TS_LOGF(Error, "%s: can't create gradle from \"%s\"\n", argv[0], makefile_name.get());
					else TS_LOGF(Error, "%s: can't create gradle\n", argv[0]);
					return 1;
				}
			}
			else {
				TS_LOGF(Error, "%s: unknown argument \"%s\"\n", argv[0], command.get());
				return 1;
			}
		}
		
		// load makefile
		else {
			makefile_name = String(argv[i]);
			#if _WIN32
				if(makefile_name.size() > 5 && makefile_name.begins("/mnt/")) {
					makefile_name = makefile_name.replace("/mnt", "");
					makefile_name[0] = makefile_name[1];
					makefile_name[1] = ':';
				}
			#endif
			if(!load_makefile(arguments, makefile_name)) {
				TS_LOGF(Error, "%s: can't load \"%s\" Makefile\n", argv[0], makefile_name.get());
				return 1;
			}
			project_path = makefile_name.dirname();
			if(project_path && project_path.back() == '/') {
				project_name = project_path.substring(0, project_path.size() - 1);
				project_name = project_name.basename();
				if(project_name && !arguments.find("name")) {
					arguments.append(String("name"), project_name);
				}
			}
		}
	}
	
	return 0;
}
