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

#pragma once

#include <ox/std/array.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>

#include <nostalgia/geo/point.hpp>
#include <nostalgia/geo/size.hpp>

#include "color.hpp"
#include "context.hpp"
#include "ptidxconv.hpp"

namespace nostalgia::core {

extern ox::Array<char, 128> charMap;

class Drawer {
	public:
		virtual ~Drawer() = default;

		virtual void draw(Context*) noexcept = 0;
};

enum class TileSheetSpace {
	Background,
	Sprite
};

struct NostalgiaPalette {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaPalette";
	static constexpr auto TypeVersion = 1;
	ox::Vector<Color16> colors = {};
};

struct Palette {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
	static constexpr auto TypeVersion = 1;
	ox::Vector<Color16> colors = {};
	[[nodiscard]]
	constexpr Color16 color(auto idx) const noexcept {
		if (idx < colors.size()) [[likely]] {
			return colors[idx];
		}
		return 0;
	}
};

// Predecessor to TileSheet, kept for backward compatibility
struct NostalgiaGraphic {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
	static constexpr auto TypeVersion = 1;
	int8_t bpp = 0;
	// rows and columns are really only used by TileSheetEditor
	int rows = 1;
	int columns = 1;
	ox::FileAddress defaultPalette;
	Palette pal;
	ox::Vector<uint8_t> pixels = {};
};

struct TileSheet {
	using SubSheetIdx = ox::Vector<std::size_t, 4>;

	struct SubSheet {
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
		static constexpr auto TypeVersion = 1;
		ox::String name;
		int columns = 0;
		int rows = 0;
		ox::Vector<SubSheet> subsheets;
		ox::Vector<uint8_t> pixels;

		constexpr SubSheet() noexcept = default;
		constexpr SubSheet(const SubSheet &other) noexcept {
			name      = other.name;
			columns   = other.columns;
			rows      = other.rows;
			subsheets = other.subsheets;
			pixels    = other.pixels;
		}
		constexpr SubSheet(SubSheet &&other) noexcept {
			name      = std::move(other.name);
			columns   = other.columns;
			rows      = other.rows;
			subsheets = std::move(other.subsheets);
			pixels    = std::move(other.pixels);
			other.name = "";
			other.columns = 0;
			other.rows = 0;
		}
		constexpr SubSheet(const char *pName, int pColumns, int pRows, int bpp) noexcept:
			name(pName), columns(pColumns), rows(pRows),
            pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
		}
		constexpr SubSheet(const char *pName, int pColumns, int pRows, ox::Vector<uint8_t> pPixels) noexcept:
			name(pName), columns(pColumns), rows(pRows), pixels(std::move(pPixels)) {
		}

		constexpr SubSheet &operator=(const SubSheet &other) noexcept {
			name      = other.name;
			columns   = other.columns;
			rows      = other.rows;
			subsheets = other.subsheets;
			pixels    = other.pixels;
			return *this;
		}

		constexpr SubSheet &operator=(SubSheet &&other) noexcept {
			name      = std::move(other.name);
			columns   = other.columns;
			rows      = other.rows;
			subsheets = std::move(other.subsheets);
			pixels    = std::move(other.pixels);
			return *this;
		}

		[[nodiscard]]
		auto idx(const geo::Point &pt) const noexcept {
			return ptToIdx(pt, columns);
		}

		/**
		 * Reads all pixels of this sheet or its children into the given pixel list
		 * @param pixels
		 */
		constexpr void readPixelsTo(ox::Vector<uint8_t> *pPixels, int8_t bpp) const noexcept {
			if (subsheets.size()) {
				for (auto &s: subsheets) {
					s.readPixelsTo(pPixels);
				}
			} else {
				if (bpp == 4) {
					for (auto p: this->pixels) {
						pPixels->emplace_back(p & 0b1111);
						pPixels->emplace_back(p >> 4);
					}
				} else {
					for (auto p: this->pixels) {
						pPixels->emplace_back(p);
					}
				}
			}
		}

		/**
		 * Reads all pixels of this sheet or its children into the given pixel list
		 * @param pixels
		 */
		constexpr void readPixelsTo(ox::Vector<uint8_t> *pPixels) const noexcept {
			if (subsheets.size()) {
				for (auto &s: subsheets) {
					s.readPixelsTo(pPixels);
				}
			} else {
				for (auto p : this->pixels) {
					pPixels->emplace_back(p);
				}
			}
		}

