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

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

#include <keel/media.hpp>

#include <glutils/glutils.hpp>

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

namespace nostalgia::gfx {

namespace renderer {

constexpr uint64_t TileRows = 128;
constexpr uint64_t TileColumns = 128;
constexpr uint64_t TileCount = TileRows * TileColumns;
constexpr uint64_t BgVertexVboRows = 4;
constexpr uint64_t BgVertexVboRowLength = 7;
constexpr uint64_t BgVertexVboLength = BgVertexVboRows * BgVertexVboRowLength;
constexpr uint64_t BgVertexEboLength = 6;
constexpr uint64_t SpriteVertexVboRows = 4;
constexpr uint64_t SpriteVertexVboRowLength = 6;
constexpr uint64_t SpriteVertexVboLength = SpriteVertexVboRows * SpriteVertexVboRowLength;
constexpr uint64_t SpriteVertexEboLength = 6;

struct CBB: glutils::BufferSet {
	bool updated = false;
	ox::Array<uint32_t, 32768> pixels;
	constexpr CBB() noexcept {
		vertices.resize(TileCount * BgVertexVboLength);
		elements.resize(TileCount * BgVertexEboLength);
	}
};

struct SpriteBlockset: glutils::BufferSet {
	bool updated = false;
	constexpr SpriteBlockset(uint64_t spriteCount, uint64_t blocksPerSprite) noexcept {
		vertices.resize(spriteCount * SpriteVertexVboLength * blocksPerSprite);
		elements.resize(spriteCount * SpriteVertexEboLength * blocksPerSprite);
	}
};

struct Background {
	float priority = 0;
	bool enabled = false;
	unsigned cbbIdx = 0;
};

}

class Context {
	public:
		class Drawer final: public turbine::gl::Drawer {
			private:
				Context &m_ctx;
			public:
				explicit Drawer(Context &ctx) noexcept: m_ctx{ctx} {}
				void draw(turbine::Context &tctx) noexcept override {
					gl::draw(m_ctx, turbine::getScreenSize(tctx));
				}
		};

		turbine::Context &turbineCtx;
		glutils::GLProgram bgShader;
		glutils::GLProgram spriteShader;
		ox::Array<renderer::CBB, 4> cbbs;
		renderer::SpriteBlockset spriteBlocks;
		ox::Array<Sprite, 128> spriteStates;
		ox::Array<GLfloat, 1024> bgPalette;
		ox::Array<renderer::Background, 4> backgrounds;
		Drawer drawer;
		uint_t spriteCount = 0;
		uint_t blocksPerSprite = 0;

		explicit Context(turbine::Context &tctx, InitParams const &params) noexcept:
			turbineCtx{tctx},
			spriteBlocks{params.glSpriteCount, params.glBlocksPerSprite},
			drawer{*this},
			spriteCount{params.glSpriteCount},
			blocksPerSprite{params.glBlocksPerSprite} {
		}
		Context(Context const&) = delete;
		Context &operator=(Context const&) = delete;
		~Context() noexcept {
			turbine::gl::removeDrawer(turbineCtx, &drawer);
		}
};

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


namespace renderer {

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

static constexpr ox::StringLiteral 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"};

static constexpr ox::StringLiteral 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"};

static constexpr ox::StringLiteral 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"};

static constexpr ox::StringLiteral 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 const x, uint_t const y) noexcept {
	return y * TileRows + x;
}

static void setSpriteBufferObject(
		uint_t const vi,
		float const enabled,
		float x,
		float y,
		uint_t const textureRow,
		uint_t const flipX,
		uint_t const priority,
		ox::Span<float> const vbo,
		ox::Span<GLuint> const 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
	};
	ox::spancpy<float>(vbo, vertices);
	ox::Array<GLuint, SpriteVertexEboLength> const elms {
		vi + 0, vi + 1, vi + 2,
		vi + 2, vi + 3, vi + 0,
	};
	ox::spancpy<GLuint>(ebo, elms);
}

static void setTileBufferObject(
		uint_t const vi,
		float x,
		float y,
		float const textureTileIdx,
		float const priority,
		float const palOffset,
		bool const flipX,
		bool const flipY,
		ox::Span<float> const vbo,
		ox::Span<GLuint> const 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
	};
	ox::spancpy<float>(vbo, vertices);
	ox::Array<GLuint, BgVertexEboLength> const elms {
		vi + 0, vi + 1, vi + 2,
		vi + 2, vi + 3, vi + 0,
	};
	ox::spancpy<GLuint>(ebo, elms);
}

