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

#include <ox/std/array.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/vec.hpp>

#include <keel/media.hpp>

#include <glutils/glutils.hpp>

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

#include "context.hpp"
#include "gfx.hpp"

namespace nostalgia::core {

namespace renderer {

constexpr auto Scale = 1;
constexpr auto PriorityScale = 0.01f;

Drawer::Drawer(Context &ctx) noexcept: m_ctx(ctx) {}

void Drawer::draw(turbine::Context &tctx) noexcept {
	core::gl::draw(m_ctx, turbine::getScreenSize(tctx));
}

constexpr ox::CStringView bgvshadTmpl = R"glsl(
	{}
	in vec2 vTexCoord;
	in vec3 vPosition;
	in float vTileIdx;
	in float vPalOffset;
	out vec2 fTexCoord;
	out float fPalOffset;
	uniform float vXScale;
	uniform float vTileHeight;
	uniform float vBgIdx;
	void main() {
		float xScaleInvert = 1.0 - vXScale;
		gl_Position = vec4(
				vPosition.x * vXScale - xScaleInvert,
				vPosition.y,
				vPosition.z - 0.001 * vBgIdx,
				1.0);
		fTexCoord = vec2(
			vTexCoord.x,
			vTexCoord.y * vTileHeight + vTileIdx * vTileHeight);
		fPalOffset = vPalOffset;
	})glsl";

constexpr ox::CStringView bgfshadTmpl = R"glsl(
	{}
	out vec4 outColor;
	in float fPalOffset;
	in vec2 fTexCoord;
	uniform sampler2D image;
	uniform vec2 fSrcImgSz;
	uniform vec4 fPalette[256];
	void main() {
		outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256) + int(fPalOffset)];
		//outColor = vec4(0.0, 0.7, 1.0, 1.0);
		if (outColor.a == 0) {
			discard;
		}
	})glsl";

constexpr ox::CStringView spritevshadTmpl = R"glsl(
	{}
	in float vEnabled;
	in vec3 vPosition;
	in vec2 vTexCoord;
	out vec2 fTexCoord;
	uniform float vXScale;
	uniform float vTileHeight;
	void main() {
		float xScaleInvert = 1.0 - vXScale;
		gl_Position = vec4(
				vPosition.x * vXScale - xScaleInvert,
				vPosition.y,
				// offset to ensure sprites draw on top of BGs by default
				vPosition.z - 0.004,
				1.0) * vEnabled;
		fTexCoord = vTexCoord * vec2(1, vTileHeight);
	})glsl";

constexpr ox::CStringView spritefshadTmpl = R"glsl(
	{}
	out vec4 outColor;
	in vec2 fTexCoord;
	uniform sampler2D image;
	uniform vec2 fSrcImgSz;
	uniform vec4 fPalette[256];
	void main() {
		outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256)];
		//outColor = vec4(0.0, 0.7, 1.0, 1.0);
		if (outColor.a == 0) {
			discard;
		}
	})glsl";

[[nodiscard]]
static constexpr auto bgVertexRow(uint_t x, uint_t y) noexcept {
	return y * TileRows + x;
}

static void setSpriteBufferObject(
		uint_t vi,
		float enabled,
		float x,
		float y,
		uint_t textureRow,
		uint_t flipX,
		uint_t priority,
		float *vbo,
		GLuint *ebo) noexcept {
	// don't worry, this memcpy gets optimized to something much more ideal
	constexpr float xmod = 0.1f;
	constexpr float ymod = 0.1f;
	x *= xmod;
	y *= -ymod;
	x -= 1.f;
	y += 1.f - ymod;
	auto const prif = static_cast<float>(priority) * PriorityScale;
	auto const textureRowf = static_cast<float>(textureRow);
	float const L = flipX ? 1 : 0;
	float const R = flipX ? 0 : 1;
	ox::Array<float, SpriteVertexVboLength> const vertices {
	// vEnabled| vPosition               | vTexCoord
	    enabled,        x,        y, prif, L, textureRowf + 1, // bottom left
	    enabled, x + xmod,        y, prif, R, textureRowf + 1, // bottom right
	    enabled, x + xmod, y + ymod, prif, R, textureRowf + 0, // top right
	    enabled,        x, y + ymod, prif, L, textureRowf + 0, // top left
	};
	memcpy(vbo, vertices.data(), sizeof(vertices));
	ox::Array<GLuint, SpriteVertexEboLength> const elms {
			vi + 0, vi + 1, vi + 2,
			vi + 2, vi + 3, vi + 0,
	};
	memcpy(ebo, elms.data(), sizeof(elms));
}

