#include <bun.h>

#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <regex>
#include <string>
#include <unordered_map>
#include <unordered_set>

#include "ggpk_vfs.h"
#include "path_rep.h"
#include "util.h"

#include <poe/util/utf.hpp>

using namespace std::string_view_literals;

static char const* const USAGE =
"bun_extract_file list-files GGPK_OR_STEAM_DIR\n"
"bun_extract_file extract-files [--regex] GGPK_OR_STEAM_DIR OUTPUT_DIR [FILE_PATHS...]\n\n"
"GGPK_OR_STEAM_DIR should be either a full path to a Standalone GGPK file or the Steam game directory.\n"
"If FILE_PATHS are omitted the file paths are taken from stdin.\n"
"If --regex is given, FILE_PATHS are interpreted as regular expressions to match.\n";

int main(int argc, char* argv[]) {
	std::error_code ec;
	if (argc < 2 || argv[1] == "--help"sv || argv[1] == "-h"sv) {
		fprintf(stderr, USAGE);
		return 1;
	}

	std::string command;
	std::filesystem::path ggpk_or_steam_dir;
	std::filesystem::path output_dir;
	bool use_regex = false;
	std::vector<std::string> tail_args;

	command = argv[1];

	int argi = 2;
	while (argi < argc) {
		if (argv[argi] == "--regex"sv) {
			use_regex = true;
			++argi;
		}
		else {
			break;
		}
	}

	if (command == "list-files"sv) {
		if (argi < argc) {
			ggpk_or_steam_dir = argv[argi++];
		}
		if (argi != argc) {
			fprintf(stderr, USAGE);
			return 1;
		}
	}
	else if (command == "extract-files"sv) {
		if (argi + 1 < argc) {
			ggpk_or_steam_dir = argv[argi++];
			output_dir = argv[argi++];
			while (argi < argc) {
				tail_args.push_back(argv[argi++]);
			}
		}
	}
	else {
		fprintf(stderr, USAGE);
		return 1;
	}

	std::shared_ptr<GgpkVfs> vfs;
	if (ggpk_or_steam_dir.extension() == ".ggpk") {
		vfs = open_ggpk(ggpk_or_steam_dir);
	}

#if _WIN32
	std::string ooz_dll = "libooz.dll";
#else
	std::string ooz_dll = "liblibooz.so";
#endif
	Bun* bun = BunNew(ooz_dll.c_str(), "Ooz_Decompress");
	if (!bun) {
		bun = BunNew(("./" + ooz_dll).c_str(), "Ooz_Decompress");
		if (!bun) {
			fprintf(stderr, "Could not initialize Bun library\n");
			return 1;
		}
	}

	BunIndex* idx = BunIndexOpen(bun, borrow_vfs(vfs), ggpk_or_steam_dir.string().c_str());
	if (!idx) {
		fprintf(stderr, "Could not open index\n");
		return 1;
	}

	if (command == "list-files") {
		auto rep_mem = BunIndexPathRepContents(idx);
		for (size_t path_rep_id = 0;; ++path_rep_id) {
			uint64_t hash;
			uint32_t offset;
			uint32_t size;
			uint32_t recursive_size;
			if (BunIndexPathRepInfo(idx, (int32_t)path_rep_id, &hash, &offset, &size, &recursive_size) < 0) {
				break;
			}
			auto generated = generate_paths(rep_mem + offset, size);
			for (auto& p : generated) {
				std::cout << p << std::endl;
			}
		}
		return 0;
	}

	std::vector<std::string> wanted_paths = tail_args;
	if (wanted_paths.empty()) {
		std::string line;
		while (std::getline(std::cin, line)) {
			wanted_paths.push_back(line);
		}
	}

	std::vector<std::regex> regexes;
	if (use_regex) {
		bool regexes_good = true;
		for (auto& path : wanted_paths) {
			try {
				regexes.push_back(std::regex(path));
			}
			catch (std::exception& e) {
				fprintf(stderr, "Could not compile regex \"%s\": %s\n", path.c_str(), e.what());
				regexes_good = false;
			}
		}
		if (!regexes_good) {
			return 1;
		}

		if (command == "extract-files") {
			std::unordered_set<std::string> matching_paths;
			auto rep_mem = BunIndexPathRepContents(idx);
			for (size_t path_rep_id = 0;; ++path_rep_id) {
				uint64_t hash;
				uint32_t offset;
				uint32_t size;
				uint32_t recursive_size;
				if (BunIndexPathRepInfo(idx, (int32_t)path_rep_id, &hash, &offset, &size, &recursive_size) < 0) {
					break;
				}
				auto generated = generate_paths(rep_mem + offset, size);
				for (auto& p : generated) {
					if (!matching_paths.count(p)) {
						for (auto& r : regexes) {
							if (std::regex_match(p, r)) {
								matching_paths.insert(p);
							}
						}
					}
				}
			}
			wanted_paths.assign(matching_paths.begin(), matching_paths.end());
		}
	}

	struct extract_info {
		std::string path;
		uint32_t offset;
		uint32_t size;
	};

	std::unordered_map<uint32_t, std::vector<extract_info>> bundle_parts_to_extract;
	for (auto path : wanted_paths) {
		if (path.front() == '"' && path.back() == '"') {
			path = path.substr(1, path.size() - 2);
		}

		int32_t file_id = BunIndexLookupFileByPath(idx, path.c_str());
		if (file_id < 0) {
			fprintf(stderr, "Could not find file \"%s\"\n", path.c_str());
		}

		uint32_t bundle_id;

		uint64_t path_hash;
		extract_info ei;
		ei.path = path;

		BunIndexFileInfo(idx, file_id, &path_hash, &bundle_id, &ei.offset, &ei.size);
		bundle_parts_to_extract[bundle_id].push_back(ei);
	}

	size_t extracted = 0;
	size_t missed = 0;
	for (auto& [bundle_id, parts] : bundle_parts_to_extract) {
		std::vector<uint8_t> bundle_data;
		auto bundle_mem = BunIndexExtractBundle(idx, bundle_id);
		if (!bundle_mem) {
			missed += parts.size();
			char const* name;
			uint32_t cb;
			BunIndexBundleInfo(idx, bundle_id, &name, &cb);
			fprintf(stderr, "Could not open bundle \"%s\", missing %zu files.\n", name, parts.size());
			continue;
		}
		for (auto& part : parts) {
			std::filesystem::path output_path = output_dir / part.path;
			std::filesystem::create_directories(output_path.parent_path(), ec);
			if (!dump_file(output_path, bundle_mem + part.offset, part.size)) {
				fprintf(stderr, "Could not write file \"%s\"\n", output_path.string().c_str());
				++missed;
				continue;
			}
			++extracted;
		}
		BunMemFree(bundle_mem);
	}
	fprintf(stderr, "Done, %zu/%zu extracted, %zu missed.\n", extracted, wanted_paths.size(), missed);
	BunIndexClose(idx);
	BunDelete(bun);

	return 0;
}
