// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.

#include <algorithm>
#include <deque>
#include <future>
#include <iostream>
#include <map>
#include <mutex>
#include <regex>
#include <set>
#include <string>
#include <thread>

#include <json/json.hpp>
#include <process.hpp>

#include <foundation/build_info.h>
#include <foundation/cmd_line.h>
#include <foundation/data.h>
#include <foundation/dir.h>
#include <foundation/file.h>
#include <foundation/format.h>
#include <foundation/log.h>
#include <foundation/path_tools.h>
#include <foundation/profiler.h>
#include <foundation/string.h>
#include <foundation/time.h>
#include <foundation/time_chrono.h>
#include <foundation/xxhash.h>

#include <engine/forward_pipeline.h>
#include <engine/geometry.h>
#include <engine/meta.h>
#include <engine/render_pipeline.h>
#include <engine/scene.h>

#include <platform/filesystem_watcher.h>
#include <platform/process.h>

#define HASH_METHOD 2 // 0:SHA1 1:Murmurv3 2:xxHash

#if USE_SHA1
#include <foundation/sha1.h>
#else
#include <foundation/murmur3.h>
#endif

#if WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <shellapi.h>
#undef CopyFile
#endif

using namespace hg;

namespace assetc {

static const int classify_progress_weight = 30;
static const int compile_progress_weight = 20;
static const int run_queue_progress_weight = 50;

//
static std::string cwd;

//
static std::atomic<int> error_count;
static bool log_errors_to_stderr = false;

static bool progress = false;

static std::mutex failed_inputs_mutex;
static std::set<std::string> failed_inputs;

static void ReportFailedInput(std::string name) {
	std::lock_guard<std::mutex> lock(failed_inputs_mutex);
	failed_inputs.insert(std::move(name));
}

void log_error(const json &js) {
	std::stringstream ss;

	if (log_errors_to_stderr) {
		ss << js.dump(4) << std::endl;
		std::cerr << ss.str();
	} else {
		const auto type = js["type"].get<std::string>();

		ss << "Error " << type << std::endl;

		if (type == "CompileProcessReturnedNonZero") {
			ss << " -> CWD: " << js["cwd"].get<std::string>() << std::endl;
			ss << " -> CMD: " << js["cmd"].get<std::string>() << std::endl;

			const auto out = js["out"].get<std::string>();
			if (!out.empty())
				ss << " -> STDOUT:" << std::endl << out << std::endl;

			const auto err = js["err"].get<std::string>();
			if (!err.empty())
				ss << " -> STDERR:" << std::endl << err << std::endl;
		}

		std::cout << ss.str();
	}

	++error_count;
}

//
static bool compile_with_debug_info = false;

//
static std::string global_shader_defines;

//
struct Toolchain {
	std::string shaderc, texturec, luac, cmft, recastc, texconv, bulletc;
};

static Toolchain toolchain;

static void ToolchainExists() {
	if (!IsFile(toolchain.shaderc.c_str()))
		toolchain.shaderc.clear();
	if (!IsFile(toolchain.texturec.c_str()))
		toolchain.texturec.clear();
	if (!IsFile(toolchain.luac.c_str()))
		toolchain.luac.clear();
	if (!IsFile(toolchain.cmft.c_str()))
		toolchain.cmft.clear();
	if (!IsFile(toolchain.recastc.c_str()))
		toolchain.recastc.clear();
	if (!IsFile(toolchain.texconv.c_str()))
		toolchain.texconv.clear();
	if (!IsFile(toolchain.bulletc.c_str()))
		toolchain.bulletc.clear();
}

static void SetToolchain(const std::string &path) {
#ifdef WIN32
	toolchain = {PathJoin({path, "shaderc.exe"}), PathJoin({path, "texturec.exe"}), PathJoin({path, "luac.exe"}), PathJoin({path, "cmft.exe"}),
		PathJoin({path, "recastc.exe"}), PathJoin({path, "texconv.exe"}), PathJoin({path, "bulletc.exe"})};
#else
	toolchain = {PathJoin({path, "shaderc"}), PathJoin({path, "texturec"}), PathJoin({path, "luac"}), PathJoin({path, "cmft"}), PathJoin({path, "recast"}), PathJoin({path, "texconv"}), PathJoin({path, "bulletc"})};
#endif
}

//
using process_id = unsigned long;
static process_id poll_process_id = 0;

static bool IsPollProcessUp() {
	if (poll_process_id == 0)
		return true;

	bool running = true;
	is_process_running(poll_process_id, running);

	return running;
}

//
static std::string api = HG_GRAPHIC_API; // DX11/DX12/GL

#if defined(WIN32)
#define ASSSETC_DEFAULT_PLATFORM "windows"
#elif defined(__linux__)
#define ASSSETC_DEFAULT_PLATFORM "linux"
#elif defined(__APPLE__)
#define ASSSETC_DEFAULT_PLATFORM "osx"
#else
#define ASSSETC_DEFAULT_PLATFORM ""
#endif
// remaining shaderc platforms
//		android
//		asm.js
//		ios
//		orbis

static std::string platform = ASSSETC_DEFAULT_PLATFORM;

#if 0
// CWE 561: The function 'SetAPI' is never used.
void SetAPI(const char *api_) { api = api_; }
#endif
//
static std::string input_dir;
static std::string output_dir;

static std::string FullInputPath(const std::string &path) { return PathJoin({input_dir, path}); }
static std::string FullOutputPath(const std::string &path) { return PathJoin({output_dir, path}); }

static std::string FullOutputDir(const std::string &path) { return CutFileName(FullOutputPath(path)); }
static bool MkOutputTree(const std::string &path) {
	const auto res = MkTree(FullOutputDir(path).c_str());
	if (!res) {
		const json json_err = {{"type", "FailedToMkOutputTree"}, {"path", path}};
		log_error(json_err);
	}
	return res;
}

#if HASH_METHOD == 0
using Hash = SHA1Hash;
#else
using Hash = std::array<char, 16>; // 128 bit
#endif

void ComputeHash(const void *data, size_t len, Hash &out) {
	ProfilerPerfSection perf("Manage/ComputeHash");

#if HASH_METHOD == 0
	ComputeSHA1(data, len, out);
#elif HASH_METHOD == 1
#if defined(_M_X64) || defined(__amd64__)
	MurmurHash3_x64_128(data, len, 0, &out);
#else
	MurmurHash3_x86_128(data, len, 0, &out);
#endif
#else
	XXH128(data, len, 0, &out);
#endif
}

//
struct CompilationDB {
	std::map<std::string, std::set<std::string>> output_to_inputs;
	std::map<std::string, Hash> source_hashes;
	std::map<std::string, Hash> output_build_params;
};

static CompilationDB compilation_db;

static size_t processed_count = 0;
static bool fast_check = false;

static bool TestInputs(std::map<std::string, Hash> &hashes, const std::set<std::string> &inputs) {
	bool need_refresh = false;

	for (const auto &i : inputs) {
		const auto input_path = FullInputPath(i);

		if (!IsFile(input_path.c_str()))
			continue;

		Data data;
		if (fast_check) {
			const auto info = GetFileInfo(input_path.c_str());
			if (info.size == 0)
				error(format("Failed to stat input '%1'").arg(input_path));

			Write(data, info.size);
			Write(data, info.modified);
		} else {
			if (!LoadDataFromFile(input_path.c_str(), data))
				error(format("Failed to load '%1' data").arg(input_path));
		}

		Hash hash;
		ComputeHash(data.GetData(), data.GetSize(), hash);

		const auto &j = compilation_db.source_hashes.find(i);

		if (j == std::end(compilation_db.source_hashes)) {
			debug(format("    [!] Source file '%1' hash missing, triggering refresh").arg(i));
			need_refresh = true;
		} else {
			if (j->second != hash) {
				debug(format("    [!] Source file '%1' hash mismatch, triggering refresh").arg(i));
				need_refresh = true; // modified source
			}
		}

		hashes[i] = hash; // update source file hash
	}

	return need_refresh;
}

static bool TestOutputs(const std::set<std::string> &outputs, const Data &build_context) {
	bool need_refresh = false;

	Hash build_params_hash{};
	ComputeHash(build_context.GetData(), build_context.GetSize(), build_params_hash);

	for (const auto &output : outputs) {
		if (!IsFile(FullOutputPath(output).c_str())) {
			debug(format("    [!] Output file '%1' missing, triggering refresh").arg(output));
			need_refresh = true; // missing output asset
		}

		auto i = compilation_db.output_build_params.find(output);
		if (i == std::end(compilation_db.output_build_params)) {
			debug(format("    [!] Output file '%1' build context missing, triggering refresh").arg(output));
			need_refresh = true;
		} else {
			if (i->second != build_params_hash) {
				debug(format("    [!] Output file '%1' build context changed, triggering refresh").arg(output));
				need_refresh = true; // build params modified
			}
		}

		compilation_db.output_build_params[output] = build_params_hash; // update build params hash
	}

	return need_refresh;
}

static bool NeedsCompilation(
	std::map<std::string, Hash> &hashes, const std::set<std::string> &inputs, const std::set<std::string> &outputs, const Data &build_context) {
	ProfilerPerfSection perf("Manage/NeedsCompilation");

	bool need_refresh = false;

	if (TestOutputs(outputs, build_context))
		need_refresh = true;

	if (TestInputs(hashes, inputs))
		need_refresh = true;

	if (need_refresh)
		processed_count += outputs.size();

	for (const auto &output : outputs)
		compilation_db.output_to_inputs[output] = inputs;

	return need_refresh;
}

static void CleanOutputs(const std::set<std::string> &outputs) {
	ProfilerPerfSection perf("Manage/CleanOutputs");

	for (const auto &output : outputs)
		Unlink(FullOutputPath(output).c_str());
}

//
void Reset() {
	compilation_db.source_hashes.clear();
	compilation_db.output_to_inputs.clear();
	compilation_db.output_build_params.clear();
}

//
void SetInputDir(const std::string &path) { input_dir = CleanPath(path); }

bool SetOutputDir(const std::string &path) {
	output_dir = CleanPath(path);
	const auto res = MkTree(output_dir.c_str());
	if (!res) {
		const json json_err = {{"type", "SetOutputDirFailedToMkTree"}, {"path", output_dir}};
		log_error(json_err);
	}
	return res;
}

//
static json LoadMeta(const std::string &path) {
	ProfilerPerfSection perf("Manage/LoadMeta");
	return LoadResourceMetaFromFile(FullInputPath(path));
}

//
static std::mutex default_log_mutex;
static std::map<std::string, std::string> default_log_output;
static std::map<std::string, std::string> default_error_log_output;

static void RunProcess(const std::string &name, const std::string &cmd, const std::string &cwd) {
	//
	const auto cmd_elms = split(cmd, " ");
	ProfilerPerfSection perf(format("Command/RunProcess/%1").arg(cmd_elms[0]));

	log(format("    Spawning compile process for %1").arg(name));

	TinyProcessLib::Process process(
		cmd, cwd,
		[&](const char *bytes, size_t n) {
			std::lock_guard<std::mutex> lock(default_log_mutex);
			default_log_output[name] += {bytes, n};
		},
		[&](const char *bytes, size_t n) {
			std::lock_guard<std::mutex> lock(default_log_mutex);
			default_error_log_output[name] += {bytes, n};
		},
		false);

	if (process.get_id() == 0) {
		ReportFailedInput(name);
		const json json_err = {{"type", "FailedToSpawnCompileProcess"}, {"name", name}};
		log_error(json_err);
	} else {
		const auto res = process.get_exit_status();

		std::string out, err;
		{
			std::lock_guard<std::mutex> lock(default_log_mutex);
			{
				auto i = default_log_output.find(name);
				if (i != std::end(default_log_output)) {
					out = std::move(i->second);
					default_log_output.erase(i);
				}
			}
			{
				auto i = default_error_log_output.find(name);
				if (i != std::end(default_error_log_output)) {
					err = std::move(i->second);
					default_error_log_output.erase(i);
				}
			}
		}

		if (res != 0) {
			ReportFailedInput(name);
			const json json_err = {{"type", "CompileProcessReturnedNonZero"}, {"res", res}, {"cmd", cmd}, {"cwd", cwd}, {"out", out}, {"err", err}};
			log_error(json_err);
		}
	}
}

//
struct task {
	std::string name;
	std::function<std::future<void>()> func;
};

static std::deque<task> task_queue;

static void PushAsyncProcessTask(const std::string &name, const std::string &cmd, const std::string &cwd) {
	task_queue.emplace_back(task{name, [=]() { return std::async(std::launch::async, RunProcess, name, cmd, cwd); }});
}

static int max_async_jobs = 0;

static void RunTaskQueue() {
	const auto task_count = task_queue.size();

	std::deque<std::future<void>> task_running;
	std::string current_task_name;

	while (!task_queue.empty() || !task_running.empty()) {
		while (!task_queue.empty() && task_running.size() < max_async_jobs) {
			current_task_name = task_queue.front().name;
			task_running.emplace_back(task_queue.front().func()); // start task
			task_queue.pop_front();
		}

		const auto task_done = task_count - (task_queue.size() + task_running.size());
		if (progress)
			std::cout << "-> Progress: " << task_done * run_queue_progress_weight / task_count + classify_progress_weight + compile_progress_weight << "% ("
					  << current_task_name << ")" << std::endl;

		time_ns t_ref = time_now();

		while (!task_running.empty()) {
			bool break_ = false;

			for (auto &t : task_running)
				if (t.wait_for(time_to_chrono(time_from_ms(1))) == std::future_status::ready) {
					break_ = true;
					break; // break asap to start a new task
				}

			if (break_)
				break;

			const auto t_now = time_now();

			if (t_now - t_ref >= time_from_sec(5)) {
				log(format("  %1 tasks running, %2 queued...").arg(task_running.size()).arg(task_queue.size()));
				t_ref = t_now;
			}
		}

		task_running.erase(std::remove_if(std::begin(task_running), std::end(task_running),
							   [](const std::future<void> &f) { return f.wait_for(time_to_chrono(0)) == std::future_status::ready; }),
			std::end(task_running));
	}
}

//
static const uint16_t CAB1 = 0xCAB1;

bool LoadCompilationDB(const char *filename) {
	ProfilerPerfSection perf("Manage/LoadCompilationDB");

	Reset();

	if (!Exists(filename))
		return false;

	ScopedFile file(Open(filename));
	if (!file)
		return false;

	//
	if (Read<uint16_t>(file) != CAB1)
		return false;

	const auto version = Read<uint16_t>(file);
	if (version > 2)
		return false;

	//
	if (version >= 2) {
		(void)ReadString(file); // build_sha
		(void)ReadString(file); // version_string
	}

	//
	const auto src_hashes_count = Read<uint32_t>(file);

	for (uint32_t i = 0; i < src_hashes_count; ++i) {
		const auto path = ReadString(file);
		const auto hash = Read<Hash>(file);
		compilation_db.source_hashes[path] = hash;
	}

	//
	const auto asset_to_source_count = Read<uint32_t>(file);

	for (uint32_t i = 0; i < asset_to_source_count; ++i) {
		const auto name = ReadString(file);
		const auto src_count = Read<uint32_t>(file);

		for (uint32_t j = 0; j < src_count; ++j) {
			const auto path = ReadString(file);
			compilation_db.output_to_inputs[name].insert(path);
		}
	}

	//
	if (Tell(file) < GetSize(file)) {
		const auto output_build_hash_count = Read<uint32_t>(file);

		for (uint32_t i = 0; i < output_build_hash_count; ++i) {
			const auto name = ReadString(file);
			const auto hash = Read<Hash>(file);
			compilation_db.output_build_params[name] = hash;
		}
	}

	return true;
}

bool LoadCompilationDB() { return LoadCompilationDB(FullOutputPath("assetc.cab").c_str()); }

bool SaveCompilationDB(const char *path) {
	ProfilerPerfSection perf("Manage/SaveCompilationDB");

	std::cout << format("    %1 input files").arg(compilation_db.source_hashes.size()) << std::endl;
	std::cout << format("    %1 output files").arg(compilation_db.output_to_inputs.size()) << std::endl;
	std::cout << format("    %1 processed").arg(processed_count) << std::endl;
	std::cout << format("    %1 failed").arg(error_count.load()) << std::endl;

	for (const auto &i : failed_inputs)
		std::cout << format("        - FAILED: %1").arg(i) << std::endl;
	failed_inputs.clear();

	std::cout << std::endl;

	processed_count = 0;
	error_count = 0;

	std::cout << format("  Saving compilation DB '%1'").arg(path) << std::endl;

	if (compilation_db.source_hashes.empty()) {
		std::cout << "    Empty DB, nothing to save" << std::endl;
		return false; // nothing to save
	}

	//
	ScopedFile file(OpenWrite(path));

	if (!file) {
		const json json_err = {{"type", "FailedToSaveCompilationDB"}, {"path", path}};
		log_error(json_err);
		return false;
	}

	/*
		version 0: initial version
		version 1: switch to murmurv3 hash
		version 2: add build sha and version string
	*/
	Write(file, CAB1);
	Write(file, uint16_t(2)); // version

	WriteString(file, get_build_sha());
	WriteString(file, get_version_string());

	//
	Write(file, uint32_t(compilation_db.source_hashes.size()));

	for (const auto &i : compilation_db.source_hashes) {
		WriteString(file, i.first);
		Write(file, i.second);
	}

	//
	Write(file, uint32_t(compilation_db.output_to_inputs.size()));

	for (const auto &i : compilation_db.output_to_inputs) {
		const auto &name = i.first;
		WriteString(file, name);

		const auto &sources = i.second;
		Write(file, uint32_t(sources.size()));
		for (auto &s : sources)
			WriteString(file, s);
	}

	//
	Write(file, uint32_t(compilation_db.output_build_params.size()));

	for (const auto &i : compilation_db.output_build_params) {
		WriteString(file, i.first);
		Write(file, i.second);
	}

	return true;
}

bool SaveCompilationDB() { return SaveCompilationDB(FullOutputPath("assetc.cab").c_str()); }

//
static PipelineInfo pipeline;

static void SetRenderPipeline(const PipelineInfo &_pipeline) { pipeline = _pipeline; }

//
static void Copy(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/Copy");

	__ASSERT__(!IsDir(path.c_str()));

	log(format("  Copy '%1'").arg(path));

	if (NeedsCompilation(hashes, {path}, {path}, {})) {
		const auto src = FullInputPath(path), dst = FullOutputPath(path);

		MkOutputTree(path);
		CleanOutputs({path});

		if (!CopyFile(src.c_str(), dst.c_str())) {
			ReportFailedInput(src);
			const json json_err = {{"type", "FailedToCopyInput"}, {"src", src}, {"dst", dst}};
			log_error(json_err);
		}
	} else {
		debug("    [O] Copy up to date");
	}
}

//
static std::string profile = "default";

//
static void ProcessScene(const std::string &src, const std::string &dst) {
	Scene scene;
	PipelineResources res;
	LoadSceneContext ctx;

	bool load_res = false;
	try {
		load_res = LoadSceneFromFile(src.c_str(), scene, res, GetForwardPipelineInfo(), ctx, LSSF_All | LSSF_DoNotLoadResources);
	} catch (...) {}

	if (!load_res) {
		ReportFailedInput(src);
		const json json_err = {{"type", "FailedToLoadScene"}, {"src", src}};
		log_error(json_err);
	} else {
		if (!SaveSceneBinaryToFile(dst.c_str(), scene, res)) {
			const json json_err = {{"type", "FailedToSaveScene"}, {"dst", dst}};
			log_error(json_err);
		}
	}
}

void Scene(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/Scene");

	log(format("  Scene '%1'").arg(path));

	Data build_ctx;
	Write(build_ctx, std::string(get_version_string()));
	Write(build_ctx, GetSceneBinaryFormatVersion());

	if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) {
		const auto src = FullInputPath(path), dst = FullOutputPath(path);

		MkOutputTree(path);
		CleanOutputs({path});

		task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessScene, src, dst); }});
	} else {
		debug("    [O] Scene up to date");
	}
}

