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

#include <ox/fs/fs.hpp>
#include <ox/model/modelvalue.hpp>

#include <keel/media.hpp>
#include <keel/pack.hpp>

namespace keel {

static ox::Error pathToInode(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelObject &obj) noexcept {
	OX_REQUIRE(typeVal, obj.at("type"));
	auto const type = static_cast<ox::FileAddressType>(typeVal->get<int8_t>());
	OX_REQUIRE(dataVal, obj.at("data"));
	auto &data = dataVal->get<ox::ModelUnion>();
	ox::String path;
	switch (type) {
		case ox::FileAddressType::Path: {
			OX_REQUIRE(pathVal, data.at("path"));
			path = pathVal->get<ox::String>();
			break;
		}
		case ox::FileAddressType::ConstPath: {
			OX_REQUIRE(pathVal, data.at("constPath"));
			path = pathVal->get<ox::String>();
			break;
		}
		case ox::FileAddressType::Inode:
		case ox::FileAddressType::None:
			return {};
	}
	if (beginsWith(path, "uuid://")) {
		auto const uuid = ox::substr(path, 7);
		OX_RETURN_ERROR(keel::uuidToPath(ctx, uuid).to<ox::String>().moveTo(path));
	}
	auto const inode = dest.stat(path)
		.or_value({.inode = 0})
		.inode;
	OX_RETURN_ERROR(typeVal->set(static_cast<int8_t>(ox::FileAddressType::Inode)));
	oxOutf("\tpath to inode: {} => {}\n", path, inode);
	return data.set(2, inode);
}

static ox::Error transformFileAddressesObj(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelObject &obj) noexcept;

static ox::Error transformFileAddressesVec(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelValueVector &v) noexcept;

static ox::Error transformFileAddresses(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelValue &v) noexcept {
	switch (v.type()) {
		case ox::ModelValue::Type::Object: {
			auto &obj = v.get<ox::ModelObject>();
			return transformFileAddressesObj(ctx, dest, obj);
		}
		case ox::ModelValue::Type::Vector: {
			auto &vec = v.get<ox::ModelValueVector>();
			return transformFileAddressesVec(ctx, dest, vec);
		}
		default:
			return {};
	}
}

static ox::Error transformFileAddressesVec(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelValueVector &v) noexcept {
	for (auto &f : v) {
		OX_RETURN_ERROR(transformFileAddresses(ctx, dest, f));
	}
	return {};
}

/**
 * Convert path references in Claw data to inodes to save space
 * @return error
 */
static ox::Error transformFileAddressesObj(
		Context &ctx,
		ox::FileSystem const &dest,
		ox::ModelObject &obj) noexcept {
	if (obj.typeName() == "net.drinkingtea.ox.FileAddress" && obj.typeVersion() == 1) {
		return pathToInode(ctx, dest, obj);
	}
	for (auto const &f : obj) {
		auto &v = f->value;
		OX_RETURN_ERROR(transformFileAddresses(ctx, dest, v));
	}
	return {};
}

static ox::Error performPackTransforms(
		ManifestEntry &entry,
		Context &ctx,
		ox::Buffer &clawData) noexcept {
	OX_REQUIRE_M(typeId, readAssetTypeId(clawData));
	for (auto const tr : packTransforms(ctx)) {
		bool changed{};
		OX_RETURN_ERROR(tr(ctx, clawData, typeId).moveTo(changed));
		if (changed) {
			OX_RETURN_ERROR(readAssetTypeId(clawData).moveTo(typeId));
		}
	}
	entry.type = ox::String{typeId};
	return {};
}

static ox::Error doTransformations(
		Manifest &manifest,
		Context &ctx,
		ox::TypeStore &ts,
		ox::FileSystem &dest,
		ox::StringViewCR filePath) noexcept {
	// load file
	OX_REQUIRE(s, dest.stat(filePath));
	// do transformations
	OX_REQUIRE_M(buff, dest.read(s.inode));
	OX_RETURN_ERROR(keel::performPackTransforms(manifest.files[filePath], ctx, buff));
	// transform FileAddresses
	OX_REQUIRE_M(obj, keel::readAsset(ts, buff));
	oxOutf("transforming {}\n", filePath);
	OX_RETURN_ERROR(transformFileAddressesObj(ctx, dest, obj));
	OX_RETURN_ERROR(ox::writeClaw(obj).moveTo(buff));
	// write file to dest
	OX_RETURN_ERROR(dest.write(s.inode, buff));
	return {};
}

// claw file transformations are broken out from copy because path to inode
// transformations need to be done after the copy to the new FS is complete
static ox::Error transformClaw(
		Manifest &manifest,
		Context &ctx,
		ox::TypeStore &ts,
		ox::FileSystem &dest,
		ox::StringViewCR path) noexcept {
	// copy
	oxTracef("pack.transformClaw", "path: {}", path);
	OX_REQUIRE(fileList, dest.ls(path));
	for (auto const &name : fileList) {
		auto const filePath = ox::sfmt("{}{}", path, name);
		OX_REQUIRE(stat, dest.stat(filePath));
		if (stat.fileType == ox::FileType::Directory) {
			auto const dir = ox::sfmt("{}{}/", path, name);
			OX_RETURN_ERROR(transformClaw(manifest, ctx, ts, dest, dir));
		} else {
			auto const err = doTransformations(manifest, ctx, ts, dest, filePath);
			if (err) {
				oxErrf("\033[31;1;1mCould not do transformations for {}:\n\t{}\n", filePath, toStr(err));
				return err;
			}
		}
	}
	return {};
}

static ox::Error copy(
		Manifest &manifest,
		ox::FileSystem &src,
		ox::FileSystem &dest,
		ox::StringViewCR path,
		ox::StringViewCR logPrefix = "") noexcept {
	oxOutf("{}copying directory: {}\n", logPrefix, path);
	auto const childLogPrefix = ox::sfmt("{}\t", logPrefix);
	// copy
	OX_REQUIRE(fileList, src.ls(path));
	for (auto const &name : fileList) {
		auto const currentFile = ox::sfmt("{}{}", path, name);
		if (beginsWith(name, ".")) {
			continue;
		}
		OX_REQUIRE(srcStat, src.stat(currentFile));
		if (srcStat.fileType == ox::FileType::Directory) {
			OX_RETURN_ERROR(dest.mkdir(currentFile, true));
			OX_RETURN_ERROR(copy(manifest, src, dest, currentFile + '/', childLogPrefix));
		} else {
			// load file
			oxOutf("{}copying file: {}...", childLogPrefix, currentFile);
			ox::StringView status = "failed";
			OX_DEFER [&status] {
				oxOutf(" {}\n", status);
			};
			OX_REQUIRE(buff, src.read(currentFile));
			// write file to dest
			OX_RETURN_ERROR(dest.write(currentFile, buff));
			status = "OK";
			OX_REQUIRE(dstStat, dest.stat(currentFile));
			manifest.files[currentFile] = {
				.inode = dstStat.inode,
				.type = ox::String{keel::readAssetTypeId(buff).or_value({})},
			};
		}
	}
	return {};
}

ox::Error pack(
		Manifest &manifest,
		Context &ctx,
		ox::TypeStore &ts,
		ox::FileSystem &dest) noexcept {
	OX_RETURN_ERROR(copy(manifest, *ctx.rom, dest, "/"));
	oxOut("Doing transforms\n");
	OX_RETURN_ERROR(transformClaw(manifest, ctx, ts, dest, "/"));
	return {};
}

}
