/*
 * PackageTool - Command-line tool for creating and managing .package archives
 *
 * Usage:
 *   PackageTool create <package.package> <file1> [file2 ...]
 *   PackageTool add <package.package> <file1> [file2 ...]
 *   PackageTool remove <package.package> <file1> [file2 ...]
 *   PackageTool extract <package.package> <file1> [dest]
 *   PackageTool extract-all <package.package> [dest-dir]
 *   PackageTool list <package.package>
 *   PackageTool info <package.package>
 */
#include "OgrePackageArchive.h"
#include "OgreException.h"

#include <cstring>
#include <filesystem>
#include <iostream>
#include <vector>

namespace fs = std::filesystem;

static void printUsage(const char *prog)
{
	std::cerr << "Usage:" << std::endl;
	std::cerr << "  " << prog
		  << " create <package.package> <file1> [file2 ...]"
		  << std::endl;
	std::cerr << "  " << prog
		  << " add <package.package> <file1> [file2 ...]" << std::endl;
	std::cerr << "  " << prog
		  << " remove <package.package> <file1> [file2 ...]"
		  << std::endl;
	std::cerr << "  " << prog << " extract <package.package> <file1> [dest]"
		  << std::endl;
	std::cerr << "  " << prog << " extract-all <package.package> [dest-dir]"
		  << std::endl;
	std::cerr << "  " << prog << " list <package.package>" << std::endl;
	std::cerr << "  " << prog << " info <package.package>" << std::endl;
	std::cerr << std::endl;
	std::cerr << "Notes:" << std::endl;
	std::cerr << "  - For 'create' and 'add', if a source path is a "
		  << "directory, its contents are added recursively."
		  << std::endl;
	std::cerr << "  - Files are stored with their relative path when "
		  << "added from subdirectories." << std::endl;
}

/**
 * Check if a filename matches a simple glob pattern.
 * Supports '*' (matches any sequence of characters except '/').
 */
static bool matchesGlob(const std::string &filename, const std::string &pattern)
{
	size_t pi = 0, fi = 0;
	while (pi < pattern.size()) {
		if (pattern[pi] == '*') {
			// '*' matches any sequence of non-'/' characters
			pi++;
			if (pi == pattern.size())
				return true; // trailing '*' matches everything
			// Find the next character in filename that matches
			// pattern[pi]
			while (fi < filename.size() && filename[fi] != '/') {
				if (filename[fi] == pattern[pi])
					break;
				fi++;
			}
			if (fi >= filename.size() || filename[fi] == '/')
				return false;
		} else {
			if (fi >= filename.size() ||
			    filename[fi] != pattern[pi])
				return false;
			fi++;
			pi++;
		}
	}
	return fi == filename.size();
}

/**
 * Check if a relative path matches any of the exclude patterns.
 * Patterns can be:
 *   - Prefix match: "CMakeFiles" matches "CMakeFiles/foo" or "CMakeFiles"
 *   - Glob match:   "*.lua-" matches "data.lua-" or "foo/bar.lua-"
 *   - Exact match:  "Makefile" matches "Makefile"
 */
static bool isExcluded(const std::string &path,
		       const std::vector<std::string> &excludes)
{
	// Extract just the filename part for glob matching
	std::string filename = path;
	auto slashPos = path.rfind('/');
	if (slashPos != std::string::npos)
		filename = path.substr(slashPos + 1);

	for (const auto &pattern : excludes) {
		// If pattern contains a glob character, use glob matching
		if (pattern.find('*') != std::string::npos) {
			if (matchesGlob(filename, pattern))
				return true;
		} else {
			// Otherwise use prefix matching (existing behavior)
			if (path == pattern || path.find(pattern + "/") == 0) {
				return true;
			}
		}
	}
	return false;
}

/**
 * Add files from a source path to the archive.
 * If the source is a directory, it is added recursively.
 * The destName is the name to store the file as in the archive.
 * For directories, destName is used as a prefix for contained files.
 */
