/*
 * Copyright 2016 - 2023 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 <keel/media.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 {

struct BgCbbData {
	unsigned bpp = 4;
};
static ox::Array<BgCbbData, 4> g_cbbData;

constexpr auto GbaTileColumns = 32;
constexpr auto GbaTileRows = 32;

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;
	BgCbbData *cbbData = nullptr;
	ox::FileAddress defaultPalette;
	GbaPaletteTarget pal;
	volatile uint16_t *tileMap = nullptr;
};

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

constexpr ox::Error model(auto *io, ox::CommonPtrWith<GbaTileMapTarget> auto *t) noexcept {
	oxReturnError(io->template setTypeInfo<CompactTileSheet>());
	oxReturnError(io->field("bpp", &t->cbbData->bpp));
	oxReturnError(io->field("defaultPalette", &t->defaultPalette));
	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 OxError(0);
	};
	return io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
}

ox::Error initGfx(Context&, InitParams const&) noexcept {
	for (auto bgCtl = &REG_BG0CTL; bgCtl <= &REG_BG3CTL; bgCtl += 2) {
		teagba::bgSetSbb(bgCtl, 28);
	}
	return {};
}

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);
}

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

void setBgCbb(Context&, unsigned bgIdx, unsigned cbb) noexcept {
	setBgCbb(nullptr, bgIdx, cbb);
}

static ox::Error loadBgTileSheet(
		ox::MemFS const&rom,
		unsigned cbb,
		ox::FileAddress const&tilesheetAddr,
		ox::FileAddress const&paletteAddr) noexcept {
	oxRequire(tsStat, rom.stat(tilesheetAddr));
	oxRequire(ts, rom.directAccess(tilesheetAddr));
	GbaTileMapTarget target;
	target.pal.palette = MEM_BG_PALETTE;
	target.cbbData = &g_cbbData[cbb];
	target.tileMap = MEM_BG_TILES[cbb].data();
	oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
	// load external palette if available
	if (paletteAddr) {
		oxRequire(palStat, rom.stat(paletteAddr));
		oxRequire(pal, rom.directAccess(paletteAddr));
		oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &target.pal));
	}
	// update bpp of all bgs with the updated cbb
	const auto bpp = g_cbbData[cbb].bpp;
	teagba::iterateBgCtl([bpp, cbb](auto bgCtl) {
		if (teagba::bgCbb(bgCtl) == cbb) {
			teagba::bgSetBpp(bgCtl, bpp);
		}
	});
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned cbb,
		ox::FileAddress const&tilesheetAddr,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &rom = ctx.rom();
	return loadBgTileSheet(rom, cbb, tilesheetAddr, paletteAddr);
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		ox::FileAddress const&tilesheetAddr,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &rom = ctx.rom();
	oxRequire(tsStat, ctx.rom().stat(tilesheetAddr));
	oxRequire(ts, rom.directAccess(tilesheetAddr));
	GbaTileMapTarget target;
	target.pal.palette = MEM_SPRITE_PALETTE;
	target.tileMap = MEM_SPRITE_TILES;
	oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
	// load external palette if available
	if (paletteAddr) {
		oxRequire(palStat, ctx.rom().stat(paletteAddr));
		oxRequire(pal, rom.directAccess(paletteAddr));
		oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &target.pal));
	}
	return {};
}

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

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

ox::Error initConsole(Context &ctx) noexcept {
	constexpr ox::FileAddress TilesheetAddr = ox::StringLiteral("/TileSheets/Charset.ng");
	constexpr ox::FileAddress PaletteAddr = ox::StringLiteral("/Palettes/Charset.npal");
	setBgStatus(ctx, 0b0001);
	oxReturnError(loadBgTileSheet(ctx, 0, TilesheetAddr, PaletteAddr));
	setBgCbb(ctx, 0, 0);
	return {};
}

void puts(Context &ctx, int column, int row, ox::CRStringView str) noexcept {
	const auto col = static_cast<unsigned>(column);
	for (auto i = 0u; i < str.bytes(); i++) {
		const auto c = charMap[static_cast<std::size_t>(str[i])];
		setTile(ctx, 0, static_cast<int>(col + i), row, static_cast<uint8_t>(c));
	}
}

void setTile(Context&, unsigned bgIdx, int column, int row, uint8_t tile) noexcept {
	const auto tileIdx = static_cast<std::size_t>(row * GbaTileColumns + column);
	MEM_BG_MAP[bgIdx][tileIdx] = tile;
}

// Do NOT use Context in the GBA version of this function.
void clearTileLayer(Context&, unsigned bgIdx) noexcept {
	memset(MEM_BG_MAP[bgIdx].data(), 0, GbaTileRows * GbaTileColumns);
}

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

void setSprite(Context&,
               unsigned idx,
               int x,
               int y,
               unsigned tileIdx,
               unsigned spriteShape,
               unsigned spriteSize,
               unsigned flipX) noexcept {
	//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
	teagba::GbaSpriteAttrUpdate oa;
	oa.attr0 = static_cast<uint16_t>(
	           static_cast<uint16_t>(y & ox::onMask<uint8_t>(0b111'1111))
	         | (static_cast<uint16_t>(1) << 10) // enable alpha
	         | (static_cast<uint16_t>(spriteShape) << 14));
	oa.attr1 = static_cast<uint16_t>(
	           (static_cast<uint16_t>(x) & ox::onMask<uint8_t>(8))
	         | (static_cast<uint16_t>(flipX) << 12)
	         | (static_cast<uint16_t>(spriteSize) << 14));
	oa.attr2 = static_cast<uint16_t>(tileIdx & ox::onMask<uint16_t>(8));
	oa.idx = static_cast<uint16_t>(idx);
	teagba::addSpriteUpdate(oa);
}

}