		[[nodiscard]]
		constexpr std::size_t size() const noexcept {
			return static_cast<std::size_t>(columns) * static_cast<std::size_t>(rows);
		}

		[[nodiscard]]
		constexpr std::size_t unusedPixels() const noexcept {
			std::size_t childrenSize = 0;
			for (auto &c : subsheets) {
				childrenSize += c.size();
			}
			return size() - childrenSize;
		}

		[[nodiscard]]
		constexpr uint8_t getPixel4Bpp(std::size_t idx) const noexcept {
			if (idx & 1) {
				return this->pixels[idx / 2] >> 4;
			} else {
				return this->pixels[idx / 2] & 0b0000'1111;
			}
		}

		[[nodiscard]]
		constexpr uint8_t getPixel8Bpp(std::size_t idx) const noexcept {
			return this->pixels[idx];
		}

		[[nodiscard]]
		constexpr auto getPixel(int8_t pBpp, std::size_t idx) const noexcept {
			if (pBpp == 4) {
				return getPixel4Bpp(idx);
			} else {
				return getPixel8Bpp(idx);
			}
		}

		[[nodiscard]]
		constexpr auto getPixel4Bpp(const geo::Point &pt) const noexcept {
			const auto idx = ptToIdx(pt, columns);
			return getPixel4Bpp(idx);
		}

		[[nodiscard]]
		constexpr auto getPixel8Bpp(const geo::Point &pt) const noexcept {
			const auto idx = ptToIdx(pt, columns);
			return getPixel8Bpp(idx);
		}

		[[nodiscard]]
		constexpr auto getPixel(int8_t pBpp, const geo::Point &pt) const noexcept {
			const auto idx = ptToIdx(pt, columns);
			return getPixel(pBpp, idx);
		}

		constexpr auto walkPixels(int8_t pBpp, auto callback) const noexcept {
			if (pBpp == 4) {
				const auto pixelCnt = ox::min<std::size_t>(static_cast<std::size_t>(columns * rows * PixelsPerTile) / 2,
				                                           pixels.size());
				//oxAssert(pixels.size() == pixelCnt, "Pixel count does not match rows and columns");
				for (std::size_t i = 0; i < pixelCnt; ++i) {
					const auto colorIdx1 = pixels[i] & 0xF;
					const auto colorIdx2 = pixels[i] >> 4;
					callback(i * 2 + 0, colorIdx1);
					callback(i * 2 + 1, colorIdx2);
				}
			} else {
				const auto pixelCnt = ox::min<std::size_t>(static_cast<std::size_t>(columns * rows * PixelsPerTile),
				                                           pixels.size());
				for (std::size_t i = 0; i < pixelCnt; ++i) {
					const auto p = pixels[i];
					callback(i, p);
				}
			}
		}