static void setTileBufferObject(
		uint_t vi,
		float x,
		float y,
		float textureTileIdx,
		float priority,
		float palOffset,
		bool flipX,
		bool flipY,
		float *vbo,
		GLuint *ebo) noexcept {
	// don't worry, this memcpy gets optimized to something much more ideal
	constexpr float ymod = 0.1f;
	constexpr float xmod = 0.1f;
	x *= xmod;
	y *= -ymod;
	x -= 1.0f;
	y += 1.0f - ymod;
	auto const prif = priority * PriorityScale;
	float const L = flipX ? 1 : 0;
	float const R = flipX ? 0 : 1;
	float const T = flipY ? 1 : 0;
	float const B = flipY ? 0 : 1;
	ox::Array<float, BgVertexVboLength> const vertices {
			       x,        y, prif, L, B, textureTileIdx, palOffset, // bottom left
			x + xmod,        y, prif, R, B, textureTileIdx, palOffset, // bottom right
			x + xmod, y + ymod, prif, R, T, textureTileIdx, palOffset, // top right
			       x, y + ymod, prif, L, T, textureTileIdx, palOffset, // top left
	};
	memcpy(vbo, vertices.data(), sizeof(vertices));
	ox::Array<GLuint, BgVertexEboLength> const elms {
			vi + 0, vi + 1, vi + 2,
			vi + 2, vi + 3, vi + 0,
	};
	memcpy(ebo, elms.data(), sizeof(elms));
}

static void initSpriteBufferObjects(Context &ctx, glutils::BufferSet &bs) noexcept {
	for (auto i = 0u; i < ctx.spriteCount; ++i) {
		auto vbo = &bs.vertices[i * static_cast<std::size_t>(SpriteVertexVboLength)];
		auto ebo = &bs.elements[i * static_cast<std::size_t>(SpriteVertexEboLength)];
		setSpriteBufferObject(i * static_cast<uint_t>(SpriteVertexVboRows) * ctx.blocksPerSprite, 0, 0, 0, 0, false, 0, vbo, ebo);
	}
}

static void initBackgroundBufferObjects(glutils::BufferSet &bs) noexcept {
	for (auto x = 0u; x < TileColumns; ++x) {
		for (auto y = 0u; y < TileRows; ++y) {
			const auto i = bgVertexRow(x, y);
			auto vbo = &bs.vertices[i * static_cast<std::size_t>(BgVertexVboLength)];
			auto ebo = &bs.elements[i * static_cast<std::size_t>(BgVertexEboLength)];
			setTileBufferObject(
				static_cast<uint_t>(i * BgVertexVboRows),
				static_cast<float>(x),
				static_cast<float>(y),
				0,
				0,
				0,
				false,
				false,
				vbo,
				ebo);
		}
	}
}

static void initSpritesBufferset(Context &ctx) noexcept {
	auto const shader = ctx.spriteShader.id;
	auto &bs = ctx.spriteBlocks;
	// vao
	bs.vao = glutils::generateVertexArrayObject();
	glBindVertexArray(bs.vao);
	// vbo & ebo
	bs.vbo = glutils::generateBuffer();
	bs.ebo = glutils::generateBuffer();
	initSpriteBufferObjects(ctx, bs);
	glutils::sendVbo(bs);
	glutils::sendEbo(bs);
	// vbo layout
	// in float vEnabled;
	auto const enabledAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vEnabled"));
	glEnableVertexAttribArray(enabledAttr);
	glVertexAttribPointer(enabledAttr, 1, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), nullptr);
	// in vec3 vPosition;
	auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
	glEnableVertexAttribArray(posAttr);
	glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
	                      std::bit_cast<void*>(uintptr_t{1 * sizeof(float)}));
	// in vec2 vTexCoord;
	auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
	glEnableVertexAttribArray(texCoordAttr);
	glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
	                      std::bit_cast<void*>(uintptr_t{4 * sizeof(float)}));
}