//
void Copy(std::map<std::string, Hash> &hashes, const std::string &path);

//
static std::string Get_texconv_Format(const std::string &f) {
	if (f == "BC1")
		return "BC1_UNORM";
	if (f == "BC2")
		return "BC2_UNORM";
	if (f == "BC3")
		return "BC3_UNORM";
	if (f == "BC4")
		return "BC4_UNORM";
	if (f == "BC5")
		return "BC5_UNORM";
	if (f == "BC6H")
		return "BC6H_SF16";
	if (f == "BC7")
		return "BC7_UNORM";
	return "";
}

static bool PreprocessTexture(const json &i_preprocess_texture, std::map<std::string, Hash> &hashes, std::string &path) {
	// gather dependencies
	std::set<std::string> dependencies;
	for (const auto &i_directive : i_preprocess_texture.items())
		if (i_directive.key() == "construct") {
			if (i_directive.value().is_array()) {
				const auto channel_count = i_directive.value().size();

				if (channel_count < 3 || channel_count > 4) {
					error(format("Cannot construct texture '%1' due to incorrect number of input channel (3 or 4 supported)").arg(path));
					return false;
				}

				for (const auto &v : i_directive.value()) {
					if (v.is_number_integer()) { // constant
						;
					} else if (v.is_string()) { // channel or texture path
						const auto channel_v = v.get<std::string>();

						if (channel_v == "R" || channel_v == "G" || channel_v == "B" || channel_v == "A")
							dependencies.insert(path); // swizzle from input texture
						else
							dependencies.insert(channel_v); // swizzle from foreign texture
					} else if (v.is_object()) { // channel or texture path
						const auto dep_path = v["path"].get<std::string>();
						const auto channel_v = v["channel"].get<std::string>();
						dependencies.insert(dep_path);
					}
				}
			} else {
				error(format("Invalid texture construct tag for '%1', expected an array of channel source").arg(path));
				return false;
			}
		}

	const auto has_construct_directive = i_preprocess_texture.find("construct") != std::end(i_preprocess_texture);

	if (!has_construct_directive)
		dependencies.insert(path); // no construct directive implicitely means we need the input texture

	// build context
	Data build_ctx;
	Write(build_ctx, i_preprocess_texture.dump()); // the whole preprocess block is our build context

	// preprocess if input changed
	const auto output_path = CutFileExtension(path) + ".tmp"; // preprocess into the input folder, output folder is *write-only*

	if (!TestInputs(hashes, dependencies) && IsFile(FullInputPath(output_path).c_str()))
		return true; // nothing to be done

	// load all dependencies
	std::map<std::string, Picture> picture_dependencies;

	for (const auto &dep : dependencies)
		if (!LoadPicture(picture_dependencies[dep], FullInputPath(dep).c_str())) {
			error(format("Failed to load '%1' to construct '%2'").arg(dep).arg(path));
			return false;
		}

	// determine output resolution
	uint16_t out_width = 0, out_height = 0;
	for (const auto &i : picture_dependencies) {
		out_width = Max(out_width, i.second.GetWidth());
		out_height = Max(out_height, i.second.GetHeight());
	}

	// conform dependencies
	for (auto i : picture_dependencies)
		if (i.second.GetWidth() != out_width && i.second.GetHeight() != out_height) {
			// resize
			picture_dependencies[i.first] = hg::Resize(i.second, out_width, out_height);
		}

	//
	Picture out_pic;

	if (!has_construct_directive)
		out_pic = picture_dependencies[path]; // initialize with input if not constructed

	for (const auto &i_directive : i_preprocess_texture.items())
		if (i_directive.key() == "construct") {
			out_pic = Picture(out_width, out_height, i_directive.value().size() == 3 ? PF_RGB24 : PF_RGBA32);

			const auto out_stride = size_of(out_pic.GetFormat());

			size_t channel_count = 0;
			for (const auto &v : i_directive.value()) {
				auto out = out_pic.GetData() + channel_count;

				if (v.is_number_integer()) { // constant
					const auto constant = v.get<int>();

					for (uint16_t x = 0; x < out_width; ++x)
						for (uint16_t y = 0; y < out_height; ++y) {
							*out = constant;
							out += out_stride;
						}
				} else if (v.is_string()) { // channel or texture path
					const auto channel_v = v.get<std::string>();

					uint8_t *in = nullptr;
					size_t in_stride = 0;

					if (channel_v == "R") {
						auto &in_pic = picture_dependencies[path];

						if (GetChannelCount(in_pic.GetFormat()) < 1) {
							error(format("Cannot construct '%1' due to missing R channel").arg(path));
							return false;
						}

						in = in_pic.GetData() + 0;
						in_stride = size_of(in_pic.GetFormat());
					} else if (channel_v == "G") {
						auto &in_pic = picture_dependencies[path];

						if (GetChannelCount(in_pic.GetFormat()) < 2) {
							error(format("Cannot construct '%1' due to missing G channel").arg(path));
							return false;
						}

						in = in_pic.GetData() + 1;
						in_stride = size_of(in_pic.GetFormat());
					} else if (channel_v == "B") {
						auto &in_pic = picture_dependencies[path];

						if (GetChannelCount(in_pic.GetFormat()) < 3) {
							error(format("Cannot construct '%1' due to missing B channel").arg(path));
							return false;
						}

						in = in_pic.GetData() + 2;
						in_stride = size_of(in_pic.GetFormat());
					} else if (channel_v == "A") {
						auto &in_pic = picture_dependencies[path];

						if (GetChannelCount(in_pic.GetFormat()) < 4) {
							error(format("Cannot construct '%1' due to missing A channel").arg(path));
							return false;
						}

						in = in_pic.GetData() + 3;
						in_stride = size_of(in_pic.GetFormat());
					} else {
						auto &in_pic = picture_dependencies[channel_v];

						if (GetChannelCount(in_pic.GetFormat()) <= channel_count) {
							const char *channel_names[] = {"R", "G", "B", "A"};
							error(format("Cannot construct '%1' due to '%2' missing %3 channel").arg(path).arg(channel_v).arg(channel_names[channel_count]));
							return false;
						}

						in = in_pic.GetData() + channel_count;
						in_stride = size_of(in_pic.GetFormat());
					}

					for (uint16_t y = 0; y < out_height; ++y)
						for (uint16_t x = 0; x < out_width; ++x) {
							*out = *in;
							in += in_stride;
							out += out_stride;
						}

				} else if (v.is_object()) { // channel and texture path

					const auto dep_path = v["path"].get<std::string>();
					auto &in_pic = picture_dependencies[dep_path];

					const char *channel_names[] = {"R", "G", "B", "A"};
					const auto input_channel_v = v["channel"].get<std::string>();
					size_t input_channel = 5;
					for (size_t c = 0; c < 4; ++c)
						if (input_channel_v == channel_names[c]) {
							input_channel = c;
							break;
						}

					if (GetChannelCount(in_pic.GetFormat()) <= input_channel) {
						error(format("Cannot construct '%1' due to '%2' missing %3 channel").arg(path).arg(dep_path).arg(input_channel));
						return false;
					}

					uint8_t *in = in_pic.GetData() + input_channel;
					size_t in_stride = size_of(in_pic.GetFormat());

					for (uint16_t y = 0; y < out_height; ++y)
						for (uint16_t x = 0; x < out_width; ++x) {
							*out = *in;
							in += in_stride;
							out += out_stride;
						}
				}
				++channel_count;
			}

			if (!SaveTGA(out_pic, FullInputPath(output_path).c_str())) {
				error(format("Failed to save constructed picture as '%1'").arg(output_path));
				return false;
			}
		}

	path = output_path;
	return true;
}

