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

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

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

#include <keel/keel.hpp>

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

OX_ALLOW_UNSAFE_BUFFERS_BEGIN

namespace nostalgia::gfx {

struct BgCbbData {
	unsigned bpp = 4;
};

class Context final {

	public:
		turbine::Context &turbineCtx;
		ox::Array<BgCbbData, 4> cbbData;
		ox::Array<OffsetPair, 4> bgOffsets;

		explicit Context(turbine::Context &tctx) noexcept: turbineCtx{tctx} {}
		Context(Context const &other) noexcept = delete;

};

void safeDelete(Context *ctx) noexcept {
	delete ctx;
}

keel::Context &keelCtx(Context &ctx) noexcept {
	return turbine::keelCtx(ctx.turbineCtx);
}

turbine::Context &turbineCtx(Context &ctx) noexcept {
	return ctx.turbineCtx;
}

static constexpr auto SpriteCount = 128;

static ox::Error initGfx() 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::Result<ox::UPtr<Context>> init(turbine::Context &tctx, InitParams const&) noexcept {
	auto ctx = ox::make_unique<Context>(tctx);
	OX_RETURN_ERROR(initGfx());
	return ctx;
}

ox::Error loadBgPalette(
		Context&,
		size_t const palBank,
		CompactPalette const &palette,
		size_t const page) noexcept {
	if (palette.pages.empty()) {
		return {};
	}
	auto const paletteMem = ox::Span{MEM_BG_PALETTE} + palBank * 16;
	for (auto i = 0u; i < colorCnt(palette, page); ++i) {
		paletteMem[i] = color(palette, page, i);
	}
	return {};
}

ox::Error loadSpritePalette(
		Context&,
		CompactPalette const &palette,
		size_t const page) noexcept {
	if (palette.pages.empty()) {
		return {};
	}
	for (auto i = 0u; i < colorCnt(palette, page); ++i) {
		MEM_SPRITE_PALETTE[i] = color(palette, page, i);
	}
	return {};
}

void clearCbb(Context&, unsigned const cbb) noexcept {
	for (auto &v : MEM_BG_TILES[cbb]) {
		v = 0;
	}
}

void clearCbbs(Context &ctx) noexcept {
	clearCbb(ctx, 0);
	clearCbb(ctx, 1);
	clearCbb(ctx, 2);
	clearCbb(ctx, 3);
}

static ox::Error loadTileSheetSet(
		Context &ctx,
		ox::Span<uint16_t> tileMapTargetMem,
		TileSheetSet const &set) noexcept {
	size_t tileWriteIdx = 0;
	size_t const bppMod = set.bpp == 4;
	for (auto const &entry : set.entries) {
		OX_REQUIRE(ts, keel::readObj<CompactTileSheet>(keelCtx(ctx), entry.tilesheet));
		if (set.bpp != ts->bpp && ts->bpp == 8) {
			return ox::Error(1, "cannot load an 8 BPP tilesheet into a 4 BPP CBB");
		}
		for (auto const &s : entry.sections) {
			auto const cnt = (static_cast<size_t>(s.tiles) * PixelsPerTile) >> bppMod;
			for (size_t i = 0; i < cnt; ++i) {
				auto const begin = static_cast<size_t>(s.begin) * (PixelsPerTile >> bppMod);
				auto const srcIdx = begin + i * 2;
				auto const v = static_cast<uint16_t>(
						static_cast<uint16_t>(ts->pixels[srcIdx]) |
				        (static_cast<uint16_t>(ts->pixels[srcIdx + 1]) << 8));
				tileMapTargetMem[tileWriteIdx + i] = v;
			}
			tileWriteIdx += cnt >> bppMod;
		}
	}
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned const cbb,
		CompactTileSheet const &ts,
		size_t const dstTileIdx,
		size_t const srcTileIdx,
		size_t const tileCnt) noexcept {
	size_t const bppMod = ts.bpp == 4;
	size_t const bytesPerTile = PixelsPerTile >> bppMod;
	auto const cnt = (tileCnt * bytesPerTile) / 2;
	auto const srcPxIdx = srcTileIdx * bytesPerTile;
	auto const dstPxIdx = (dstTileIdx * bytesPerTile) / 2;
	for (size_t i = 0; i < cnt; ++i) {
		auto const srcIdx = srcPxIdx + i * 2;
		auto const p1 = static_cast<uint16_t>(ts.pixels[srcIdx]);
		auto const p2 = static_cast<uint16_t>(ts.pixels[srcIdx + 1]);
		MEM_BG_TILES[cbb][dstPxIdx + i] = static_cast<uint16_t>(p1 | (p2 << 8));
	}
	// update bpp of all bgs with the updated cbb
	auto const bpp = ctx.cbbData[cbb].bpp;
	teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) {
		if (teagba::bgCbb(bgCtl) == cbb) {
			teagba::bgSetBpp(bgCtl, bpp);
		}
	});
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned const cbb,
		CompactTileSheet const &ts,
		ox::Optional<unsigned> const &paletteBank) noexcept {
	auto const cnt = ts.pixels.size() >> (ts.bpp == 4);
	for (size_t i{}; i < cnt; ++i) {
		auto const srcIdx = i * 2;
		auto const p1 = static_cast<uint16_t>(ts.pixels[srcIdx]);
		auto const p2 = static_cast<uint16_t>(ts.pixels[srcIdx + 1]);
		MEM_BG_TILES[cbb][i] = static_cast<uint16_t>(p1 | (p2 << 8));
	}
	// update bpp of all bgs with the updated cbb
	auto const 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() && ts.defaultPalette) {
		OX_RETURN_ERROR(loadBgPalette(ctx, *paletteBank, ts.defaultPalette));
	}
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned const cbb,
		TileSheetSet const &set) noexcept {
	auto const bpp = static_cast<unsigned>(set.bpp);
	OX_RETURN_ERROR(loadTileSheetSet(ctx, MEM_BG_TILES[cbb], 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,
		CompactTileSheet const &ts,
		bool const loadDefaultPalette) noexcept {
	for (size_t i{}; i < ts.pixels.size(); i += 2) {
		MEM_SPRITE_TILES[i >> 1] =
			static_cast<uint16_t>(
				ts.pixels[i] |
				(static_cast<uint16_t>(ts.pixels[i + 1]) << 8));
	}
	if (loadDefaultPalette && ts.defaultPalette) {
		OX_RETURN_ERROR(loadSpritePalette(ctx, ts.defaultPalette));
	}
	setSpritesBpp(static_cast<unsigned>(ts.bpp));
	return {};
}

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

void setBgTile(
	Context &ctx,
	uint_t const bgIdx,
	int const column,
	int const row,
	BgTile const &tile) noexcept {
	auto const tileIdx = static_cast<std::size_t>(row * tileColumns(ctx) + 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 &ctx, uint_t const bgIdx) noexcept {
	memset(MEM_BG_MAP[bgIdx].data(), 0, static_cast<size_t>(tileRows(ctx) * tileColumns(ctx)));
}

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

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

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

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

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

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

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

void setBgOffset(Context &ctx, uint16_t const bg, int16_t const x, int16_t const y) noexcept {
	ctx.bgOffsets[bg] = {.x = x, .y = y};
	teagba::setBgOffset(bg, x, y);
}

void scrollBgOffset(Context &ctx, uint16_t const bg, int16_t const x, int16_t const y) noexcept {
	auto &o = ctx.bgOffsets[bg];
	o.x += x;
	o.y += y;
	teagba::setBgOffset(bg, o.x, o.y);
}

void hideSprite(Context&, unsigned const 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 const 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 const idx, Sprite const &sprite) noexcept {
	//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
	uint16_t const eightBpp = sprite.bpp == 8;
	teagba::addSpriteUpdate({
		.attr0 = static_cast<uint16_t>(
			  (static_cast<uint16_t>(sprite.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>(sprite.spriteShape) << 14)),
		.attr1 = static_cast<uint16_t>(
			  (static_cast<uint16_t>(sprite.x) & ox::onMask<uint8_t>(8))
			| (static_cast<uint16_t>(sprite.flipX) << 12)
			| (static_cast<uint16_t>(sprite.spriteSize) << 14)),
		.attr2 = static_cast<uint16_t>(
			// double tileIdx if 8 bpp
			  (static_cast<uint16_t>((sprite.tileIdx * (1 + eightBpp)) & ox::onMask<uint16_t>(8)))
			| (static_cast<uint16_t>(sprite.priority & 0b11) << 10)),
		.idx = static_cast<uint16_t>(idx),
	});
}

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

}

namespace ox {

void panic(char const*panicMsg, Error const&err, std::source_location const &src) noexcept {
	using namespace nostalgia::gfx;
	// reset heap to make sure we have enough memory to allocate context data
	OX_ALLOW_UNSAFE_BUFFERS_BEGIN
	auto const heapBegin = reinterpret_cast<char*>(MEM_EWRAM.data());
	auto const heapSz = MEM_EWRAM.size() / 2;
	auto const heapEnd = reinterpret_cast<char*>(MEM_EWRAM.data() + heapSz);
	ox::heapmgr::initHeap(heapBegin, heapEnd);
	OX_ALLOW_UNSAFE_BUFFERS_END
	auto tctx = turbine::init(keel::loadRomFs("").unwrap(), "Nostalgia").unwrap();
	auto ctx = init(*tctx).unwrap();
	std::ignore = initGfx();
	std::ignore = initConsole(*ctx);
	setBgStatus(*ctx, 0, true);
	clearBg(*ctx, 0);
	auto const serr = ox::sfmt<ox::IString<23>>("Error code: {}", static_cast<int64_t>(err));
	consoleWrite(*ctx, 32 + 1,  1, "SADNESS...");
	consoleWrite(*ctx, 32 + 1,  4, "UNEXPECTED STATE:");
	consoleWrite(*ctx, 32 + 2,  6, panicMsg);
	if (err) {
		consoleWrite(*ctx, 32 + 2,  8, serr);
	}
	consoleWrite(*ctx, 32 + 1, 15, "PLEASE RESTART THE SYSTEM");
	// print to terminal if in mGBA
	oxErrf("\033[31;1;1mPANIC:\033[0m [{}:{}]: {}\n", src.file_name(), src.line(), panicMsg);
	if (err.msg) {
		oxErrf("\tError Message:\t{}\n", err.msg);
	}
	oxErrf("\tError Code:\t{}\n", static_cast<ErrorCode>(err));
	if (err.src.file_name() != nullptr) {
		oxErrf("\tError Location:\t{}:{}\n", err.src.file_name(), err.src.line());
	}
	abort();
}

}

OX_ALLOW_UNSAFE_BUFFERS_END