static void initBackgroundBufferset(
		GLuint shader,
		glutils::BufferSet &bs) noexcept {
	// vao
	bs.vao = glutils::generateVertexArrayObject();
	glBindVertexArray(bs.vao);
	// vbo & ebo
	bs.vbo = glutils::generateBuffer();
	bs.ebo = glutils::generateBuffer();
	initBackgroundBufferObjects(bs);
	glutils::sendVbo(bs);
	glutils::sendEbo(bs);
	// vbo layout
	auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
	glEnableVertexAttribArray(posAttr);
	glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float), nullptr);
	auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
	glEnableVertexAttribArray(texCoordAttr);
	glVertexAttribPointer(
			texCoordAttr, 2, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
			std::bit_cast<void*>(uintptr_t{3 * sizeof(float)}));
	auto const heightMultAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTileIdx"));
	glEnableVertexAttribArray(heightMultAttr);
	glVertexAttribPointer(
			heightMultAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
			std::bit_cast<void*>(uintptr_t{5 * sizeof(float)}));
	auto const palBankAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPalOffset"));
	glEnableVertexAttribArray(palBankAttr);
	glVertexAttribPointer(
			palBankAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
			std::bit_cast<void*>(uintptr_t{6 * sizeof(float)}));
}

static glutils::GLTexture createTexture(
		GLsizei w,
		GLsizei h,
		const void *pixels) noexcept {
	GLuint texId = 0;
	glGenTextures(1, &texId);
	glutils::GLTexture tex(texId);
	tex.width = w;
	tex.height = h;
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, tex.id);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.width, tex.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	return tex;
}

static void drawBackground(CBB &cbb) noexcept {
	glBindVertexArray(cbb.vao);
	if (cbb.updated) {
		cbb.updated = false;
		glutils::sendVbo(cbb);
	}
	glBindTexture(GL_TEXTURE_2D, cbb.tex);
	glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(cbb.elements.size()), GL_UNSIGNED_INT, nullptr);
}

static void drawBackgrounds(
		Context &ctx,
		ox::Size const&renderSz) noexcept {
	// load background shader and its uniforms
	glUseProgram(ctx.bgShader);
	const auto uniformSrcImgSz = glGetUniformLocation(ctx.bgShader, "fSrcImgSz");
	const auto uniformXScale = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vXScale"));
	const auto uniformTileHeight = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vTileHeight"));
	const auto uniformBgIdx = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vBgIdx"));
	const auto [wi, hi] = renderSz;
	const auto wf = static_cast<float>(wi);
	const auto hf = static_cast<float>(hi);
	glUniform1f(uniformXScale, hf / wf);
	auto bgIdx = 0.f;
	for (const auto &bg : ctx.backgrounds) {
		if (bg.enabled) {
			auto &cbb = ctx.cbbs[bg.cbbIdx];
			const auto tileRows = cbb.tex.height / (TileHeight * Scale);
			glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
			glUniform2f(
					uniformSrcImgSz,
					static_cast<float>(cbb.tex.width),
					static_cast<float>(cbb.tex.height));
			glUniform1f(uniformBgIdx, bgIdx);
			drawBackground(cbb);
			++bgIdx;
		}
	}
}

static void drawSprites(Context &ctx, ox::Size const&renderSz) noexcept {
	glUseProgram(ctx.spriteShader);
	auto &sb = ctx.spriteBlocks;
	const auto uniformXScale = glGetUniformLocation(ctx.bgShader, "vXScale");
	const auto uniformTileHeight = glGetUniformLocation(ctx.spriteShader, "vTileHeight");
	const auto [wi, hi] = renderSz;
	const auto wf = static_cast<float>(wi);
	const auto hf = static_cast<float>(hi);
	glUniform1f(uniformXScale, hf / wf);
	// update vbo
	glBindVertexArray(sb.vao);
	if (sb.updated) {
		sb.updated = false;
		glutils::sendVbo(sb);
	}
	// set vTileHeight uniform
	const auto tileRows = sb.tex.height / (TileHeight * Scale);
	glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
	// draw
	glBindTexture(GL_TEXTURE_2D, sb.tex);
	glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(sb.elements.size()), GL_UNSIGNED_INT, nullptr);
}

static void loadPalette(
		ox::Array<GLfloat, 1024> &palette,
		size_t palOffset,
		GLuint shaderPgrm,
		Palette const&pal,
		size_t page = 0) noexcept {
	static constexpr std::size_t ColorCnt = 256;
	for (auto i = palOffset; auto const c : pal.pages[page]) {
		palette[i++] = redf(c);
		palette[i++] = greenf(c);
		palette[i++] = bluef(c);
		palette[i++] = 255;
	}
	// make first color transparent
	palette[palOffset + 3] = 0;
	glUseProgram(shaderPgrm);
	const auto uniformPalette = static_cast<GLint>(glGetUniformLocation(shaderPgrm, "fPalette"));
	glUniform4fv(uniformPalette, ColorCnt, palette.data());
}