void Texture(std::map<std::string, Hash> &hashes, std::string path) {
	ProfilerPerfSection perf("Command/Texture");

	log(format("  Texture '%1'").arg(path));

	std::string in_path = path; // may be changed by a construct directive

	//
	const auto meta_db = LoadMeta(path);

	std::string type = "Standard";
	GetMetaValue(meta_db, "type", type, profile);
	int max_size = 16384;
	GetMetaValue(meta_db, "max-size", max_size, profile);
	std::string compression = "RAW";
	GetMetaValue(meta_db, "compression", compression, profile);
	bool generate_mips = true;
	GetMetaValue(meta_db, "generate-mips", generate_mips, profile);
	bool generate_probe = false;
	GetMetaValue(meta_db, "generate-probe", generate_probe, profile);
	int max_probe_size = 512;
	GetMetaValue(meta_db, "max-probe-size", max_probe_size, profile);
	bool radiance_edge_fixup = false;
	GetMetaValue(meta_db, "radiance-edge-fixup", radiance_edge_fixup, profile);

	// preprocessing
	const json *i_preprocess;
	if (GetMetaTag(meta_db, "preprocess", i_preprocess, profile)) {
		if (PreprocessTexture(*i_preprocess, hashes, in_path)) {
			;
		} else {
			const json json_err = {{"type", "FailedTexturePreprocessing"}};
			log_error(json_err);
		}
	}

	//
	if (api == "DX12" || api == "DX11" || api == "GL" || api == "GLES" || api == "VK" || api == "MTL" ) {
		const auto src = FullInputPath(in_path);

		if (type == "Copy") {
			Copy(hashes, in_path);
		} else if (type != "Ignore") {
			const auto dst = FullOutputPath(path);

			Data build_ctx;
			Write(build_ctx, max_size);
			Write(build_ctx, generate_mips);
			Write(build_ctx, compression);

			const auto texconv_fmt = Get_texconv_Format(compression);
			const auto use_texconv = !toolchain.texconv.empty() && !texconv_fmt.empty();

			if (use_texconv)
				Write(build_ctx, "texconv");

			if (toolchain.texturec.empty()) {
				warn("    Skipping, no compiler found for texture resource");
			} else {
				if (NeedsCompilation(hashes, {in_path}, {path}, build_ctx)) {
					MkOutputTree(path);
					CleanOutputs({path});

					std::string cmd;

					if (use_texconv) { // favor the much faster texconv over texturec
						cmd = format("%1 -y -nologo -maxsize %2 -f %3 -bc x").arg(toolchain.texconv).arg(max_size).arg(texconv_fmt).str();
						if (!generate_mips)
							cmd += " -m 1";
						cmd += format(" \"%1\" \"%2\"").arg(src).arg(dst).str();
					}

					if (cmd.empty()) { // fallback to texturec
						cmd = format("%1 -f \"%2\" -o \"%3\" --as dds --max %4").arg(toolchain.texturec).arg(src).arg(dst).arg(max_size).str();

						if (compression != "RAW")
							cmd += format(" -t %1").arg(compression).str();
						else if (assetc::api == "DX11")
							cmd += " -t BGRA8"; // prevent useless swizzle at runtime
						else
							cmd += " -t RGBA8"; // prevent useless swizzle at runtime

						if (generate_mips)
							cmd += " -m";
					}

					PushAsyncProcessTask(path + " (Texture)", cmd, cwd);
				} else {
					debug("    [O] Texture up to date");
				}
			}
		}

		if (generate_probe) {
			const auto dst = FullOutputPath(path + ".radiance");

			Data build_ctx;
			Write(build_ctx, max_probe_size);
			Write(build_ctx, radiance_edge_fixup);
			Write(build_ctx, 20); // gloss scale
			Write(build_ctx, 0); // gloss bias

			if (toolchain.cmft.empty()) {
				warn("    Skipping, no compiler found for radiance probe resource");
			} else {
				if (NeedsCompilation(hashes, {in_path}, {path + ".radiance"}, build_ctx)) {
					MkOutputTree(path + ".radiance");
					CleanOutputs({path + ".radiance"});

					const auto cmd = format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL true --filter radiance "
											"--srcFaceSize %4 --edgeFixup %5 --glossScale 20 --glossBias 0")
										 .arg(toolchain.cmft)
										 .arg(src)
										 .arg(dst)
										 .arg(max_probe_size)
										 .arg(radiance_edge_fixup ? "warp" : "none")
										 .str();

					PushAsyncProcessTask(path + " (Radiance Probe)", cmd, cwd);
				} else {
					debug("    [O] Texture radiance up to date");
				}
			}
		}

		if (generate_probe) {
			const auto dst = FullOutputPath(path + ".irradiance");

			Data build_ctx;
			Write(build_ctx, max_probe_size);

			if (toolchain.cmft.empty()) {
				warn("    Skipping, no compiler found for irradiance probe resource");
			} else {
				if (NeedsCompilation(hashes, {in_path}, {path + ".irradiance"}, build_ctx)) {
					MkOutputTree(path + ".irradiance");
					CleanOutputs({path + ".irradiance"});

					const auto cmd =
						format("%1 --input \"%2\" --output0 \"%3\" --output0params dds,rgba16f,cubemap --useOpenCL false --filter irradiance --srcFaceSize %4")
							.arg(toolchain.cmft)
							.arg(src)
							.arg(dst)
							.arg(max_probe_size)
							.str();

					PushAsyncProcessTask(path + " (Irradiance Probe)", cmd, cwd);
				} else {
					debug("    [O] Texture irradiance up to date");
				}
			}
		}

	} else {
		const json json_err = {{"type", "UnsupportedTextureAPI"}, {"api", api}};
		log_error(json_err);
	}
}

