/*
 * Copyright 2016 - 2023 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/tilesheet.hpp>

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

namespace nostalgia::core {

constexpr auto Scale = 1;

namespace renderer {

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::StringView bgvshadTmpl = R"glsl(
	{}
	in vec2 vTexCoord;
	in vec2 vPosition;
	in float vTileIdx;
	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,
				0.0, 1.0);
		fTexCoord = vec2(
			vTexCoord.x,
			vTexCoord.y * vTileHeight + vTileIdx * vTileHeight);
	})glsl";

constexpr ox::StringView bgfshadTmpl = R"glsl(
	{}
	out vec4 outColor;
	in vec2 fTexCoord;
	uniform sampler2D image;
	uniform vec2 fSrcImgSz;
	uniform vec4 fPalette[256];
	vec2 pixelSz;
	vec4 getColor(vec2 offset) {
		vec2 p = fTexCoord + pixelSz * offset;
		int idx = int(texture(image, p).rgb.r * 256);
		return fPalette[idx];
	}
	void main() {
		pixelSz = vec2(1, 1) / (fSrcImgSz);
		vec2 pixelCoord = floor(fTexCoord / pixelSz) * pixelSz;
		outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256)];
		//outColor = vec4(0.0, 0.7, 1.0, 1.0);
	})glsl";

constexpr ox::StringView spritevshadTmpl = R"glsl(
	{}
	in float vEnabled;
	in vec2 vTexCoord;
	in vec2 vPosition;
	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,
				0.0, 1.0);
		fTexCoord = vTexCoord * vec2(1, vTileHeight) * vec2(vEnabled, vEnabled);
	})glsl";

constexpr ox::StringView spritefshadTmpl = bgfshadTmpl;

[[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,
		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;
	const auto textureRowf = static_cast<float>(textureRow);
	const float L = flipX ? 1 : 0;
	const float R = flipX ? 0 : 1;
	const ox::Array<float, SpriteVertexVboLength> vertices {
			enabled,        x,        y, L, textureRowf + 1, // bottom left
			enabled, x + xmod,        y, R, textureRowf + 1, // bottom right
			enabled, x + xmod, y + ymod, R, textureRowf + 0, // top right
			enabled,        x, y + ymod, L, textureRowf + 0, // top left
	};
	memcpy(vbo, vertices.data(), sizeof(vertices));
	const ox::Array<GLuint, SpriteVertexEboLength> 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 *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;
	ox::Array<float, BgVertexVboLength> const vertices {
			       x,        y, 0, 1, textureTileIdx, // bottom left
			x + xmod,        y, 1, 1, textureTileIdx, // bottom right
			x + xmod, y + ymod, 1, 0, textureTileIdx, // top right
			       x, y + ymod, 0, 0, textureTileIdx, // 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(glutils::BufferSet &bs) noexcept {
	for (auto i = 0u; i < 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 * SpriteVertexVboRows, 0, 0, 0, 0, false, vbo, ebo);
	}
}

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

static void initSpritesBufferset(GLuint shader, glutils::BufferSet &bs) noexcept {
	// vao
	bs.vao = glutils::generateVertexArrayObject();
	glBindVertexArray(bs.vao);
	// vbo & ebo
	bs.vbo = glutils::generateBuffer();
	bs.ebo = glutils::generateBuffer();
	initSpriteBufferObjects(bs);
	glutils::sendVbo(bs);
	glutils::sendEbo(bs);
	// vbo layout
	auto const enabledAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vEnabled"));
	glEnableVertexAttribArray(enabledAttr);
	glVertexAttribPointer(enabledAttr, 1, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), nullptr);
	auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
	glEnableVertexAttribArray(posAttr);
	glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
	                      reinterpret_cast<void*>(1 * sizeof(float)));
	auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
	glEnableVertexAttribArray(texCoordAttr);
	glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
	                      reinterpret_cast<void*>(3 * sizeof(float)));
}

static void initBackgroundBufferset(
		GLuint shader,
		glutils::BufferSet &bg) noexcept {
	// vao
	bg.vao = glutils::generateVertexArrayObject();
	glBindVertexArray(bg.vao);
	// vbo & ebo
	bg.vbo = glutils::generateBuffer();
	bg.ebo = glutils::generateBuffer();
	initBackgroundBufferObjects(bg);
	glutils::sendVbo(bg);
	glutils::sendEbo(bg);
	// vbo layout
	auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
	glEnableVertexAttribArray(posAttr);
	glVertexAttribPointer(posAttr, 2, 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),
			reinterpret_cast<void*>(2 * sizeof(float)));
	auto const heightMultAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTileIdx"));
	glEnableVertexAttribArray(heightMultAttr);
	glVertexAttribPointer(
			heightMultAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
			reinterpret_cast<void*>(4 * 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 [wi, hi] = renderSz;
	const auto wf = static_cast<float>(wi);
	const auto hf = static_cast<float>(hi);
	glUniform1f(uniformXScale, hf / wf);
	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));
			drawBackground(cbb);
		}
	}
}

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(
		GLuint shaderPgrm,
		Palette const&pal,
		bool firstIsTransparent = false) noexcept {
	static constexpr std::size_t ColorCnt = 256;
	ox::Array<GLfloat, ColorCnt * 4> palette{};
	for (auto i = 0u; const auto c : pal.colors) {
		palette[i++] = redf(c);
		palette[i++] = greenf(c);
		palette[i++] = bluef(c);
		palette[i++] = 255;
	}
	if (firstIsTransparent) {
		palette[3] = 0;
	}
	glUseProgram(shaderPgrm);
	const auto uniformPalette = static_cast<GLint>(glGetUniformLocation(shaderPgrm, "fPalette"));
	glUniform4fv(uniformPalette, ColorCnt, palette.data());
}

static void loadBgPalette(Context &ctx, Palette const&pal) noexcept {
	loadPalette(ctx.bgShader, pal);
}

static void loadSpritePalette(Context &ctx, Palette const&pal) noexcept {
	loadPalette(ctx.spriteShader, pal, true);
}

static void loadBgTexture(
		Context &ctx,
		uint_t cbbIdx,
		const void *pixels,
		int w,
		int h) noexcept {
	oxTracef("nostalgia.core.gfx.gl", "loadBgTexture: { cbbIdx: {}, w: {}, h: {} }", cbbIdx, w, h);
	ctx.cbbs[cbbIdx].tex = createTexture(w, h, pixels);
}

static void loadSpriteTexture(
		Context &ctx,
		const void *pixels,
		int w,
		int h) noexcept {
	oxTracef("nostalgia.core.gfx.gl", "loadSpriteTexture: { w: {}, h: {} }", w, h);
	ctx.spriteBlocks.tex = createTexture(w, h, pixels);
}

}

ox::Error initGfx(
		Context &ctx,
		InitParams const&initParams) noexcept {
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	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 &bg : ctx.cbbs) {
		initBackgroundBufferset(ctx.bgShader, bg);
	}
	initSpritesBufferset(ctx.spriteShader, ctx.spriteBlocks);
	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> loadTileSheet(
		Context &ctx,
		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;
		}
	}
	renderer::loadSpriteTexture(ctx, pixels.data(), width, height);
	return TileSheetData{std::move(pixels), width, height};
}

ox::Error loadBgTileSheet(
		Context &ctx,
		uint_t cbb,
		ox::FileAddress const&tilesheetAddr,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
	oxRequire(palette, readObj<Palette>(kctx, paletteAddr ? paletteAddr : tilesheet->defaultPalette));
	oxRequire(tsd, loadTileSheet(ctx, *tilesheet).to([](TileSheetData const&t) -> TileSheetData {
		return {
			.pixels = resizeTileSheetData(t.pixels, t.size(), Scale),
			.width = t.width * Scale,
			.height = t.height * Scale,
		};
	}));
	renderer::loadBgTexture(ctx, cbb, tsd.pixels.data(), tsd.width, tsd.height);
	renderer::loadBgPalette(ctx, *palette);
	return {};
}

ox::Error loadSpriteTileSheet(
		Context &ctx,
		ox::FileAddress const&tilesheetAddr,
		ox::FileAddress const&paletteAddr) noexcept {
	auto &kctx = keelCtx(ctx.turbineCtx);
	oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
	oxRequire(palette, readObj<Palette>(kctx, paletteAddr ? paletteAddr : tilesheet->defaultPalette));
	oxRequire(tsd, loadTileSheet(ctx, *tilesheet));
	renderer::loadSpriteTexture(ctx, tsd.pixels.data(), tsd.width, tsd.height);
	renderer::loadSpritePalette(ctx, *palette);
	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);
	setBgCbb(ctx, 0, 0);
	return loadBgTileSheet(ctx, 0, TilesheetAddr, PaletteAddr);
}

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

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

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 clearTileLayer(Context &ctx, uint_t bgIdx) noexcept {
	auto &bg = ctx.cbbs[static_cast<std::size_t>(bgIdx)];
	initBackgroundBufferObjects(bg);
	bg.updated = true;
}

void hideSprite(Context &ctx, uint_t idx) noexcept {
	auto vbo = &ctx.spriteBlocks.vertices[idx * renderer::SpriteVertexVboLength];
	auto ebo = &ctx.spriteBlocks.elements[idx * renderer::SpriteVertexEboLength];
	renderer::setSpriteBufferObject(
			idx * renderer::SpriteVertexVboRows, 0, 0, 0, 0, false, vbo, ebo);
	ctx.spriteBlocks.updated = true;
}

void setSprite(
		Context &ctx,
		uint_t idx,
		int x,
		int y,
		uint_t tileIdx,
		uint_t spriteShape,
		uint_t spriteSize,
		uint_t flipX) noexcept {
	//oxTracef("nostalgia::core::gfx::gl", "setSprite(ctx, {}, {}, {}, {}, {}, {}, {})",
	//         idx, x, y, tileIdx, spriteShape, spriteSize, flipX);
	// 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
	};
	const auto dim = dimensions[(spriteShape << 2) | spriteSize];
	const auto uX = static_cast<int>(x) % 255;
	const auto uY = static_cast<int>(y + 8) % 255 - 8;
	auto i = 0u;
	const auto set = [&](int xIt, int yIt) {
		const auto fX = static_cast<float>(uX + xIt * 8) / 8;
		const auto fY = static_cast<float>(uY + yIt * 8) / 8;
		const auto cidx = idx + i;
		auto vbo = &ctx.spriteBlocks.vertices[cidx * renderer::SpriteVertexVboLength];
		auto ebo = &ctx.spriteBlocks.elements[cidx * renderer::SpriteVertexEboLength];
		renderer::setSpriteBufferObject(
			cidx * renderer::SpriteVertexVboRows,
			1,
			fX,
			fY,
			tileIdx + i,
			flipX,
			vbo,
			ebo);
		++i;
	};
	if (!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));
			}
		}
	} 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));
			}
		}
	}
	ctx.spriteBlocks.updated = true;
}

void setTile(
		Context &ctx,
		uint_t bgIdx,
		int column,
		int row,
		uint8_t tile) noexcept {
	oxTracef(
			"nostalgia.core.gfx.setTile",
			"bgIdx: {}, column: {}, row: {}, tile: {}",
			bgIdx, column, row, tile);
	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 &bg = ctx.cbbs[z];
	const auto vbo = &bg.vertices[i * renderer::BgVertexVboLength];
	const auto ebo = &bg.elements[i * renderer::BgVertexEboLength];
	renderer::setTileBufferObject(
			static_cast<uint_t>(i * renderer::BgVertexVboRows),
			static_cast<float>(x),
			static_cast<float>(y),
			static_cast<float>(tile),
			vbo,
			ebo);
	bg.updated = true;
}

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);
	glutils::clearScreen();
	renderer::drawBackgrounds(ctx, renderSz);
	if (ctx.spriteBlocks.tex) {
		renderer::drawSprites(ctx, renderSz);
	}
}

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

}

}