static void setSprite(
		Context &ctx,
		uint_t const idx,
		Sprite const&s) noexcept {
	// Tonc Table 8.4
	static constexpr ox::Array<ox::Vec<uint_t>, 12> dimensions{
			// col 0
			{1, 1}, // 0, 0
			{2, 2}, // 0, 1
			{4, 4}, // 0, 2
			{8, 8}, // 0, 3
			// col 1
			{2, 1}, // 1, 0
			{4, 1}, // 1, 1
			{4, 2}, // 1, 2
			{8, 4}, // 1, 3
			// col 2
			{1, 1}, // 2, 0
			{1, 4}, // 2, 1
			{2, 4}, // 2, 2
			{4, 8}, // 2, 3
	};
	oxAssert(idx < ctx.spriteStates.size(), "overflow");
	auto const dim = dimensions[(s.spriteShape << 2) | s.spriteSize];
	auto const uX = static_cast<int>(s.x) % 255;
	auto const uY = static_cast<int>(s.y + 8) % 255 - 8;
	oxAssert(1 < ctx.spriteBlocks.vertices.size(), "vbo overflow");
	oxAssert(1 < ctx.spriteBlocks.elements.size(), "ebo overflow");
	const auto spriteVboSz = ctx.blocksPerSprite * renderer::SpriteVertexVboLength;
	const auto spriteEboSz = ctx.blocksPerSprite * renderer::SpriteVertexEboLength;
	auto const vboBase = spriteVboSz * idx;
	auto const eboBase = spriteEboSz * idx;
	auto i = 0u;
	const auto set = [&](int xIt, int yIt, bool enabled) {
		auto const fX = static_cast<float>(uX + xIt * 8) / 8;
		auto const fY = static_cast<float>(uY + yIt * 8) / 8;
		auto const vboIdx = vboBase + renderer::SpriteVertexVboLength * i;
		auto const eboIdx = eboBase + renderer::SpriteVertexEboLength * i;
		oxAssert(vboIdx < ctx.spriteBlocks.vertices.size(), "vbo overflow");
		oxAssert(eboIdx < ctx.spriteBlocks.elements.size(), "ebo overflow");
		auto const vbo = &ctx.spriteBlocks.vertices[vboIdx];
		auto const ebo = &ctx.spriteBlocks.elements[eboIdx];
		renderer::setSpriteBufferObject(
				static_cast<uint_t>(vboIdx),
				enabled,
				fX,
				fY,
				s.tileIdx + i,
				s.flipX,
				s.priority & 0b11,
				vbo,
				ebo);
		++i;
	};
	if (!s.flipX) {
		for (auto yIt = 0; yIt < static_cast<int>(dim.y); ++yIt) {
			for (auto xIt = 0u; xIt < dim.x; ++xIt) {
				set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
			}
		}
	} else {
		for (auto yIt = 0u; yIt < dim.y; ++yIt) {
			for (auto xIt = dim.x - 1; xIt < ~0u; --xIt) {
				set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
			}
		}
	}
	// clear remaining blocks in the sprite
	for (; i < ctx.blocksPerSprite; ++i) {
		set(0, 0, false);
	}
	ctx.spriteBlocks.updated = true;
}

}

ox::Error initGfx(
		Context &ctx,
		InitParams const&initParams) noexcept {
	const auto bgVshad = ox::sfmt(renderer::bgvshadTmpl, gl::GlslVersion);
	const auto bgFshad = ox::sfmt(renderer::bgfshadTmpl, gl::GlslVersion);
	const auto spriteVshad = ox::sfmt(renderer::spritevshadTmpl, gl::GlslVersion);
	const auto spriteFshad = ox::sfmt(renderer::spritefshadTmpl, gl::GlslVersion);
	oxReturnError(glutils::buildShaderProgram(bgVshad, bgFshad).moveTo(ctx.bgShader));
	oxReturnError(
			glutils::buildShaderProgram(spriteVshad, spriteFshad).moveTo(ctx.spriteShader));
	for (auto &cbb : ctx.cbbs) {
		initBackgroundBufferset(ctx.bgShader, cbb);
	}
	renderer::initSpritesBufferset(ctx);
	if (initParams.glInstallDrawer) {
		turbine::gl::addDrawer(ctx.turbineCtx, &ctx.drawer);
	}
	return {};
}

