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

#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/std/array.hpp>

#include <teagba/addresses.hpp>
#include <teagba/gfx.hpp>
#include <teagba/registers.hpp>

#include <turbine/turbine.hpp>

#include <nostalgia/core/color.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/tilesheet.hpp>

#include "context.hpp"

namespace nostalgia::core {

constexpr auto GbaTileColumns = 32;
constexpr auto GbaTileRows = 32;
constexpr auto SpriteCount = 128;

struct GbaPaletteTarget {
	static constexpr auto TypeName = Palette::TypeName;
	static constexpr auto TypeVersion = Palette::TypeVersion;
	volatile uint16_t *palette = nullptr;
};

struct GbaTileMapTarget {
	static constexpr auto TypeName = CompactTileSheet::TypeName;
	static constexpr auto TypeVersion = CompactTileSheet::TypeVersion;
	unsigned &bpp;
	ox::FileAddress defaultPalette;
	volatile uint16_t *tileMap = nullptr;
	// the following values are not actually in CompactTileSheet,
	// and only exist to communicate with the loading process
	size_t tileWriteIdx = 0;
	unsigned targetBpp = 0;
	TileSheetSetEntry const*setEntry = nullptr;
};

constexpr ox::Error model(auto *io, ox::CommonPtrWith<GbaPaletteTarget> auto *t) noexcept {
	oxReturnError(io->template setTypeInfo<Palette>());
	if (t->palette) {
		const auto colorHandler = [t](std::size_t i, const Color16 *c) {
			t->palette[i] = *c;
			return ox::Error{};
		};
		return io->template field<Color16, decltype(colorHandler)>("colors", colorHandler);
	} else {
		constexpr auto colorHandler = [](std::size_t, const Color16*) {
			return ox::Error{};
		};
		return io->template field<Color16, decltype(colorHandler)>("colors", colorHandler);
	}
}

[[nodiscard]]
static bool loadPixel(TileSheetSetEntry const&setEntry, size_t &sectionIdx, int tileIdx) noexcept {
	if (setEntry.sections.size() <= sectionIdx) {
		return false;
	}
	auto &section = setEntry.sections[sectionIdx];
	if (tileIdx < section.begin) {
		return false;
	}
	if (tileIdx > section.end()) {
		if (sectionIdx >= setEntry.sections.size()) {
			return false;
		}
		++sectionIdx;
		return tileIdx > section.begin && tileIdx <= section.end();
	}
	return true;
}

constexpr ox::Error model(auto *io, ox::CommonPtrWith<GbaTileMapTarget> auto *t) noexcept {
	oxReturnError(io->template setTypeInfo<CompactTileSheet>());
	oxReturnError(io->field("bpp", &t->bpp));
	oxReturnError(io->field("defaultPalette", &t->defaultPalette));
	if (t->targetBpp == 0) {
		t->targetBpp = t->bpp;
	}
	if (t->targetBpp != t->bpp && t->bpp == 8) {
		return OxError(1, "Cannot load an 8 BPP tilesheet into a 4 BPP CBB");
	}
	ox::Error out{};
	if (t->setEntry) {
		// The following code is atrocious, but it works.
		// It might be possible to clean it up a little, but it probably
		// cannot be seriously optimized without preloading TileSheets.
		size_t sectionIdx = 0;
		if (t->targetBpp == t->bpp) {
			uint16_t intermediate = 0;
			size_t const fourBpp = t->bpp == 4;
			const auto handleTileMap = [t, &intermediate, &sectionIdx, fourBpp]
					(std::size_t i, uint8_t const*tile) {
				auto const tileIdx = static_cast<int>((i * (2 * fourBpp)) / PixelsPerTile);
				if (!loadPixel(*t->setEntry, sectionIdx, tileIdx)) {
					return ox::Error{};
				}
				if (i & 1) { // i is odd
					intermediate |= static_cast<uint16_t>(*tile) << 8;
					t->tileMap[t->tileWriteIdx] = intermediate;
					++t->tileWriteIdx;
				} else { // i is even
					intermediate = *tile & 0x00ff;
				}
				return ox::Error{};
			};
			out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
		} else if (t->targetBpp > t->bpp) { // 4 -> 8 bits
			const auto handleTileMap = [t, &sectionIdx](std::size_t i, uint8_t const*tile) {
				auto constexpr BytesPerTile4Bpp = 32;
				auto const tileIdx = static_cast<int>(i / BytesPerTile4Bpp);
				if (!loadPixel(*t->setEntry, sectionIdx, tileIdx)) {
					return ox::Error{};
				}
				uint16_t const px1 = *tile & 0xf;
				uint16_t const px2 = *tile >> 4;
				t->tileMap[t->tileWriteIdx] = static_cast<uint16_t>(px1 | (px2 << 8));
				++t->tileWriteIdx;
				return ox::Error{};
			};
			out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
		}
	} else {
		uint16_t intermediate = 0;
		const auto handleTileMap = [t, &intermediate](std::size_t i, const uint8_t*tile) {
			if (i & 1) { // i is odd
				intermediate |= static_cast<uint16_t>(*tile) << 8;
				t->tileMap[i / 2] = intermediate;
			} else { // i is even
				intermediate = *tile & 0x00ff;
			}
			return ox::Error{};
		};
		out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
	}
	return out;
}

ox::Error initGfx(Context&, InitParams const&) noexcept {
	for (auto bgCtl = &REG_BG0CTL; bgCtl <= &REG_BG3CTL; bgCtl += 2) {
		teagba::bgSetSbb(*bgCtl, 28);
	}
	for (uint16_t i = 0; i < SpriteCount; ++i) {
		auto &sa = teagba::spriteAttr(i);
		sa.idx = i;
	}
	return {};
}

ox::Error loadBgPalette(
		Context &ctx,
		size_t palBank,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &rom = ctx.rom();
	GbaPaletteTarget const palTarget{.palette = MEM_BG_PALETTE + palBank * 16};
	oxRequire(palStat, rom.stat(paletteAddr));
	oxRequire(pal, rom.directAccess(paletteAddr));
	oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &palTarget));
	return {};
}