static void addPathToArchive(Ogre::PackageArchive &archive,
			     const fs::path &sourcePath,
			     const std::string &destPrefix = "",
			     const std::vector<std::string> &excludes = {})
{
	std::error_code ec;

	if (fs::is_directory(sourcePath, ec)) {
		// Recursively add directory contents
		for (const auto &entry :
		     fs::recursive_directory_iterator(sourcePath, ec)) {
			if (fs::is_regular_file(entry.path(), ec)) {
				// Compute relative path from the source
				// directory
				fs::path relativePath = fs::relative(
					entry.path(), sourcePath, ec);
				std::string destName =
					destPrefix.empty() ?
						relativePath.string() :
						destPrefix + "/" +
							relativePath.string();

				// Normalize path separators to forward slash
				for (auto &c : destName) {
					if (c == '\\')
						c = '/';
				}

				// Skip excluded paths
				if (isExcluded(destName, excludes)) {
					std::cout << "Skipping (excluded): "
						  << entry.path().string()
						  << std::endl;
					continue;
				}

				std::cout << "Adding: " << entry.path().string()
					  << " -> " << destName << std::endl;
				archive.addFileFromDisk(destName,
							entry.path().string());
			}
		}
	} else if (fs::is_regular_file(sourcePath, ec)) {
		// Single file
		std::string destName = destPrefix.empty() ?
					       sourcePath.filename().string() :
					       destPrefix;

		std::cout << "Adding: " << sourcePath.string() << " -> "
			  << destName << std::endl;
		archive.addFileFromDisk(destName, sourcePath.string());
	} else {
		std::cerr << "Warning: skipping '" << sourcePath.string()
			  << "' - not a regular file or directory" << std::endl;
	}
}

/**
 * Parse --exclude options from the argument list.
 * Returns the remaining non-option arguments.
 */
static std::vector<std::string>
parseExcludes(const std::vector<std::string> &args,
	      std::vector<std::string> &excludes)
{
	std::vector<std::string> remaining;
	for (size_t i = 0; i < args.size(); i++) {
		if (args[i] == "--exclude" && i + 1 < args.size()) {
			excludes.push_back(args[i + 1]);
			i++;
		} else {
			remaining.push_back(args[i]);
		}
	}
	return remaining;
}

static int cmdCreate(const std::vector<std::string> &args)
{
	std::vector<std::string> excludes;
	std::vector<std::string> remaining = parseExcludes(args, excludes);

	if (remaining.size() < 2) {
		std::cerr << "Error: create requires a package path and at "
			     "least one source file"
			  << std::endl;
		return 1;
	}

	const std::string &packagePath = remaining[0];
	Ogre::PackageArchive archive(packagePath, "Package");

	// Load will create the file if it doesn't exist
	archive.load();

	for (size_t i = 1; i < remaining.size(); i++) {
		addPathToArchive(archive, remaining[i], "", excludes);
	}

	std::cout << "Created package: " << packagePath << " with "
		  << archive.listFiles().size() << " file(s)" << std::endl;
	return 0;
}