void shutdownGfx(Context &ctx) noexcept {
	turbine::gl::removeDrawer(ctx.turbineCtx, &ctx.drawer);
}

struct TileSheetData {
	ox::Vector<uint32_t> pixels;
	int width = 0;
	int height = 0;
	[[nodiscard]]
	constexpr ox::Size size() const noexcept {
		return {width, height};
	}
};

static ox::Result<TileSheetData> normalizeTileSheet(
		CompactTileSheet const&tilesheet) noexcept {
	const uint_t bytesPerTile = tilesheet.bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
	const auto tiles = tilesheet.pixels.size() / bytesPerTile;
	constexpr int width = 8;
	const int height = 8 * static_cast<int>(tiles);
	ox::Vector<uint32_t> pixels;
	if (bytesPerTile == 64) { // 8 BPP
		pixels.resize(tilesheet.pixels.size());
		for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
			pixels[i] = tilesheet.pixels[i];
		}
	} else { // 4 BPP
		pixels.resize(tilesheet.pixels.size() * 2);
		for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
			pixels[i * 2 + 0] = tilesheet.pixels[i] & 0xF;
			pixels[i * 2 + 1] = tilesheet.pixels[i] >> 4;
		}
	}
	return TileSheetData{std::move(pixels), width, height};
}

ox::Error loadBgPalette(
		Context &ctx,
		size_t palBank,
		Palette const&palette,
		size_t page) noexcept {
	renderer::loadPalette(ctx.bgPalette, palBank * 16 * 4, ctx.bgShader, palette, page);
	return {};
}

ox::Error loadSpritePalette(
		Context &ctx,
		Palette const&palette,
		size_t page) noexcept {
	ox::Array<GLfloat, 1024> pal;
	renderer::loadPalette(pal, 0, ctx.spriteShader, palette, page);
	return {};
}

ox::Error loadBgPalette(
		Context &ctx,
		size_t palBank,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
	renderer::loadPalette(ctx.bgPalette, palBank * 16 * 4, ctx.bgShader, *palette);
	return {};
}

ox::Error loadSpritePalette(
		Context &ctx,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
	ox::Array<GLfloat, 1024> pal;
	renderer::loadPalette(pal, 0, ctx.spriteShader, *palette);
	return {};
}

static ox::Result<TileSheetData> buildSetTsd(
		Context &ctx,
		TileSheetSet const&set) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	TileSheetData setTsd;
	setTsd.width = TileWidth;
	for (auto const&entry : set.entries) {
		oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, entry.tilesheet));
		oxRequire(tsd, normalizeTileSheet(*tilesheet));
		for (auto const&s : entry.sections) {
			auto const size = s.tiles * PixelsPerTile;
			for (auto i = 0; i < size; ++i) {
				auto const srcIdx = static_cast<size_t>(i) + static_cast<size_t>(s.begin * PixelsPerTile);
				setTsd.pixels.push_back(tsd.pixels[srcIdx]);
			}
			setTsd.height += TileHeight * s.tiles;
		}
	}
	return setTsd;
}

ox::Error loadBgTileSheet(
		Context &ctx,
		uint_t cbb,
		ox::FileAddress const&tilesheetAddr,
		ox::Optional<unsigned> const&paletteBank) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
	oxRequire(tsd, normalizeTileSheet(*tilesheet));
	oxTracef("nostalgia.core.gfx.gl", "loadBgTexture: { cbbIdx: {}, w: {}, h: {} }", cbb, tsd.width, tsd.height);
	ctx.cbbs[cbb].tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
	if (paletteBank.has_value() && tilesheet->defaultPalette) {
		oxReturnError(loadBgPalette(ctx, *paletteBank, tilesheet->defaultPalette));
	}
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned cbb,
		TileSheetSet const&set) noexcept {
	oxRequire(setTsd, buildSetTsd(ctx, set));
	ctx.cbbs[cbb].tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
	return {};
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		ox::FileAddress const&tilesheetAddr,
		bool loadDefaultPalette) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
	oxRequire(tsd, normalizeTileSheet(*tilesheet));
	oxTracef("nostalgia.core.gfx.gl", "loadSpriteTexture: { w: {}, h: {} }", tsd.width, tsd.height);
	ctx.spriteBlocks.tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
	if (loadDefaultPalette) {
		oxReturnError(loadSpritePalette(ctx, tilesheet->defaultPalette));
	}
	return {};
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		TileSheetSet const&set) noexcept {
	oxRequire(setTsd, buildSetTsd(ctx, set));
	ctx.spriteBlocks.tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
	return {};
}