ox::Error loadSpritePalette(
		Context &ctx,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &rom = ctx.rom();
	GbaPaletteTarget const palTarget{.palette = MEM_SPRITE_PALETTE};
	oxRequire(palStat, rom.stat(paletteAddr));
	oxRequire(pal, rom.directAccess(paletteAddr));
	oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &palTarget));
	return {};
}

static ox::Error loadTileSheetSet(
		Context &ctx,
		uint16_t *tileMapTargetMem,
		TileSheetSet const&set) noexcept {
	auto &rom = ctx.rom();
	size_t tileWriteIdx = 0;
	for (auto const&entry : set.entries) {
		oxRequire(tsStat, rom.stat(entry.tilesheet));
		oxRequire(ts, rom.directAccess(entry.tilesheet));
		unsigned tilesheetBpp{};
		GbaTileMapTarget target{
			.bpp = tilesheetBpp,
			.defaultPalette = {},
			.tileMap = tileMapTargetMem + tileWriteIdx,
			.targetBpp = static_cast<unsigned>(set.bpp),
			.setEntry = &entry,
		};
		oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
		tileWriteIdx += target.tileWriteIdx;
	}
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned cbb,
		ox::FileAddress const&tilesheetAddr,
		ox::Optional<unsigned> const&paletteBank) noexcept {
	auto &rom = ctx.rom();
	oxRequire(tsStat, rom.stat(tilesheetAddr));
	oxRequire(ts, rom.directAccess(tilesheetAddr));
	GbaTileMapTarget target{
		.bpp = ctx.cbbData[cbb].bpp,
		.defaultPalette = {},
		.tileMap = MEM_BG_TILES[cbb].data(),
	};
	oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
	// update bpp of all bgs with the updated cbb
	const auto bpp = ctx.cbbData[cbb].bpp;
	teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) {
		if (teagba::bgCbb(bgCtl) == cbb) {
			teagba::bgSetBpp(bgCtl, bpp);
		}
	});
	if (paletteBank.has_value() && target.defaultPalette) {
		oxReturnError(loadBgPalette(ctx, *paletteBank, target.defaultPalette));
	}
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned cbb,
		TileSheetSet const&set) noexcept {
	auto const bpp = static_cast<unsigned>(set.bpp);
	oxReturnError(loadTileSheetSet(ctx, MEM_BG_TILES[cbb].data(), set));
	// update bpp of all bgs with the updated cbb
	ctx.cbbData[cbb].bpp = bpp;
	teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) {
		if (teagba::bgCbb(bgCtl) == cbb) {
			teagba::bgSetBpp(bgCtl, bpp);
		}
	});
	return {};
}

static void setSpritesBpp(unsigned const bpp) noexcept {
	auto const eightBpp = static_cast<uint16_t >(bpp == 8);
	for (auto i = 0u; i < SpriteCount; ++i) {
		auto &sa = teagba::spriteAttr(i);
		auto &a = sa.attr0;
		a |= static_cast<uint16_t>((a & ~static_cast<uint16_t>(1u << 13)) | (eightBpp << 13));
	}
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		ox::FileAddress const&tilesheetAddr,
		bool loadDefaultPalette) noexcept {
	auto &rom = ctx.rom();
	oxRequire(tsStat, rom.stat(tilesheetAddr));
	oxRequire(ts, rom.directAccess(tilesheetAddr));
	unsigned bpp{};
	GbaTileMapTarget target{
		.bpp = bpp,
		.defaultPalette = {},
		.tileMap = MEM_SPRITE_TILES,
	};
	oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
	if (loadDefaultPalette && target.defaultPalette) {
		oxReturnError(loadSpritePalette(ctx, target.defaultPalette));
	}
	setSpritesBpp(bpp);
	return {};
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		TileSheetSet const&set) noexcept {
	auto const bpp = static_cast<unsigned>(set.bpp);
	oxReturnError(loadTileSheetSet(ctx, MEM_SPRITE_TILES, set));
	setSpritesBpp(bpp);
	return {};
}