//
static void ProcessGeometry(const std::string &src, const std::string &dst, ModelOptimisationLevel optimisation_level) {
	const auto geo = LoadGeometryFromFile(src.c_str());

	if (!Validate(geo)) {
		const json json_err = {{"type", "InvalidGeometry"}, {"dst", dst}};
		log_error(json_err);
	} else {
		if (!SaveGeometryModelToFile(dst.c_str(), geo, optimisation_level)) {
			const json json_err = {{"type", "FailedToSaveModel"}, {"dst", dst}};
			log_error(json_err);
		}
	}
}

void Geometry(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/Geometry");

	log(format("  Geometry '%1'").arg(path));

	const auto meta_db = LoadMeta(path);

	bool cook_model = true;
	GetMetaValue(meta_db, "cook-model", cook_model, profile);

	ModelOptimisationLevel optimisation_level = MOL_Full;

	Data build_ctx;
	Write(build_ctx, std::string(get_version_string()));
	Write(build_ctx, GetModelBinaryFormatVersion());
	Write(build_ctx, cook_model);
	Write(build_ctx, optimisation_level);

	if (NeedsCompilation(hashes, {path}, {path}, build_ctx)) {
		const auto src = FullInputPath(path), dst = FullOutputPath(path);

		MkOutputTree(path);
		CleanOutputs({path});

		task_queue.emplace_back(task{path, [=]() { return std::async(std::launch::async, ProcessGeometry, src, dst, optimisation_level); }});
	} else {
		debug("    [O] Geometry up to date");
	}
}

