/*
 * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#include <fstream>

#include <ox/clargs/clargs.hpp>
#include <ox/fs/fs.hpp>
#include <ox/oc/write.hpp>

#include <keel/keel.hpp>

static ox::Error writeFileBuff(ox::StringViewCR path, ox::BufferView const &buff) noexcept {
	try {
		std::ofstream f(std::string(toStdStringView(path)), std::ios::binary);
		f.write(buff.data(), static_cast<intptr_t>(buff.size()));
	} catch (std::fstream::failure const&) {
		return ox::Error(2, "failed to write file");
	}
	return {};
}

static ox::Result<ox::Buffer> readFileBuff(ox::StringViewCR path) noexcept {
	std::ifstream file(std::string(toStdStringView(path)), std::ios::binary | std::ios::ate);
	if (!file.good()) {
		oxErrorf("Could not find OxFS file: {}", path);
		return ox::Error(1, "Could not find OxFS file");
	}
	try {
		auto const size = static_cast<std::size_t>(file.tellg());
		ox::Buffer buff(size);
		file.seekg(0, std::ios::beg);
		file.read(buff.data(), static_cast<std::streamsize>(buff.size()));
		return buff;
	} catch (std::ios_base::failure const &e) {
		oxErrorf("Could not read OxFS file: {}", e.what());
		return ox::Error(2, "Could not read OxFS file");
	}
}

static ox::Error generateTypes(ox::TypeStore &ts) noexcept {
	for (auto const mod : keel::modules()) {
		for (auto const gen : mod->types()) {
			OX_RETURN_ERROR(gen(ts));
		}
	}
	return {};
}

static ox::Error pack(
		ox::StringViewCR argSrc,
		ox::StringViewCR argRomBin,
		ox::StringViewCR argManifest,
		ox::StringViewCR projectDataDir) noexcept {
	ox::Buffer dstBuff(32 * ox::units::MB);
	OX_RETURN_ERROR(ox::FileSystem32::format(dstBuff.data(), dstBuff.size()));
	ox::FileSystem32 dst(dstBuff);
	keel::DuplicateSet duplicateSet;
	OX_REQUIRE(ctx, keel::init(
		ox::make_unique<ox::PassThroughFS>(argSrc), "keel-pack", duplicateSet));
	if (duplicateSet.size()) {
		oxErr("Multiple files have the same UUID:\n");
		for (auto const &k : duplicateSet.keys()) {
			oxErrf("\n\t{}:\n", k.toString());
			for (auto const &v : duplicateSet[k]) {
				oxErrf("\t\t{}\n", v);
			}
		}
		oxErr("\n");
		std::exit(1);
	}
	keel::TypeStore ts(*ctx->rom, ox::sfmt("{}/type_descriptors", projectDataDir));
	OX_RETURN_ERROR(generateTypes(ts));
	keel::Manifest manifest;
	OX_RETURN_ERROR(keel::pack(manifest, *ctx, ts, dst));
	OX_REQUIRE_M(pl, keel::GbaPreloader::make());
	OX_RETURN_ERROR(preload(manifest, ts, dst, *pl));
	OX_RETURN_ERROR(dst.resize());
	// resize buffer
	OX_REQUIRE(dstSize, dst.size());
	dstBuff.resize(dstSize);
	// concatenate ROM segments
	OX_REQUIRE_M(romBuff, readFileBuff(argRomBin));
	oxOutf("Input exe size: {} bytes\n", romBuff.size());
	oxOutf("Dest FS size: {} bytes\n", dstSize);
	oxOutf("Preload buff size: {} bytes\n", pl->buff().size());
	OX_RETURN_ERROR(appendBinary(romBuff, dstBuff, *pl));
	oxOutf("Final ROM buff size: {} bytes\n", romBuff.size());
	OX_RETURN_ERROR(writeFileBuff(argRomBin, romBuff));
	OX_REQUIRE(manifestJson, ox::writeOCString(manifest));
	OX_RETURN_ERROR(writeFileBuff(argManifest, {manifestJson.data(), manifestJson.size()}));
	return {};
}

namespace keel {

ox::Error pack(
		ox::StringViewCR projectDir,
		ox::StringViewCR projectDataDir,
		ox::SpanView<ox::CString> const argv) noexcept {
	ox::ClArgs const args(argv);
	auto const argRomBin = args.getString("rom-bin", "");
	auto const argManifest = args.getString("manifest", "");
	if (argRomBin == "") {
		oxErr("\033[31;1;1merror:\033[0m must specify a path for ROM file\n");
		return ox::Error(1, "must specify a path for preload file");
	}
	return ::pack(projectDir, argRomBin, argManifest, projectDataDir);
}

}