void setBgTile(Context&, uint_t bgIdx, int column, int row, BgTile const&tile) noexcept {
	auto const tileIdx = static_cast<std::size_t>(row * GbaTileColumns + column);
	// see Tonc 9.3
	MEM_BG_MAP[bgIdx][tileIdx] =
			static_cast<uint16_t>(tile.tileIdx & 0b1'1111'1111) |
			static_cast<uint16_t>(tile.flipX << 0xa) |
			static_cast<uint16_t>(tile.flipY << 0xb) |
			static_cast<uint16_t>(tile.palBank << 0xc);
}

void clearBg(Context&, uint_t bgIdx) noexcept {
	memset(MEM_BG_MAP[bgIdx].data(), 0, GbaTileRows * GbaTileColumns);
}

uint8_t bgStatus(Context&) noexcept {
	return (REG_DISPCTL >> 8u) & 0b1111u;
}

void setBgStatus(Context&, uint32_t status) noexcept {
	constexpr auto BgStatus = 8;
	REG_DISPCTL = (REG_DISPCTL & ~0b111100000000u) | status << BgStatus;
}

bool bgStatus(Context&, unsigned bg) noexcept {
	return (REG_DISPCTL >> (8 + bg)) & 1;
}

void setBgStatus(Context&, unsigned bg, bool status) noexcept {
	constexpr auto Bg0Status = 8;
	const auto mask = static_cast<uint32_t>(status) << (Bg0Status + bg);
	REG_DISPCTL = REG_DISPCTL | ((REG_DISPCTL & ~mask) | mask);
}

void setBgBpp(Context&, unsigned bgIdx, unsigned bpp) noexcept {
	auto &bgCtl = regBgCtl(bgIdx);
	teagba::bgSetBpp(bgCtl, bpp);
}

void setBgCbb(Context &ctx, unsigned bgIdx, unsigned cbb) noexcept {
	auto &bgCtl = regBgCtl(bgIdx);
	const auto &cbbData = ctx.cbbData[cbb];
	teagba::bgSetBpp(bgCtl, cbbData.bpp);
	teagba::bgSetCbb(bgCtl, cbb);
}

void setBgPriority(Context&, uint_t bgIdx, uint_t priority) noexcept {
	auto &bgCtl = regBgCtl(bgIdx);
	bgCtl = (bgCtl & 0b1111'1111'1111'1100u) | (priority & 0b11);
}

void hideSprite(Context&, unsigned idx) noexcept {
	//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
	teagba::addSpriteUpdate({
		.attr0 = uint16_t{0b11 << 8},
		.idx = static_cast<uint16_t>(idx),
	});
}

void showSprite(Context&, unsigned idx) noexcept {
	//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
	teagba::addSpriteUpdate({
		.attr0 = 0,
		.idx = static_cast<uint16_t>(idx),
	});
}

void setSprite(Context&, uint_t idx, Sprite const&s) noexcept {
	//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
	uint16_t const eightBpp = s.bpp == 8;
	teagba::addSpriteUpdate({
		.attr0 = static_cast<uint16_t>(
			  (static_cast<uint16_t>(s.y & ox::onMask<uint8_t>(0b111'1111)))
			| (static_cast<uint16_t>(1) << 10) // enable alpha
			| (static_cast<uint16_t>(eightBpp) << 13)
			| (static_cast<uint16_t>(s.spriteShape) << 14)),
		.attr1 = static_cast<uint16_t>(
			  (static_cast<uint16_t>(s.x) & ox::onMask<uint8_t>(8))
			| (static_cast<uint16_t>(s.flipX) << 12)
			| (static_cast<uint16_t>(s.spriteSize) << 14)),
		.attr2 = static_cast<uint16_t>(
			// double tileIdx if 8 bpp
			  (static_cast<uint16_t>((s.tileIdx * (1 + eightBpp)) & ox::onMask<uint16_t>(8)))
			| (static_cast<uint16_t>(s.priority & 0b11) << 10)),
		.idx = static_cast<uint16_t>(idx),
	});
}

uint_t spriteCount(Context&) noexcept {
	return SpriteCount;
}

}