static void initSpriteBufferObjects(Context const &ctx, glutils::BufferSet &bs) noexcept {
	for (auto i = 0u; i < ctx.spriteCount; ++i) {
		auto const vbo = ox::Span{bs.vertices}
			+ i * static_cast<std::size_t>(SpriteVertexVboLength);
		auto const ebo = ox::Span{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) {
			auto const i = bgVertexRow(x, y);
			auto const vbo = ox::Span{bs.vertices}
				+ i * static_cast<std::size_t>(BgVertexVboLength);
			auto const ebo = ox::Span{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)}));
	glBindVertexArray(0);
}

static void initBackgroundBufferset(
		GLuint const 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)}));
	glBindVertexArray(0);
}

static glutils::GLTexture createTexture(
		GLsizei const w,
		GLsizei const h,
		void const *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);
	glBindVertexArray(0);
}

static void drawBackgrounds(
		Context &ctx,
		ox::Size const &renderSz) noexcept {
	// load background shader and its uniforms
	glUseProgram(ctx.bgShader);
	auto const uniformSrcImgSz = glGetUniformLocation(ctx.bgShader, "fSrcImgSz");
	auto const uniformXScale = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vXScale"));
	auto const uniformTileHeight = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vTileHeight"));
	auto const uniformBgIdx = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vBgIdx"));
	auto const [wi, hi] = renderSz;
	auto const wf = static_cast<float>(wi);
	auto const hf = static_cast<float>(hi);
	glUniform1f(uniformXScale, hf / wf);
	auto bgIdx = 0.f;
	for (auto const &bg : ctx.backgrounds) {
		if (bg.enabled) {
			auto &cbb = ctx.cbbs[bg.cbbIdx];
			auto const 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;
	auto const uniformXScale = glGetUniformLocation(ctx.bgShader, "vXScale");
	auto const uniformTileHeight = glGetUniformLocation(ctx.spriteShader, "vTileHeight");
	auto const [wi, hi] = renderSz;
	auto const wf = static_cast<float>(wi);
	auto const 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
	auto const 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);
	glBindVertexArray(0);
}

static void loadPalette(
		ox::Array<GLfloat, 1024> &palette,
		size_t const palOffset,
		GLuint const shaderPgrm,
		CompactPalette const &pal,
		size_t const 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);
	auto const 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
	struct Sz { uint_t x{}, y{}; };
	static constexpr ox::Array<Sz, 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");
	auto const spriteVboSz = ctx.blocksPerSprite * renderer::SpriteVertexVboLength;
	auto const spriteEboSz = ctx.blocksPerSprite * renderer::SpriteVertexEboLength;
	auto const vboBase = spriteVboSz * idx;
	auto const eboBase = spriteEboSz * idx;
	auto i = 0u;
	auto const 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 = ox::Span{ctx.spriteBlocks.vertices} + vboIdx;
		auto const ebo = ox::Span{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 = 0u; yIt < 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::Result<ox::UPtr<Context>> init(turbine::Context &tctx, InitParams const &params) noexcept {
	auto ctx = ox::make_unique<Context>(tctx, params);
	auto const bgVshad = ox::sfmt(renderer::bgvshadTmpl, gl::GlslVersion);
	auto const bgFshad = ox::sfmt(renderer::bgfshadTmpl, gl::GlslVersion);
	auto const spriteVshad = ox::sfmt(renderer::spritevshadTmpl, gl::GlslVersion);
	auto const spriteFshad = ox::sfmt(renderer::spritefshadTmpl, gl::GlslVersion);
	OX_RETURN_ERROR(glutils::buildShaderProgram(bgVshad, bgFshad).moveTo(ctx->bgShader));
	OX_RETURN_ERROR(
			glutils::buildShaderProgram(spriteVshad, spriteFshad).moveTo(ctx->spriteShader));
	for (auto &cbb : ctx->cbbs) {
		initBackgroundBufferset(ctx->bgShader, cbb);
	}
	renderer::initSpritesBufferset(*ctx);
	if (params.glInstallDrawer) {
		turbine::gl::addDrawer(ctx->turbineCtx, &ctx->drawer);
	}
	return ctx;
}

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

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

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

static ox::Result<TileSheetData> buildSetTsd(
		Context const &ctx,
		TileSheetSet const &set) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	TileSheetData setTsd;
	setTsd.width = TileWidth;
	for (auto const &entry : set.entries) {
		OX_REQUIRE(tilesheet, readObj<CompactTileSheet>(kctx, entry.tilesheet));
		OX_REQUIRE(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;
}

static void copyPixels(
		CompactTileSheet const &ts,
		ox::Span<uint32_t> const dst,
		size_t const srcPxIdx,
		size_t const pxlCnt) noexcept {
	size_t idx{};
	if (ts.bpp == 4) {
		for (size_t i = 0; i < pxlCnt; i += 2) {
			auto const [a, b] = get2Pixels4Bpp(ts, i + srcPxIdx);
			dst[idx++] = a;
			dst[idx++] = b;
		}
	} else if (ts.bpp == 8) {
		for (size_t i = 0; i < pxlCnt; i += 2) {
			auto const [a, b] = get2Pixels8Bpp(ts, i + srcPxIdx);
			dst[idx++] = a;
			dst[idx++] = b;
		}
	}
}

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

void clearCbbs(Context &ctx) noexcept {
	for (unsigned i = 0 ; i < ctx.cbbs.size(); ++i) {
		clearCbb(ctx, i);
	}
}

ox::Error loadBgTileSheet(
		Context &ctx,
		unsigned const cbb,
		CompactTileSheet const &ts,
		size_t const dstTileIdx,
		size_t const srcTileIdx,
		size_t const tileCnt) noexcept {
	auto &cbbPxls = ctx.cbbs[cbb].pixels;
	auto const bytesPerTile = static_cast<uint64_t>(PixelsPerTile / (1 + (ts.bpp == 4)));
	auto const pxlCnt = tileCnt * PixelsPerTile;
	auto const srcPxIdx = srcTileIdx * PixelsPerTile;
	auto const dstPxIdx = dstTileIdx * PixelsPerTile;
	if (dstPxIdx + pxlCnt >= cbbPxls.size()) {
		return ox::Error(1, "video mem dst overflow");
	}
	auto const dst = ox::Span{cbbPxls} + dstPxIdx;
	copyPixels(ts, dst, srcPxIdx, pxlCnt);
	auto const cbbTiles = cbbPxls.size() / bytesPerTile;
	int constexpr cbbWidth = 8;
	int const cbbHeight = 8 * static_cast<int>(cbbTiles);
	ctx.cbbs[cbb].tex = renderer::createTexture(cbbWidth, cbbHeight, cbbPxls.data());
	return {};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		uint_t const cbb,
		CompactTileSheet const &ts,
		ox::Optional<unsigned> const &paletteBank) noexcept {
	auto const bytesPerTile = static_cast<uint64_t>(PixelsPerTile / (1 + (ts.bpp == 4)));
	auto const tiles = ts.pixels.size() / bytesPerTile;
	OX_RETURN_ERROR(loadBgTileSheet(ctx, cbb, ts, 0, 0, tiles));
	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 {
	OX_REQUIRE(setTsd, buildSetTsd(ctx, set));
	ctx.cbbs[cbb].tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
	return {};
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		CompactTileSheet const &ts,
		bool const loadDefaultPalette) noexcept {
	OX_REQUIRE(tsd, normalizeTileSheet(ts));
	oxTracef("nostalgia.gfx.gl", "loadSpriteTexture: { w: {}, h: {} }", tsd.width, tsd.height);
	ctx.spriteBlocks.tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
	if (loadDefaultPalette) {
		OX_RETURN_ERROR(loadSpritePalette(ctx, ts.defaultPalette));
	}
	return {};
}

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

void setBgTile(
		Context &ctx,
		uint_t const bgIdx,
		int const column,
		int const row,
		BgTile const &tile) noexcept {
	oxTracef(
			"nostalgia.gfx.setBgTile",
			"bgIdx: {}, column: {}, row: {}, tile: {}, palBank: {}",
			bgIdx, column, row, tile.tileIdx, tile.palBank);
	auto const z = static_cast<uint_t>(bgIdx);
	auto const y = static_cast<uint_t>(row);
	auto const x = static_cast<uint_t>(column);
	auto const i = renderer::bgVertexRow(x, y);
	auto &cbb = ctx.cbbs[z];
	auto const vbo = ox::Span{cbb.vertices} + i * renderer::BgVertexVboLength;
	auto const ebo = ox::Span{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 const 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 const 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 const bg) noexcept {
	return ctx.backgrounds[bg].enabled;
}

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

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

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

void setBgOffset(Context&, uint16_t const, int16_t const, int16_t const) noexcept {
}

void scrollBgOffset(Context&, uint16_t const, int16_t const, int16_t const) noexcept {
}

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

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

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

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

namespace gl {

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

void draw(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(Context &ctx, int const scale) noexcept {
	draw(ctx, drawSize(scale));
}

}

}