//
void GatherShaderDependencies(const std::string &path, std::set<std::string> &deps, const std::string &base) {
	const auto rel_path = base + path;

	if (deps.find(rel_path) != std::end(deps))
		return; // processed already, don't need no cyclic references

	deps.insert(rel_path);
	const auto src = hg::FileToString(FullInputPath(rel_path).c_str());

	static std::regex include_regex("#include\\s*(?:\"|<)(.+)(?:\"|>)");

	const auto base_path = hg::GetFilePath(rel_path); // #include directives are relative to the current source
	for (std::sregex_iterator i = std::sregex_iterator(src.begin(), src.end(), include_regex); i != std::sregex_iterator(); ++i)
		GatherShaderDependencies((*i)[1], deps, base_path); // recurse down the dependency tree
}

void GatherShaderDependencies(const std::set<std::string> &paths, std::set<std::string> &deps) {
	for (const auto &path : paths)
		GatherShaderDependencies(path, deps, "");
}

//
static void BuildComputeShader(std::map<std::string, Hash> &hashes, const std::string &cs_path, const std::string &defines) {
	ProfilerPerfSection perf("Command/ComputeShader");

	std::string cs_profile;
	if (api == "DX11" || api == "DX12") {
		cs_profile = "cs_5_0";
	} else if (api == "GL") {
		cs_profile = "130";
	} else if (api == "GLES") {
		// no profile => essl
	} else if (api == "VK") {
		cs_profile = "spirv";
	} else if (api == "MTL") {
		cs_profile = "metal";
	} else {
		const json json_err = {{"type", "UnsupportedComputeAPI"}, {"api", api}};
		log_error(json_err);
		return;
	}

	const std::string optim_flags = compile_with_debug_info ? "-O 0 --debug" : "-O 3";

	//
	const auto vs_defines = std::string("IS_COMPUTE_SHADER=1;") + defines;

	Data cs_build_ctx;
	Write(cs_build_ctx, cs_profile);
	Write(cs_build_ctx, optim_flags);
	Write(cs_build_ctx, vs_defines);

	if (toolchain.shaderc.empty()) {
		warn("    Skipping, no compiler found for compute resource");
	} else {
		std::set<std::string> inputs;
		GatherShaderDependencies({cs_path}, inputs);

		if (NeedsCompilation(hashes, inputs, {cs_path}, cs_build_ctx)) {
			if (!cs_profile.empty()) // GLES profile must be empty...
				cs_profile = "-p " + cs_profile;

			const auto cs_out = FullOutputPath(cs_path);

			MkOutputTree(cs_path);
			CleanOutputs({cs_path});

			const auto cs_cmd = format("%1 -f \"%2\" -o \"%3\" --platform %4 %5 %6 --type compute --define \"%7\"")
									.arg(toolchain.shaderc)
									.arg(FullInputPath(cs_path))
									.arg(cs_out)
									.arg(assetc::platform)
									.arg(cs_profile)
									.arg(optim_flags)
									.arg(vs_defines);

			PushAsyncProcessTask(cs_path + " (Compute Shader)", cs_cmd, cwd);
		} else {
			debug("    [O] Compute shader up to date");
		}
	}
}

static void ComputeShader(std::map<std::string, Hash> &hashes, const std::string &cs_path) {
	log(format("  Compute Shader '%1'").arg(cs_path));
	BuildComputeShader(hashes, cs_path, global_shader_defines);
}

//
static void BuildShader(std::map<std::string, Hash> &hashes, const std::string &name, const std::string &vs_path, const std::string &fs_path,
	const std::string &varying_path, const std::string &defines) {
	ProfilerPerfSection perf("Command/Shader");

	std::string vs_profile, fs_profile;
	if (api == "DX11" || api == "DX12") {
		vs_profile = "vs_5_0";
		fs_profile = "ps_5_0";
	} else if (api == "GL") {
		vs_profile = fs_profile = "130";
	} else if (api == "GLES") {
		// no profile => essl
	} else if (api == "VK") {
		vs_profile = fs_profile = "spirv";
	} else if (api == "MTL") {
		vs_profile = fs_profile = "metal";
	} else {
		const json json_err = {{"type", "UnsupportedShaderAPI"}, {"api", api}};
		log_error(json_err);
		return;
	}

	const std::string optim_flags = compile_with_debug_info ? "--debug" : "-O 3";

	//
	const auto vs_defines = std::string("IS_VERTEX_SHADER=1;") + defines;

	Data vs_build_ctx;
	Write(vs_build_ctx, vs_profile);
	Write(vs_build_ctx, optim_flags);
	Write(vs_build_ctx, vs_defines);

	const auto vs_name = format("%1.vsb").arg(name);

	if (toolchain.shaderc.empty()) {
		warn("    Skipping, no compiler found for shader resource");
	} else {
		std::set<std::string> inputs;
		GatherShaderDependencies({vs_path, varying_path}, inputs);

		if (NeedsCompilation(hashes, inputs, {vs_name}, vs_build_ctx)) {
			if (!vs_profile.empty()) // GLES profile must be empty...
				vs_profile = "-p " + vs_profile;

			const auto vs_out = FullOutputPath(vs_name);

			MkOutputTree(vs_name);
			CleanOutputs({vs_name});

			const auto vs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type v --platform %5 %6 %7 --define \"%8\"")
									.arg(toolchain.shaderc)
									.arg(FullInputPath(vs_path))
									.arg(vs_out)
									.arg(FullInputPath(varying_path))
									.arg(assetc::platform)
									.arg(vs_profile)
									.arg(optim_flags)
									.arg(vs_defines);

			PushAsyncProcessTask(name + " (Vertex Shader)", vs_cmd, cwd);
		} else {
			debug("    [O] Vertex shader up to date");
		}
	}

	//
	const auto fs_defines = std::string("IS_FRAGMENT_SHADER=1;") + defines;

	Data fs_build_ctx;
	Write(fs_build_ctx, fs_profile);
	Write(fs_build_ctx, optim_flags);
	Write(fs_build_ctx, fs_defines);

	const auto fs_name = format("%1.fsb").arg(name);

	if (toolchain.shaderc.empty()) {
		warn("    Skipping, no compiler found for shader resource");
	} else {
		std::set<std::string> inputs;
		GatherShaderDependencies({fs_path, varying_path}, inputs);

		if (NeedsCompilation(hashes, inputs, {fs_name}, fs_build_ctx)) {
			if (!fs_profile.empty())
				fs_profile = "-p " + fs_profile;

			const auto fs_out = FullOutputPath(fs_name);

			MkOutputTree(fs_name);
			CleanOutputs({fs_name});

			const auto fs_cmd = format("%1 -f \"%2\" -o \"%3\" --varyingdef \"%4\" --type f --platform %5 %6 %7 --define \"%8\"")
									.arg(toolchain.shaderc)
									.arg(FullInputPath(fs_path))
									.arg(fs_out)
									.arg(FullInputPath(varying_path))
									.arg(assetc::platform)
									.arg(fs_profile)
									.arg(optim_flags)
									.arg(fs_defines);

			PushAsyncProcessTask(name + " (Fragment Shader)", fs_cmd, cwd);
		} else {
			debug("    [O] Pixel shader up to date");
		}
	}
}

static void Shader(std::map<std::string, Hash> &hashes, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) {
	const auto name = slice(vs_path, 0, -6);
	log(format("  Shader '%1'").arg(name));
	BuildShader(hashes, name, vs_path, fs_path, varying_path, global_shader_defines);
}

//
static void BuildPipelineShaderVariant(std::map<std::string, Hash> &hashes, const std::string &name, const std::string &feats_path, const std::string &vs_path,
	const std::string &fs_path, const std::string &varying_path, const std::string &defines) {
	ProfilerPerfSection perf("Manage/BuildPipelineShaderVariant");

	size_t config = 0;
	for (auto &variant : pipeline.configs) {
		log(format("    Pipeline shader variant '%1' for pipeline config %2").arg(name).arg(config));
		const auto variant_name = format("%1_pipe-%2-cfg-%3").arg(name).arg(pipeline.name).arg(config++);
		const auto variant_defines = join(std::begin(variant), std::end(variant), ";") + ";" + defines;
		BuildShader(hashes, variant_name, vs_path, fs_path, varying_path, global_shader_defines + variant_defines);
	}
}