		constexpr void setPixel(int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
			auto &pixel = this->pixels[idx / 2];
			if (pBpp == 4) {
				if (idx & 1) {
					pixel = (pixel & 0b0000'1111) | (palIdx << 4);
				} else {
					pixel = (pixel & 0b1111'0000) | (palIdx);
				}
			} else {
				pixel = palIdx;
			}
		}

		constexpr void setPixel(int8_t pBpp, const geo::Point &pt, uint8_t palIdx) noexcept {
			const auto idx = ptToIdx(pt, columns);
			setPixel(pBpp, idx, palIdx);
		}

		constexpr auto setPixelCount(int8_t pBpp, std::size_t cnt) noexcept {
			switch (pBpp) {
				case 4:
					pixels.resize(cnt / 2);
					return OxError(0);
				case 8:
					pixels.resize(cnt);
					return OxError(0);
				default:
					return OxError(1, "Invalid pBpp used for TileSheet::SubSheet::setPixelCount");
			}
		}

		/**
		 * Gets a count of the pixels in this sheet, and not that of its children.
		 * @param pBpp bits per pixel, need for knowing how to count the pixels
		 * @return a count of the pixels in this sheet
		 */
		constexpr auto pixelCnt(int8_t pBpp) const noexcept {
			return pBpp == 4 ? pixels.size() * 2 : pixels.size();
		}
	};

	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
	static constexpr auto TypeVersion = 2;
	int8_t bpp = 4;
	ox::FileAddress defaultPalette;
	SubSheet subsheet{"Root", 1, 1, bpp};

	constexpr TileSheet() noexcept = default;
	inline TileSheet(const TileSheet &other) noexcept:
		bpp(other.bpp),
		defaultPalette(other.defaultPalette),
		subsheet(other.subsheet) {
	}
	inline TileSheet(TileSheet &&other) noexcept:
		bpp(std::move(other.bpp)),
		defaultPalette(std::move(other.defaultPalette)),
		subsheet(std::move(other.subsheet)) {
	}

	inline auto &operator=(const TileSheet &other) noexcept {
		if (this != &other) {
			bpp = other.bpp;
			defaultPalette = other.defaultPalette;
			subsheet = other.subsheet; } return *this;
	}
	inline auto &operator=(TileSheet &&other) noexcept {
		bpp = other.bpp;
		defaultPalette = std::move(other.defaultPalette);
		subsheet = std::move(other.subsheet);
		return *this;
	}

	[[nodiscard]]
	constexpr auto validateSubSheetIdx(const SubSheetIdx &pIdx, std::size_t pIdxIt, const SubSheet *pSubsheet) noexcept {
		if (pIdxIt == pIdx.size()) {
			return pIdx;
		}
		const auto currentIdx = pIdx[pIdxIt];
		if (pSubsheet->subsheets.size() <= currentIdx) {
			auto out = pIdx;
			if (pSubsheet->subsheets.size()) {
				out.back().value = pSubsheet->subsheets.size() - 1;
			} else {
				out.pop_back();
			}
			return out;
		}
		return validateSubSheetIdx(pIdx, pIdxIt + 1, &pSubsheet->subsheets[pIdx[pIdxIt]]);
	}

	/**
	 * validateSubSheetIdx takes a SubSheetIdx and moves the index to the
	 * preceding or parent sheet if the current corresponding sheet does
	 * not exist.
	 * @param idx SubSheetIdx to validate and correct
	 * @return a valid version of idx
	 */
	[[nodiscard]]
	constexpr auto validateSubSheetIdx(const SubSheetIdx &idx) noexcept {
		return validateSubSheetIdx(idx, 0, &subsheet);
	}

	[[nodiscard]]
	constexpr static const auto &getSubSheet(const SubSheetIdx &idx, std::size_t idxIt,
                                             const SubSheet *pSubsheet) noexcept {
		if (idxIt == idx.size()) {
			return *pSubsheet;
		}
		const auto currentIdx = idx[idxIt];
		if (pSubsheet->subsheets.size() < currentIdx) {
			return *pSubsheet;
		}
		return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[currentIdx]);
	}

	[[nodiscard]]
	constexpr static auto &getSubSheet(const SubSheetIdx &idx, std::size_t idxIt, SubSheet *pSubsheet) noexcept {
		if (idxIt == idx.size()) {
			return *pSubsheet;
		}
		return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
	}

	[[nodiscard]]
	constexpr const auto &getSubSheet(const SubSheetIdx &idx) const noexcept {
		return getSubSheet(idx, 0, &subsheet);
	}

	[[nodiscard]]
	constexpr auto &getSubSheet(const SubSheetIdx &idx) noexcept {
		return getSubSheet(idx, 0, &subsheet);
	}

	constexpr ox::Error addSubSheet(const SubSheetIdx &idx) noexcept {
		auto &parent = getSubSheet(idx);
		if (parent.subsheets.size() < 2) {
			parent.subsheets.emplace_back(ox::sfmt("Subsheet {}", parent.subsheets.size()).c_str(), 1, 1, bpp);
		} else {
			parent.subsheets.emplace_back("Subsheet 0", parent.columns, parent.rows, bpp);
			parent.subsheets.emplace_back("Subsheet 1", 1, 1, bpp);
		}
		return OxError(0);
	}

	[[nodiscard]]
	constexpr static auto rmSubSheet(const SubSheetIdx &idx, std::size_t idxIt, SubSheet *pSubsheet) noexcept {
		if (idxIt == idx.size() - 1) {
			return pSubsheet->subsheets.erase(idx[idxIt]).error;
		}
		return rmSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
	}

	[[nodiscard]]
	constexpr auto rmSubSheet(const SubSheetIdx &idx) noexcept {
		return rmSubSheet(idx, 0, &subsheet);
	}

	[[nodiscard]]
	constexpr auto getPixel4Bpp(const geo::Point &pt, const SubSheetIdx &subsheetIdx) const noexcept {
		oxAssert(bpp == 4, "TileSheetV1::getPixel4Bpp: wrong bpp");
		auto &s = this->getSubSheet(subsheetIdx);
		const auto idx = ptToIdx(pt, s.columns);
		return s.getPixel4Bpp(idx);
	}

