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

#include <keel/media.hpp>

#ifndef OX_BARE_METAL

#include <fstream>

#include <ox/std/trace.hpp>

namespace keel {

ox::Result<char*> loadRom(ox::CRStringView path) noexcept {
	std::ifstream file(std::string(toStdStringView(path)), std::ios::binary | std::ios::ate);
	if (!file.good()) {
		oxErrorf("Could not find ROM file: {}", path);
		return OxError(1, "Could not find ROM file");
	}
	try {
		const auto size = file.tellg();
		file.seekg(0, std::ios::beg);
		auto buff = new char[static_cast<std::size_t>(size)];
		file.read(buff, size);
		return buff;
	} catch (std::ios_base::failure const&e) {
		oxErrorf("Could not read ROM file due to file IO failure: {}", e.what());
		return OxError(2, "Could not read ROM file");
	} catch (std::bad_alloc const&e) {
		oxErrorf("Could not read ROM file due to new failure: {}", e.what());
		return OxError(2, "Could not allocate memory for ROM file");
	}
}

void unloadRom(char *rom) noexcept {
	ox::safeDelete(rom);
}

static void clearUuidMap(Context &ctx) noexcept {
	ctx.uuidToPath.clear();
	ctx.pathToUuid.clear();
}

void createUuidMapping(Context &ctx, ox::StringView filePath, ox::UUID const&uuid) noexcept {
	ctx.pathToUuid[filePath] = uuid;
	ctx.uuidToPath[uuid.toString()] = filePath;
}

static ox::Error buildUuidMap(Context &ctx, ox::CRStringView path) noexcept {
	oxRequire(files, ctx.rom->ls(path));
	for (const auto &f : files) {
		oxRequireM(filePath, ox::join("/", ox::Array<ox::StringView, 2>{path, f}));
		oxRequire(stat, ctx.rom->stat(filePath));
		if (stat.fileType == ox::FileType::NormalFile) {
			oxRequire(data, ctx.rom->read(filePath));
			const auto [hdr, err] = readAssetHeader(data);
			if (!err) {
				createUuidMapping(ctx, filePath, hdr.uuid);
			}
		} else if (stat.fileType == ox::FileType::Directory) {
			if (!beginsWith(f, ".")) {
				oxReturnError(buildUuidMap(ctx, filePath));
			}
		}
	}
	return {};
}

ox::Error buildUuidMap(Context &ctx) noexcept {
	if (!ctx.rom) {
		return OxError(1, "No ROM FS");
	}
	return buildUuidMap(ctx, "");
}

ox::Result<ox::UUID> pathToUuid(Context &ctx, ox::CRStringView path) noexcept {
#ifndef OX_BARE_METAL
	oxRequire(out, ctx.pathToUuid.at(path));
	return *out;
#else
	return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}

ox::Result<ox::String> uuidToPath(Context &ctx, ox::CRStringView uuid) noexcept {
#ifndef OX_BARE_METAL
	oxRequire(out, ctx.uuidToPath.at(uuid));
	return *out;
#else
	return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}

ox::Result<ox::String> uuidToPath(Context &ctx, ox::UUID const&uuid) noexcept {
#ifndef OX_BARE_METAL
	oxRequire(out, ctx.uuidToPath.at(uuid.toString()));
	return *out;
#else
	return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}

ox::Error performPackTransforms(Context &ctx, ox::Buffer &clawData) noexcept {
#ifndef OX_BARE_METAL
	for (auto tr : ctx.packTransforms) {
		oxReturnError(tr(ctx, clawData));
	}
	return {};
#else
	return OxError(1, "Transformations not supported on this platform");
#endif
}

}

#else

#include <keel/context.hpp>

#define MEM_ROM reinterpret_cast<char*>(0x0800'0000)

namespace keel {

static void clearUuidMap(Context&) noexcept {
}

ox::Error buildUuidMap(Context&) noexcept {
	return {};
}

ox::Result<char*> loadRom(ox::CRStringView) noexcept {
	// put the header in the wrong order to prevent mistaking this code for the
	// media section
	constexpr auto headerP2 = "R_______________";
	constexpr auto headerP1 = "KEEL_MEDIA_HEADE";
	constexpr auto headerP1Len = ox_strlen(headerP2);
	constexpr auto headerP2Len = ox_strlen(headerP1);
	constexpr auto headerLen = headerP1Len + headerP2Len;
	for (auto current = MEM_ROM; current < reinterpret_cast<char*>(0x0a000000); current += headerLen) {
		if (ox_memcmp(current, headerP1, headerP1Len) == 0 &&
		    ox_memcmp(current + headerP1Len, headerP2, headerP2Len) == 0) {
			return current + headerLen;
		}
	}
	return OxError(1);
}

void unloadRom(char*) noexcept {
}

ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, ox::CRStringView path) noexcept {
	oxRequire(stat, ctx.rom->stat(path));
	oxRequire(buff, static_cast<ox::MemFS*>(ctx.rom.get())->directAccess(path));
	PreloadPtr p;
	oxReturnError(ox::readMC(buff, static_cast<std::size_t>(stat.size), &p));
	return static_cast<std::size_t>(p.preloadAddr) + ctx.preloadSectionOffset;
}

ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, const ox::FileAddress &file) noexcept {
	oxRequire(stat, ctx.rom->stat(file));
	oxRequire(buff, static_cast<ox::MemFS*>(ctx.rom.get())->directAccess(file));
	PreloadPtr p;
	oxReturnError(ox::readMC(buff, static_cast<std::size_t>(stat.size), &p));
	return static_cast<std::size_t>(p.preloadAddr) + ctx.preloadSectionOffset;
}

}

#endif

namespace keel {

ox::Error setRomFs(Context &ctx, ox::UPtr<ox::FileSystem> &&fs) noexcept {
	ctx.rom = std::move(fs);
	clearUuidMap(ctx);
	return buildUuidMap(ctx);
}

ox::Result<ox::UniquePtr<ox::FileSystem>> loadRomFs(ox::CRStringView path) noexcept {
	auto const lastDot = ox::lastIndexOf(path, '.');
	if (!lastDot.error && substr(path, lastDot.value) == ".oxfs") {
		oxRequire(rom, loadRom(path));
		return {ox::make_unique<ox::FileSystem32>(rom, 32 * ox::units::MB, unloadRom)};
	} else {
#ifdef OX_HAS_PASSTHROUGHFS
		return {ox::make_unique<ox::PassThroughFS>(path)};
#else
		return OxError(2);
#endif
	}
}

}