//
static bool IterateProgramFeatureStates(const std::vector<PipelineProgramFeature> &feats, std::vector<int> &states,
	const std::function<void(const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states)> &process_states, int i = 0) {
	if (states.empty()) {
		process_states(feats, states); // single program for no feature pipeline program
		return false;
	}

	if (i == states.size())
		return false;

	for (int j = 0; j < GetPipelineProgramFeatureStateCount(feats[i]); ++j) {
		states[i] = j;
		if (!IterateProgramFeatureStates(feats, states, process_states, i + 1))
			process_states(feats, states);
	}
	return true;
}

static std::string GetDefines(const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states) {
	std::vector<std::string> defines;

	bool has_ambient_uv_feature = false;

	for (size_t i = 0; i < states.size(); ++i) {
		const auto state = states[i];

		if (feats[i] == OptionalBaseColorOpacityMap) {
			defines.push_back(format("USE_BASE_COLOR_OPACITY_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalOcclusionRoughnessMetalnessMap) {
			defines.push_back(format("USE_OCCLUSION_ROUGHNESS_METALNESS_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalDiffuseMap) {
			defines.push_back(format("USE_DIFFUSE_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalSpecularMap) {
			defines.push_back(format("USE_SPECULAR_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalLightMap) {
			defines.push_back(format("USE_LIGHT_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalSelfMap) {
			defines.push_back(format("USE_SELF_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalOpacityMap) {
			defines.push_back(format("USE_OPACITY_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalAmbientMap) {
			defines.push_back(format("USE_AMBIENT_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalReflectionMap) {
			defines.push_back(format("USE_REFLECTION_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalNormalMap) {
			defines.push_back(format("USE_NORMAL_MAP=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == NormalMapInWorldSpace) {
			defines.push_back(format("NORMAL_MAP_IN_WORLD_SPACE=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == DiffuseUV1) {
			defines.push_back(format("DIFFUSE_UV_CHANNEL=%1").arg(state));
		} else if (feats[i] == SpecularUV1) {
			defines.push_back(format("SPECULAR_UV_CHANNEL=%1").arg(state));
		} else if (feats[i] == AmbientUV1) {
			defines.push_back(format("AMBIENT_UV_CHANNEL=%1").arg(state));
			has_ambient_uv_feature = true;
		} else if (feats[i] == OptionalSkinning) {
			defines.push_back(format("ENABLE_SKINNING=%1").arg(state ? "1" : "0"));
		} else if (feats[i] == OptionalAlphaCut) {
			defines.push_back(format("ENABLE_ALPHA_CUT=%1").arg(state ? "1" : "0"));
		}
	}

	if (!has_ambient_uv_feature)
		defines.push_back("AMBIENT_UV_CHANNEL=1"); // [EJ02042022] default to legacy behavior

	return join(std::begin(defines), std::end(defines), ";");
}

//
static void PipelineShader(
	std::map<std::string, Hash> &hashes, const std::string &hps_path, const std::string &vs_path, const std::string &fs_path, const std::string &varying_path) {
	ProfilerPerfSection perf("Command/PipelineShader");

	log(format("  Pipeline shader '%1' for pipeline '%2'").arg(hps_path).arg(pipeline.name));

	Copy(hashes, hps_path);

	const auto name = CutFileExtension(hps_path);
	bool success;
	const auto feats = LoadPipelineProgramFeaturesFromFile(PathJoin({input_dir, hps_path}).c_str(), success);
	if (!success) {
		const json json_err = {{"type", "FailedToLoadPipelineProgramFeatures"}, {"path", hps_path}};
		log_error(json_err);
	}

	std::vector<int> states(feats.size(), 0);
	IterateProgramFeatureStates(feats, states, [&](const std::vector<PipelineProgramFeature> &feats, const std::vector<int> &states) {
		const auto variant_defines = GetDefines(feats, states);
		const auto variant_name = GetPipelineProgramVariantName(name.c_str(), feats, states);
		BuildPipelineShaderVariant(hashes, variant_name, hps_path, vs_path, fs_path, varying_path, variant_defines);
	});
}

//
static void LuaScript(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/LuaScript");

	log(format("  Lua script '%1'").arg(path));

	if (toolchain.luac.empty()) {
		warn("    Skipping, no compiler found for Lua script resource");
	} else {
		if (NeedsCompilation(hashes, {path}, {path}, {})) {
			const auto src = FullInputPath(path), dst = FullOutputPath(path);

			MkOutputTree(path);
			CleanOutputs({path});

			const auto cmd = format("%1 -o \"%3\" -s \"%2\"").arg(toolchain.luac).arg(src).arg(dst);
			PushAsyncProcessTask(path, cmd, cwd);
		} else {
			debug("  [O] Lua script up to date");
		}
	}
}

//
static void Physics(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/Physics");

	log(format("  Physics resource '%1'").arg(path));

	if (toolchain.bulletc.empty()) {
		debug("    Skipping, no compiler found for physics bullet resource");
	} else {
		if (NeedsCompilation(hashes, {path}, {path}, {})) {
			const auto cwd = GetCurrentWorkingDirectory();
			const auto src = FullInputPath(path), dst = FullOutputPath(path);

			if (!CopyFile(src.c_str(), dst.c_str())) {
				ReportFailedInput(src);
				const json json_err = {{"type", "FailedToCopyInput"}, {"src", src}, {"dst", dst}};
				log_error(json_err);
			}

			const auto cmd = format("\"%1\" \"%2\" \"%3_bullet\" -root \"%4\"").arg(toolchain.bulletc).arg(src).arg(dst).arg(input_dir);
			PushAsyncProcessTask(path, cmd, cwd);
		} else {
			debug("  [O] Physics resource up to date");
		}
	}

}

static void PathFinding(std::map<std::string, Hash> &hashes, const std::string &path) {
	ProfilerPerfSection perf("Command/PathFinding");

	log(format("  Pathfinding resource '%1'").arg(path));

	if (toolchain.recastc.empty()) {
		debug("    Skipping, no compiler found for pathfinding resource");
	} else {
		if (NeedsCompilation(hashes, {path}, {path}, {})) {
			const auto src = FullInputPath(path), dst = FullOutputPath(path);

			MkOutputTree(path);
			CleanOutputs({path});

			const auto cmd = format("%1 \"%2\" \"%3\" -root \"%4\"").arg(toolchain.recastc).arg(src).arg(dst).arg(input_dir);
			PushAsyncProcessTask(path, cmd, GetCurrentWorkingDirectory());
		} else {
			debug("  [O] Pathfinding resource up to date");
		}
	}
}

//
enum class AssetType { Unprocessed, Ignore, Broken, Scene, Texture, Geometry, Lua, Shader, PipelineShader, Physics, PathFinding, ComputeShader, Count };

#if 0
// CWE 561: The function 'AssetTypeToString' is never used.
static const std::string& AssetTypeToString(AssetType type) {
	static const std::string types[static_cast<int>(AssetType::Count) + 1] = {
		"Unprocessed", "Ignore", "Broken", "Scene", "Texture", "Geometry", "Lua", "Shader", "PipelineShader", "Physics", "PathFinding", "ComputeShader", "Undefined"};
	return types[int(type)];
}
#endif

struct AssetConfig {
	AssetType type;
};

static AssetType GetAssetFileType(std::string path, const std::vector<std::string> &all_files, std::vector<std::string> &out_files) {
	static const std::set<std::string> texture_exts = {"bmp", "exr", "gif", "jpg", "jpeg", "hdr", "png", "psd", "tga"};
	static const std::set<std::string> ignored_exts = {"tmp"};

	const auto ext = tolower(GetFileExtension(path));
	if (ext == "scn") {
		out_files.push_back(path);
		return AssetType::Scene;
	} else if (texture_exts.find(ext) != std::end(texture_exts)) {
		out_files.push_back(path);
		return AssetType::Texture;
	} else if (ignored_exts.find(ext) != std::end(ignored_exts)) {
		return AssetType::Ignore;
	} else if (ext == "geo") {
		out_files.push_back(path);
		return AssetType::Geometry;
	} else if (ext == "lua") {
		out_files.push_back(path);
		return AssetType::Lua;
	} else if (ext == "sc") {
		std::string name;
		if (ends_with(path, "_vs.sc") || ends_with(path, "_fs.sc"))
			name = slice(path, 0, -6);
		else if (ends_with(path, "_varying.def"))
			name = slice(path, 0, -11);
		else
			name = slice(path, 0, -3);

		const auto hps_name = name + ".hps", vs_name = name + "_vs.sc", fs_name = name + "_fs.sc", varying_name = name + "_varying.def";

		const auto hps_i = std::find(std::begin(all_files), std::end(all_files), hps_name);
		const auto vs_i = std::find(std::begin(all_files), std::end(all_files), vs_name);
		const auto fs_i = std::find(std::begin(all_files), std::end(all_files), fs_name);
		const auto varying_i = std::find(std::begin(all_files), std::end(all_files), varying_name);

		const bool has_hps = hps_i != std::end(all_files), has_vs = vs_i != std::end(all_files), has_fs = fs_i != std::end(all_files),
				   has_varying = varying_i != std::end(all_files);

		if (has_hps) { // that's a pipeline shader
			if (has_vs && has_fs && has_varying) {
				out_files.push_back(hps_name);
				out_files.push_back(vs_name);
				out_files.push_back(fs_name);
				out_files.push_back(varying_name);
				return AssetType::PipelineShader;
			}
			out_files.push_back(hps_name);
		} else {
			if (has_vs && has_fs && has_varying) {
				out_files.push_back(vs_name);
				out_files.push_back(fs_name);
				out_files.push_back(varying_name);
				return AssetType::Shader;
			} else {
				out_files.push_back(path);
				return AssetType::ComputeShader;
			}
		}

		if (has_vs)
			out_files.push_back(vs_name);
		if (has_fs)
			out_files.push_back(fs_name);
		if (has_varying)
			out_files.push_back(varying_name);

		return AssetType::Broken;
	} else if (ext == "hps") {
		const auto name = slice(path, 0, -4);
		const auto vs_name = name + "_vs.sc", fs_name = name + "_fs.sc", varying_name = name + "_varying.def";

		const auto vs_i = std::find(std::begin(all_files), std::end(all_files), vs_name);
		const auto fs_i = std::find(std::begin(all_files), std::end(all_files), fs_name);
		const auto varying_i = std::find(std::begin(all_files), std::end(all_files), varying_name);

		const bool has_vs = vs_i != std::end(all_files), has_fs = fs_i != std::end(all_files), has_varying = varying_i != std::end(all_files);

		out_files.push_back(path);

		if (has_vs)
			out_files.push_back(vs_name);
		if (has_fs)
			out_files.push_back(fs_name);
		if (has_varying)
			out_files.push_back(varying_name);

		if (has_vs && has_fs && has_varying)
			return AssetType::PipelineShader;

		return AssetType::Broken;
	} else if (ext == "physics") {
		out_files.push_back(path);
		return AssetType::Physics;
	} else if (ext == "pathfinding") {
		out_files.push_back(path);
		return AssetType::PathFinding;
	}

	out_files.push_back(path);
	return AssetType::Unprocessed;
}

//
static std::vector<std::string> DirEntriesToPaths(const std::vector<DirEntry> &entries) {
	std::vector<std::string> paths;
	std::transform(std::begin(entries), std::end(entries), std::back_inserter(paths), [](const DirEntry &e) { return e.name; });
	return paths;
}

static std::vector<std::string> blacklist = {"_meta", "_prod", ".git", ".svn"};

static std::map<std::vector<std::string>, AssetConfig> ClassifyInputs(
	const std::vector<std::string> &all_files, const std::vector<std::string> &files_to_classify) {
	ProfilerPerfSection perf("Manage/ClassifyInputs");

	std::map<std::vector<std::string>, AssetConfig> asset_configs;

	size_t i = 0;
	const size_t file_count = files_to_classify.size();

	for (auto &path : files_to_classify) {
		if (progress)
			std::cout << "-> Progress: " << i * classify_progress_weight / file_count << "% (" << path << ")" << std::endl;
		++i;

		bool is_blacklisted = false;
		for (auto &prefix : blacklist)
			if (starts_with(path, prefix)) {
				is_blacklisted = true;
				break;
			}

		if (is_blacklisted)
			continue;

		bool already_classified = false;
		for (auto &cfg : asset_configs)
			if (std::find(std::begin(cfg.first), std::end(cfg.first), path) != std::end(cfg.first)) {
				already_classified = true;
				break;
			}

		if (already_classified)
			continue; // file already classified

		std::vector<std::string> out_files;
		const auto type = GetAssetFileType(path, all_files, out_files);

		asset_configs[std::move(out_files)] = {type};
	}
	return asset_configs;
}

static std::vector<std::string> GetInputDirFiles() { return DirEntriesToPaths(ListDirRecursive(input_dir.c_str(), DE_File)); }

static std::map<std::vector<std::string>, AssetConfig> ClassifyInputDir() {
	const auto all_files = GetInputDirFiles();
	return ClassifyInputs(all_files, all_files);
}

//
static bool CompileClassifiedInputs(const std::map<std::vector<std::string>, AssetConfig> &inputs) {
	ProfilerPerfSection perf("Manage/CompileClassifiedInputs");

	const auto t_start = time_now();
	std::cout << "Compilation start" << std::endl;

	std::map<std::string, Hash> updated_hashes;

	const auto input_count = inputs.size();
	size_t j = 0;

	for (auto &i : inputs) {
		const auto &names = i.first;

		if (!names.empty()) {
			const auto &config = i.second;

			if (progress)
				std::cout << "-> Progress: " << j * compile_progress_weight / input_count + classify_progress_weight << "% (" << names[0] << ")" << std::endl;
			++j;

			if (config.type == AssetType::Unprocessed)
				Copy(updated_hashes, names[0]);
			else if (config.type == AssetType::Scene)
				Scene(updated_hashes, names[0]);
			else if (config.type == AssetType::Texture)
				Texture(updated_hashes, names[0]);
			else if (config.type == AssetType::Geometry)
				Geometry(updated_hashes, names[0]);
			else if (config.type == AssetType::Lua)
				LuaScript(updated_hashes, names[0]);
			else if (config.type == AssetType::ComputeShader)
				ComputeShader(updated_hashes, names[0]);
			else if (config.type == AssetType::Shader)
				Shader(updated_hashes, names[0], names[1], names[2]);
			else if (config.type == AssetType::PipelineShader)
				PipelineShader(updated_hashes, names[0], names[1], names[2], names[3]);
			else if (config.type == AssetType::Physics)
				Physics(updated_hashes, names[0]);
			else if (config.type == AssetType::PathFinding)
				PathFinding(updated_hashes, names[0]);
			else if (config.type == AssetType::Broken)
				((void)0);
		}

		if (!IsPollProcessUp()) {
			std::cout << "Compilation aborted after " << time_to_ms(time_now() - t_start) << "ms, poll process down" << std::endl;
			return false;
		}
	}

	for (const auto &h : updated_hashes)
		compilation_db.source_hashes[h.first] = h.second; // commit updated hashes

	assetc::RunTaskQueue();
	assetc::SaveCompilationDB();

	std::cout << "Compilation done, took " << time_to_ms(time_now() - t_start) << "ms" << std::endl;
	return true;
}

//
static bool clean_outputs_for_removed_inputs = true;

static void CleanOutputsForRemovedInputs() {
	ProfilerPerfSection perf("Manage/CleanOutputsForRemovedInputs");

	std::map<std::string, std::set<std::string>> db_input_to_outputs;
	for (const auto &i : compilation_db.output_to_inputs)
		for (const auto &input : i.second)
			db_input_to_outputs[input].insert(i.first);

	const auto input_files = GetInputDirFiles(); // available inputs

	size_t removed = 0;
	for (const auto &i : db_input_to_outputs)
		if (std::find(std::begin(input_files), std::end(input_files), i.first) == std::end(input_files)) {
			for (const auto &output : i.second) // DB input is missing from input files, remove its associated outputs
				if (Unlink(FullOutputPath(output).c_str()))
					++removed;
		}

	std::cout << "Removed " << removed << " outputs due to missing input" << std::endl;
}

//
static void DaemonMode() {
	log("Entering daemon mode, press Ctrl+C to close\n");

	WatchDirectory(input_dir, true);

	while (true) {
		std::this_thread::sleep_for(std::chrono::seconds(1));

		const auto events = GetDirectoryWatchEvents(input_dir);

		std::set<std::string> modified_files;
		for (const auto &ev : events)
			if ((ev.type == WatchEvent::FileAdded) || (ev.type == WatchEvent::FileModified))
				if (IsFile(PathJoin({input_dir, ev.path}).c_str()))
					modified_files.insert(ev.path);

		// change in .meta must trickle down to the file it refers to
		std::set<std::string> modified_files_;
		for (const auto &i : modified_files) {
			modified_files_.insert(i);
			if (ends_with(i, ".meta", insensitive))
				modified_files_.insert(left(i, -5));
		}

		if (!modified_files_.empty()) {
			log(format("File system changes detected (%1):").arg(modified_files_.size()));
			for (const auto &f : modified_files_)
				log((std::string("    - ") + f).c_str());

			if (assetc::clean_outputs_for_removed_inputs)
				assetc::CleanOutputsForRemovedInputs();

			if (!CompileClassifiedInputs(ClassifyInputs(GetInputDirFiles(), {std::begin(modified_files_), std::end(modified_files_)})))
				break;
			log("Press Ctrl+C to close\n");
		}
	}

	UnwatchDirectory(input_dir);
}

//
static void OutputPerfReport() {
	const auto profile = EndProfilerFrame();

	time_ns total = 0;
	for (auto &task : profile.tasks)
		if (task.name == "Total")
			total = task.duration;

	if (total) {
		std::cout << std::endl << "Performance report" << std::endl;
		for (auto &task : profile.tasks)
			std::cout << "  - " << task.name << ": "
					  << "(" << (task.duration * 100 / total) << "%) " << FormatTime(task.duration) << " (" << task.section_indexes.size() << " call)"
					  << std::endl;

		std::cout << std::endl;
	}
}

} // namespace assetc

//
static void OutputUsage(const CmdLineFormat &cmd_format) {
	std::cout << "Usage: assetc " << word_wrap(FormatCmdLineArgs(cmd_format), 120, 14) << std::endl << std::endl;
	std::cout << FormatCmdLineArgsDescription(cmd_format);
}

#if WIN32
int wmain(int narg, wchar_t **argv) {
	std::vector<std::string> args_utf8(narg + 1);
	for (int i = 0; i < narg; ++i)
		args_utf8[i] = hg::wchar_to_utf8(argv[i]);

	std::vector<const char *> _args(narg + 1);
	for (int i = 0; i < narg; ++i)
		_args[i] = args_utf8[i].c_str();

	const char **args = _args.data();
#else
int main(int narg, const char **args) {
#endif
	const auto exe_path = GetFilePath(args[0]);

	std::cout << "Harfang ASSETC 1.1" << std::endl;
	assetc::cwd = GetCurrentWorkingDirectory();

	const auto default_toolchain_path = format("toolchains/host-%1-target-%2").arg(get_host_string()).arg(get_target_string()).str();

	// parse command line
	CmdLineFormat cmd_format = {
		{
			{"-daemon", "Run in the background and watch for modifications to the input folder"},
			{"-progress", "Output progress to the standard C output stream"},
			{"-log_errors_to_stderr", "Log errors as JSON to the standard C error output stream"},
			{"-debug", "Compile in debug mode (eg. output debug informations in shader)"},
			{"-quiet", "Disable all build information but errors"},
			{"-verbose", "Output additional information about the compilation process"},
			{"-fast_check", "Perform modification detection using input file timestamp"},
			{"-no_clean_removed_inputs", "Do not remove outputs for removed input files"},
		},
		{
			{"-job", "Maximum number of parallel job (0 - automatic)", true},
			{"-toolchain", format("Path to the toolchain folder (default: %1)").arg(default_toolchain_path), true},
			{"-platform", "Select the target platform to compile for (defaults to current platform)", true},
			{"-api", "Select the platform graphic API to compile for", true},
			{"-defines", "Semicolon separated defines to pass to shaderc (eg. FLAG;VALUE=2)", true},
			{"-poll_pid", "Poll the provided process and exit assetc if down", true},
		},
		{
			{"input", "Input folder to compile sources from"},
			{"output", "Output folder for compiled assets", true},
		},
		{
			{"-d", "-daemon"},
			{"-l", "-log_errors_to_stderr"},
			{"-j", "-job"},
			{"-t", "-toolchain"},
			{"-p", "-platform"},
			{"-q", "-quiet"},
			{"-v", "-verbose"},
			{"-D", "-defines"},
			{"-f", "-fast_check"},
			{"-n", "-no_clean_removed_inputs"},
		},
	};

	CmdLineContent cmd_content;
	if (!ParseCmdLine({args + 1, args + narg}, cmd_format, cmd_content)) {
		OutputUsage(cmd_format);
		return -1;
	}

	// toolchain
	const auto toolchain_path = GetCmdLineSingleValue(cmd_content, "-toolchain", PathJoin({exe_path, default_toolchain_path}));

	assetc::SetToolchain(toolchain_path);
	assetc::ToolchainExists();

	/*
	if (!assetc::ToolchainExists()) {
		std::cout << "Error: Toolchain not found " << toolchain_path << std::endl;
		OutputUsage(cmd_format);
		return -4;
	}
	*/

	assetc::api = GetCmdLineSingleValue(cmd_content, "-api", assetc::api);
	assetc::platform = GetCmdLineSingleValue(cmd_content, "-platform", assetc::platform);
	assetc::global_shader_defines = GetCmdLineSingleValue(cmd_content, "-defines", "");

	if (!assetc::global_shader_defines.empty() && !ends_with(assetc::global_shader_defines, ";"))
		assetc::global_shader_defines += ";";

	assetc::max_async_jobs = GetCmdLineSingleValue(cmd_content, "-job", 0);
	if (assetc::max_async_jobs < 1)
		assetc::max_async_jobs = std::thread::hardware_concurrency();

	assetc::progress = GetCmdLineFlagValue(cmd_content, "-progress");
	assetc::log_errors_to_stderr = GetCmdLineFlagValue(cmd_content, "-log_errors_to_stderr");
	assetc::compile_with_debug_info = GetCmdLineFlagValue(cmd_content, "-debug");

	assetc::fast_check = GetCmdLineFlagValue(cmd_content, "-fast_check");

	assetc::poll_process_id = GetCmdLineSingleValue(cmd_content, "-poll_pid", 0);
	assetc::clean_outputs_for_removed_inputs = !GetCmdLineFlagValue(cmd_content, "-no_clean_removed_inputs");

	assetc::SetRenderPipeline(GetForwardPipelineInfo());

	//
	int log_level = LL_Normal | LL_Warning | LL_Error;

	if (GetCmdLineFlagValue(cmd_content, "-quiet"))
		log_level = LL_Warning | LL_Error;

	if (GetCmdLineFlagValue(cmd_content, "-verbose"))
		log_level = LL_All; // verbose logs all

	set_log_level(log_level);

	// input folder compilation mode
	if (cmd_content.positionals.empty()) {
		std::cout << "Error: No input folder specified" << std::endl;
		OutputUsage(cmd_format);
		return -2;
	}

	assetc::SetInputDir(cmd_content.positionals[0]);

	if (!IsDir(assetc::input_dir.c_str())) {
		std::cout << "Error: Input dir '" << assetc::input_dir << "' not found" << std::endl;
		OutputUsage(cmd_format);
		return -3;
	}

	if (cmd_content.positionals.size() > 1)
		assetc::SetOutputDir(cmd_content.positionals[1]);
	else
		assetc::SetOutputDir(assetc::input_dir + "_compiled");

	//
	auto exe_path_or_nothing = [](const std::string &path) { return path.empty() ? "-" : path; };

	log(format("> Input dir: %1").arg(assetc::input_dir));
	log(format("> Output dir: %1").arg(assetc::output_dir));
	log("");
	log(format("> Target platform: %1").arg(assetc::platform));
	log(format("> Target graphics API: %1").arg(assetc::api));
	log(format("> Target pipeline: %1").arg(assetc::pipeline.name));
	log("");
	log(format("> Using %1 parallel job").arg(assetc::max_async_jobs));
	log(format("> Toolchain compilers (%1):").arg(toolchain_path));
	log(format("  - Shader      %1").arg(exe_path_or_nothing(assetc::toolchain.shaderc)));
	log(format("  - Texture     %1").arg(exe_path_or_nothing(assetc::toolchain.texturec)));
	log(format("  - Probe       %1").arg(exe_path_or_nothing(assetc::toolchain.cmft)));
	log(format("  - Lua         %1").arg(exe_path_or_nothing(assetc::toolchain.luac)));
	log(format("  - Pathfinding %1").arg(exe_path_or_nothing(assetc::toolchain.recastc)));
	log("");

	// initial run over input directory (process all files)
	{
		ProfilerPerfSection perf("Total");

		assetc::LoadCompilationDB();

		if (assetc::clean_outputs_for_removed_inputs)
			assetc::CleanOutputsForRemovedInputs();

		if (assetc::CompileClassifiedInputs(assetc::ClassifyInputDir())) {
			// enter daemon mode, process input dir files as they change
			if (GetCmdLineFlagValue(cmd_content, "-daemon"))
				assetc::DaemonMode();
		}
	}

	//
	assetc::OutputPerfReport();

	std::cout << "Exit" << std::endl;
	return 0;
}