void setBgTile(
		Context &ctx,
		uint_t bgIdx,
		int column,
		int row,
		BgTile const&tile) noexcept {
	oxTracef(
			"nostalgia.core.gfx.setBgTile",
			"bgIdx: {}, column: {}, row: {}, tile: {}, palBank: {}",
			bgIdx, column, row, tile.tileIdx, tile.palBank);
	const auto z = static_cast<uint_t>(bgIdx);
	const auto y = static_cast<uint_t>(row);
	const auto x = static_cast<uint_t>(column);
	const auto i = renderer::bgVertexRow(x, y);
	auto &cbb = ctx.cbbs[z];
	const auto vbo = &cbb.vertices[i * renderer::BgVertexVboLength];
	const auto ebo = &cbb.elements[i * renderer::BgVertexEboLength];
	auto &bg = ctx.backgrounds[bgIdx];
	renderer::setTileBufferObject(
			static_cast<uint_t>(i * renderer::BgVertexVboRows),
			static_cast<float>(x),
			static_cast<float>(y),
			static_cast<float>(tile.tileIdx),
			bg.priority,
			static_cast<float>(tile.palBank * 16),
			tile.flipX,
			tile.flipY,
			vbo,
			ebo);
	cbb.updated = true;
}

void clearBg(Context &ctx, uint_t bgIdx) noexcept {
	auto &cbb = ctx.cbbs[static_cast<std::size_t>(bgIdx)];
	initBackgroundBufferObjects(cbb);
	cbb.updated = true;
	auto &bg = ctx.backgrounds[static_cast<std::size_t>(bgIdx)];
	bg.priority = 0;
}

uint8_t bgStatus(Context &ctx) noexcept {
	uint8_t out = 0;
	for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
		out |= static_cast<uint8_t>(static_cast<uint_t>(ctx.backgrounds[i].enabled) << i);
	}
	return out;
}

void setBgStatus(Context &ctx, uint32_t status) noexcept {
	for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
		ctx.backgrounds[i].enabled = (status >> i) & 1;
	}
}

bool bgStatus(Context &ctx, uint_t bg) noexcept {
	return ctx.backgrounds[bg].enabled;
}

void setBgStatus(Context&ctx, uint_t bg, bool status) noexcept {
	ctx.backgrounds[bg].enabled = status;
}

void setBgBpp(Context&, unsigned, unsigned) noexcept {}

void setBgCbb(Context &ctx, uint_t bgIdx, uint_t cbbIdx) noexcept {
	auto &bg = ctx.backgrounds[bgIdx];
	bg.cbbIdx = cbbIdx;
}

void setBgPriority(Context &ctx, uint_t bgIdx, uint_t priority) noexcept {
	auto &bg = ctx.backgrounds[bgIdx];
	bg.priority = static_cast<float>(priority & 0b11);
}

void hideSprite(Context &ctx, uint_t idx) noexcept {
	auto &s = ctx.spriteStates[idx];
	s.enabled = false;
	renderer::setSprite(ctx, idx, s);
}

void showSprite(Context &ctx, uint_t idx) noexcept {
	auto &s = ctx.spriteStates[idx];
	s.enabled = true;
	renderer::setSprite(ctx, idx, s);
}

void setSprite(Context &ctx, uint_t idx, Sprite const&sprite) noexcept {
	auto &s = ctx.spriteStates[idx];
	s = sprite;
	renderer::setSprite(ctx, idx, s);
}

uint_t spriteCount(Context &ctx) noexcept {
	return ctx.spriteCount;
}

namespace gl {

ox::Size drawSize(int scale) noexcept {
	return {240 * scale, 160 * scale};
}

void draw(core::Context &ctx, ox::Size const&renderSz) noexcept {
	glViewport(0, 0, renderSz.width, renderSz.height);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glClearColor(0, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	renderer::drawBackgrounds(ctx, renderSz);
	if (ctx.spriteBlocks.tex) {
		renderer::drawSprites(ctx, renderSz);
	}
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_BLEND);
}

void draw(core::Context &ctx, int scale) noexcept {
	draw(ctx, drawSize(scale));
}

}

}