	[[nodiscard]]
	constexpr auto getPixel8Bpp(const geo::Point &pt, const SubSheetIdx &subsheetIdx) const noexcept {
		oxAssert(bpp == 8, "TileSheetV1::getPixel8Bpp: wrong bpp");
		auto &s = this->getSubSheet(subsheetIdx);
		const auto idx = ptToIdx(pt, s.columns);
		return s.getPixel8Bpp(idx);
	}

	[[nodiscard]]
	auto pixels() const noexcept {
		ox::Vector<uint8_t> out;
		subsheet.readPixelsTo(&out);
		return out;
	}

};

struct CompactTileSheet {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.CompactTileSheet";
	static constexpr auto TypeVersion = 1;
	int8_t bpp = 0;
	ox::FileAddress defaultPalette;
	ox::Vector<uint8_t> pixels = {};
};

oxModelBegin(NostalgiaPalette)
	oxModelField(colors)
oxModelEnd()

oxModelBegin(Palette)
	oxModelField(colors)
oxModelEnd()

oxModelBegin(NostalgiaGraphic)
	oxModelField(bpp)
	oxModelField(rows)
	oxModelField(columns)
	oxModelField(defaultPalette)
	oxModelField(pal)
	oxModelField(pixels)
oxModelEnd()

oxModelBegin(TileSheet::SubSheet)
	oxModelField(name);
	oxModelField(rows);
	oxModelField(columns);
	oxModelField(subsheets)
	oxModelField(pixels)
oxModelEnd()

oxModelBegin(TileSheet)
	oxModelField(bpp)
	oxModelField(defaultPalette)
	oxModelField(subsheet)
oxModelEnd()

oxModelBegin(CompactTileSheet)
	oxModelField(bpp)
	oxModelField(defaultPalette)
	oxModelField(pixels)
oxModelEnd()

struct Sprite {
	unsigned idx = 0;
	unsigned x = 0;
	unsigned y = 0;
	unsigned tileIdx = 0;
	unsigned spriteShape = 0;
	unsigned spriteSize = 0;
	unsigned flipX = 0;
};

ox::Error initGfx(Context *ctx) noexcept;

void addCustomDrawer(Context *ctx, Drawer *cd) noexcept;

void removeCustomDrawer(Context *ctx, Drawer *cd) noexcept;

void setWindowTitle(Context *ctx, const char *title) noexcept;

void focusWindow(Context *ctx) noexcept;

[[nodiscard]]
int getScreenWidth(Context *ctx) noexcept;

[[nodiscard]]
int getScreenHeight(Context *ctx) noexcept;

[[nodiscard]]
geo::Size getScreenSize(Context *ctx) noexcept;

[[nodiscard]]
uint8_t bgStatus(Context *ctx) noexcept;

void setBgStatus(Context *ctx, uint32_t status) noexcept;

bool bgStatus(Context *ctx, unsigned bg) noexcept;

void setBgStatus(Context *ctx, unsigned bg, bool status) noexcept;

ox::Error initConsole(Context *ctx) noexcept;

void setBgCbb(Context *ctx, unsigned bgIdx, unsigned cbb) noexcept;

/**
 * @param section describes which section of the selected TileSheetSpace to use (e.g. MEM_PALLETE_BG[section])
 */
ox::Error loadBgTileSheet(Context *ctx, unsigned cbb, const ox::FileAddress &tilesheet,
                          const ox::FileAddress &palette = nullptr) noexcept;

ox::Error loadSpriteTileSheet(Context *ctx,
                              unsigned section,
                              const ox::FileAddress &tilesheetAddr,
                              const ox::FileAddress &paletteAddr) noexcept;

void puts(Context *ctx, int column, int row, const char *str) noexcept;

void setTile(Context *ctx, unsigned bgIdx, int column, int row, uint8_t tile) noexcept;

void clearTileLayer(Context *ctx, unsigned bgIdx) noexcept;

void hideSprite(Context *ctx, unsigned) noexcept;

void setSprite(Context *ctx, unsigned idx, unsigned x, unsigned y, unsigned tileIdx,
               unsigned spriteShape = 0, unsigned spriteSize = 0, unsigned flipX = 0) noexcept;

void setSprite(Context *ctx, const Sprite &s) noexcept;

}