static int cmdAdd(const std::vector<std::string> &args)
{
	if (args.size() < 2) {
		std::cerr << "Error: add requires a package path and at least "
			     "one source file"
			  << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	Ogre::PackageArchive archive(packagePath, "Package");

	archive.load();

	for (size_t i = 1; i < args.size(); i++) {
		addPathToArchive(archive, args[i]);
	}

	std::cout << "Added files to " << packagePath
		  << " (total: " << archive.listFiles().size() << " file(s))"
		  << std::endl;
	return 0;
}

static int cmdRemove(const std::vector<std::string> &args)
{
	if (args.size() < 2) {
		std::cerr << "Error: remove requires a package path and at "
			     "least one filename"
			  << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	Ogre::PackageArchive archive(packagePath, "Package");

	archive.load();

	for (size_t i = 1; i < args.size(); i++) {
		const std::string &filename = args[i];
		std::cout << "Removing: " << filename << std::endl;
		archive.remove(filename);
	}

	std::cout << "Removed " << args.size() - 1 << " file(s) from "
		  << packagePath << std::endl;
	return 0;
}

static int cmdExtract(const std::vector<std::string> &args)
{
	if (args.size() < 2) {
		std::cerr << "Error: extract requires a package path and a "
			     "filename"
			  << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	const std::string &filename = args[1];
	std::string destPath;

	if (args.size() >= 3) {
		destPath = args[2];
	} else {
		destPath = filename;
	}

	Ogre::PackageArchive archive(packagePath, "Package");
	archive.load();

	if (archive.extractFile(filename, destPath)) {
		std::cout << "Extracted: " << filename << " -> " << destPath
			  << std::endl;
		return 0;
	} else {
		std::cerr << "Error: could not extract " << filename
			  << std::endl;
		return 1;
	}
}

static int cmdExtractAll(const std::vector<std::string> &args)
{
	if (args.size() < 1) {
		std::cerr << "Error: extract-all requires a package path"
			  << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	std::string destDir;

	if (args.size() >= 2) {
		destDir = args[1];
	} else {
		destDir = ".";
	}

	Ogre::PackageArchive archive(packagePath, "Package");
	archive.load();

	Ogre::StringVector files = archive.listFiles();
	if (files.empty()) {
		std::cout << "Package is empty." << std::endl;
		return 0;
	}

	// Create destination directory if needed
	fs::create_directories(destDir);

	for (const auto &filename : files) {
		// Preserve directory structure in the extracted path
		fs::path filePath(filename);
		fs::path fullDestPath = fs::path(destDir) / filePath;

		// Create parent directories as needed
		fs::create_directories(fullDestPath.parent_path());

		if (archive.extractFile(filename, fullDestPath.string())) {
			std::cout << "Extracted: " << filename << " -> "
				  << fullDestPath.string() << std::endl;
		} else {
			std::cerr << "Error: could not extract " << filename
				  << std::endl;
		}
	}

	return 0;
}

static int cmdList(const std::vector<std::string> &args)
{
	if (args.size() < 1) {
		std::cerr << "Error: list requires a package path" << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	Ogre::PackageArchive archive(packagePath, "Package");
	archive.load();

	Ogre::StringVector files = archive.listFiles();

	std::cout << "Files in " << packagePath << ":" << std::endl;
	if (files.empty()) {
		std::cout << "  (empty)" << std::endl;
	} else {
		for (const auto &filename : files) {
			std::cout << "  " << filename << std::endl;
		}
	}
	std::cout << "Total: " << files.size() << " file(s)" << std::endl;

	return 0;
}

static int cmdInfo(const std::vector<std::string> &args)
{
	if (args.size() < 1) {
		std::cerr << "Error: info requires a package path" << std::endl;
		return 1;
	}

	const std::string &packagePath = args[0];
	Ogre::PackageArchive archive(packagePath, "Package");
	archive.load();

	Ogre::StringVector files = archive.listFiles();

	std::cout << "Package: " << packagePath << std::endl;
	std::cout << "Type: Ogre Package Archive" << std::endl;
	std::cout << "Files: " << files.size() << std::endl;

	uint64_t totalSize = 0;
	for (const auto &filename : files) {
		// Get file info via listFileInfo
		Ogre::FileInfoListPtr infoList =
			archive.findFileInfo(filename, false, false);
		if (infoList && !infoList->empty()) {
			totalSize += infoList->at(0).uncompressedSize;
		}
	}

	std::cout << "Total uncompressed size: " << totalSize << " bytes"
		  << std::endl;
	std::cout << std::endl;
	std::cout << "Files:" << std::endl;

	for (const auto &filename : files) {
		Ogre::FileInfoListPtr infoList =
			archive.findFileInfo(filename, false, false);
		if (infoList && !infoList->empty()) {
			const auto &info = infoList->at(0);
			std::cout << "  " << filename << " ("
				  << info.uncompressedSize << " bytes)"
				  << std::endl;
		} else {
			std::cout << "  " << filename << std::endl;
		}
	}

	return 0;
}

int main(int argc, char **argv)
{
	if (argc < 2) {
		printUsage(argv[0]);
		return 1;
	}

	std::string command = argv[1];
	std::vector<std::string> args;
	for (int i = 2; i < argc; i++) {
		args.push_back(argv[i]);
	}

	try {
		if (command == "create") {
			return cmdCreate(args);
		} else if (command == "add") {
			return cmdAdd(args);
		} else if (command == "remove") {
			return cmdRemove(args);
		} else if (command == "extract") {
			return cmdExtract(args);
		} else if (command == "extract-all") {
			return cmdExtractAll(args);
		} else if (command == "list") {
			return cmdList(args);
		} else if (command == "info") {
			return cmdInfo(args);
		} else {
			std::cerr << "Error: unknown command '" << command
				  << "'" << std::endl;
			printUsage(argv[0]);
			return 1;
		}
	} catch (const Ogre::Exception &e) {
		std::cerr << "Ogre Error: " << e.getFullDescription()
			  << std::endl;
		return 1;
	} catch (const std::exception &e) {
		std::cerr << "Error: " << e.what() << std::endl;
		return 1;
	}
}
