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

#include "nostalgia/gfx/tilesheet.hpp"

#include <ox/std/size.hpp>
#include <ox/std/vector.hpp>

#include <nostalgia/gfx/ptidxconv.hpp>
#include <nostalgia/gfx/tilesheet.hpp>

namespace nostalgia::gfx {

std::size_t idx(TileSheet::SubSheet const &ss, ox::Point const &pt) noexcept {
	return ptToIdx(pt, ss.columns);
}

[[nodiscard]]
static TileSheet::SubSheet const *getSubsheet(TileSheet::SubSheet const &ss, SubSheetId const id) noexcept {
	if (ss.id == id) {
		return &ss;
	}
	for (auto const &child: ss.subsheets) {
		if (auto out = getSubsheet(child, id)) {
			return out;
		}
	}
	return {};
}

[[nodiscard]]
static size_t getTileCnt(TileSheet::SubSheet const &ss) noexcept {
	if (ss.subsheets.empty()) {
		return ss.pixels.size() / PixelsPerTile;
	} else {
		size_t out{};
		for (auto const &child: ss.subsheets) {
			out += getTileCnt(child);
		}
		return out;
	}
}

size_t getTileCnt(TileSheet const &ts) noexcept {
	return getTileCnt(ts.subsheet);
}

TileSheet::SubSheet const *getSubsheet(TileSheet const &ts, SubSheetId const id) noexcept {
	return getSubsheet(ts.subsheet, id);
}

static ox::Optional<size_t> getPixelIdx(
		TileSheet::SubSheet const &ss,
		SubSheetId const id,
		size_t &idx) noexcept {
	for (auto const &child: ss.subsheets) {
		if (child.id == id) {
			return ox::Optional<size_t>(ox::in_place, idx);
		}
		if (auto out = getPixelIdx(child, id, idx)) {
			return out;
		}
		idx += pixelCnt(child);
	}
	return ox::Optional<size_t>{};
}

ox::Optional<size_t> getTileIdx(TileSheet const &ts, SubSheetId const id) noexcept {
	size_t idx{};
	auto const out = getPixelIdx(ts.subsheet, id, idx);
	return out ? ox::Optional<size_t>{ox::in_place, *out / PixelsPerTile} : out;
}

uint8_t getPixel(TileSheet::SubSheet const &ss, std::size_t const idx) noexcept {
	return ss.pixels[idx];
}

uint8_t getPixel(TileSheet::SubSheet const &ss, ox::Point const &pt) noexcept {
	auto const idx = ptToIdx(pt, ss.columns);
	return getPixel(ss, idx);
}

static void setPixel(
	ox::Vector<uint8_t> &pixels,
	int const columns,
	ox::Point const &pt,
	uint8_t const palIdx) noexcept {
	auto const idx = ptToIdx(pt, columns);
	pixels[idx] = palIdx;
}

void setPixel(TileSheet::SubSheet &ss, ox::Point const &pt, uint8_t const palIdx) noexcept {
	auto const idx = ptToIdx(pt, ss.columns);
	ss.pixels[idx] = palIdx;
}

static ox::Error setPixelCount(ox::Vector<uint8_t> &pixels, std::size_t const cnt) noexcept {
	pixels.reserve(cnt);
	pixels.resize(cnt);
	return {};
}

ox::Error setPixelCount(TileSheet::SubSheet &ss, std::size_t const cnt) noexcept {
	return setPixelCount(ss.pixels, cnt);
}

void flipX(TileSheet::SubSheet &ss, ox::Point const &a, ox::Point const &b) noexcept {
	auto const w = (b.x - a.x) / 2 + 1;
	auto const pixCols = ss.columns * TileWidth;
	for (int32_t yi = a.y; yi <= b.y; ++yi) {
		for (int32_t xi = 0; xi < w; ++xi) {
			auto const aIdx = ptToIdx(a.x + xi, yi, pixCols);
			auto const bIdx = ptToIdx(b.x - xi, yi, pixCols);
			std::swap(ss.pixels[aIdx], ss.pixels[bIdx]);
		}
	}
}

void flipY(TileSheet::SubSheet &ss, ox::Point const &a, ox::Point const &b) noexcept {
	auto const h = (b.y - a.y) / 2 + 1;
	auto const pixCols = ss.columns * TileWidth;
	for (int32_t yi = 0; yi < h; ++yi) {
		for (int32_t xi = a.x; xi <= b.x; ++xi) {
			auto const aIdx = ptToIdx(xi, a.y + yi, pixCols);
			auto const bIdx = ptToIdx(xi, b.y - yi, pixCols);
			std::swap(ss.pixels[aIdx], ss.pixels[bIdx]);
		}
	}
}

unsigned pixelCnt(TileSheet::SubSheet const &ss) noexcept {
	return static_cast<unsigned>(ss.pixels.size());
}

ox::Error resizeSubsheet(TileSheet::SubSheet &ss, ox::Size const &sz) noexcept {
	ox::Vector<uint8_t> out;
	OX_RETURN_ERROR(setPixelCount(out, static_cast<size_t>(sz.width * sz.height) * PixelsPerTile));
	auto const w = ox::min<int32_t>(ss.columns, sz.width) * TileWidth;
	auto const h = ox::min<int32_t>(ss.rows, sz.height) * TileHeight;
	for (auto x = 0; x < w; ++x) {
		for (auto y = 0; y < h; ++y) {
			auto const palIdx = getPixel(ss, {x, y});
			setPixel(out, sz.width, {x, y}, palIdx);
		}
	}
	ss.columns = sz.width;
	ss.rows = sz.height;
	ss.pixels = std::move(out);
	return {};
}

ox::Result<ox::StringView> getNameFor(TileSheet::SubSheet const &ss, SubSheetId const pId) noexcept {
	if (ss.id == pId) {
		return ox::StringView(ss.name);
	}
	for (auto const &sub : ss.subsheets) {
		auto const [name, err] = getNameFor(sub, pId);
		if (!err) {
			return name;
		}
	}
	return ox::Error(1, "SubSheet not found");
}


TileSheet::SubSheetIdx validateSubSheetIdx(
		TileSheet::SubSheetIdx &&pIdx,
		std::size_t const pIdxIt,
		TileSheet::SubSheet const &pSubsheet) noexcept {
	if (pIdxIt >= pIdx.size()) {
		return std::move(pIdx);
	}
	auto &currentIdx = pIdx[pIdxIt];
	if (pSubsheet.subsheets.size() <= currentIdx) {
		if (pSubsheet.subsheets.empty()) {
			// currentIdx could not be repaired, remove
			// this and all succeeding idxs and return
			pIdx.resize(pIdxIt);
			return std::move(pIdx);
		} else {
			currentIdx = static_cast<uint32_t>(pSubsheet.subsheets.size() - 1);
		}
	}
	return validateSubSheetIdx(std::move(pIdx), pIdxIt + 1, pSubsheet.subsheets[currentIdx]);
}

TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const &ts, TileSheet::SubSheetIdx idx) noexcept {
	return validateSubSheetIdx(std::move(idx), 0, ts.subsheet);
}

static ox::Error getSubSheetIdx(TileSheet::SubSheet const &ss, SubSheetId const pId, TileSheet::SubSheetIdx &idx) noexcept {
	if (ss.id == pId) {
		return {};
	}
	for (uint32_t i{}; auto const &sub : ss.subsheets) {
		idx.emplace_back(i);
		auto const err = getSubSheetIdx(sub, pId, idx);
		if (!err) {
			return {};
		}
		idx.pop_back();
		++i;
	}
	return ox::Error{1, "SubSheet not found"};
}

ox::Result<TileSheet::SubSheetIdx> getSubSheetIdx(TileSheet const &ts, SubSheetId const pId) noexcept {
	ox::Result<TileSheet::SubSheetIdx> out;
	OX_RETURN_ERROR(getSubSheetIdx(ts.subsheet, pId, out.value));
	return out;
}


template<typename SubSheet>
static ox::Result<SubSheet*> getSubSheet(
		ox::SpanView<ox::StringView> const &idx,
		std::size_t const idxIt,
		SubSheet &pSubSheet) noexcept {
	if (idxIt == idx.size()) {
		return &pSubSheet;
	}
	auto const &currentIdx = idx[idxIt];
	auto const next = ox::find_if(
		pSubSheet.subsheets.begin(),
		pSubSheet.subsheets.end(),
		[&currentIdx](TileSheet::SubSheet const &ss) {
			return ss.name == currentIdx;
		});
	if (next == pSubSheet.subsheets.end()) {
		return ox::Error{1, "SubSheet not found"};
	}
	return getSubSheet(idx, idxIt + 1, *next);
}

ox::Result<TileSheet::SubSheet const*> getSubSheet(
		ox::SpanView<ox::StringView> const &idx,
		TileSheet const &ts) noexcept {
	if (!idx.empty() && idx[0] == ts.subsheet.name) {
		return getSubSheet<TileSheet::SubSheet const>(idx, 1, ts.subsheet);
	}
	return ox::Error{1, "SubSheet not found"};
}

ox::Result<TileSheet::SubSheet*> getSubSheet(
		ox::SpanView<ox::StringView> const &idx,
		TileSheet &ts) noexcept {
	if (!idx.empty() && idx[0] == ts.subsheet.name) {
		return getSubSheet<TileSheet::SubSheet>(idx, 1, ts.subsheet);
	}
	return ox::Error{1, "SubSheet not found"};
}


#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-reference"
#endif
static TileSheet::SubSheet const &getSubSheet(
		ox::SpanView<uint32_t> const &idx,
		std::size_t const idxIt,
		TileSheet::SubSheet const &pSubsheet) noexcept {
	if (idxIt == idx.size()) {
		return pSubsheet;
	}
	auto const currentIdx = idx[idxIt];
	if (pSubsheet.subsheets.size() < currentIdx) {
		return pSubsheet;
	}
	return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[currentIdx]);
}
#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic pop
#endif

TileSheet::SubSheet &getSubSheet(
		ox::SpanView<uint32_t> const &idx,
		std::size_t const idxIt,
		TileSheet::SubSheet &pSubsheet) noexcept {
	if (idxIt == idx.size()) {
		return pSubsheet;
	}
	return getSubSheet(idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]);
}

#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-reference"
#endif
TileSheet::SubSheet const &getSubSheet(TileSheet const &ts, ox::SpanView<uint32_t> const &idx) noexcept {
	return gfx::getSubSheet(idx, 0, ts.subsheet);
}

TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx) noexcept {
	return gfx::getSubSheet(idx, 0, ts.subsheet);
}
#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic pop
#endif

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

ox::Error insertSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx, TileSheet::SubSheet ss) noexcept {
	if (idx.empty()) {
		return ox::Error{1, "invalid insert idx"};
	}
	auto &parent = getSubSheet(ts, {idx.data(), idx.size() - 1});
	auto const insertIdx = idx[idx.size() - 1];
	if (insertIdx > parent.subsheets.size()) {
		return ox::Error{1, "invalid insert idx"};
	}
	parent.subsheets.emplace(insertIdx, std::move(ss));
	return {};
}

ox::Error rmSubSheet(
		TileSheet &ts,
		TileSheet::SubSheetIdx const &idx,
		std::size_t const idxIt,
		TileSheet::SubSheet &pSubsheet) noexcept {
	if (idxIt == idx.size() - 1) {
		return pSubsheet.subsheets.erase(idx[idxIt]).error;
	}
	return rmSubSheet(ts, idx, idxIt + 1, pSubsheet.subsheets[idx[idxIt]]);
}

ox::Error rmSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept {
	return rmSubSheet(ts, idx, 0, ts.subsheet);
}

uint8_t getPixel(
		TileSheet const &ts,
		ox::Point const &pt,
		TileSheet::SubSheetIdx const &subsheetIdx) noexcept {
	auto &s = getSubSheet(ts, subsheetIdx);
	auto const idx = ptToIdx(pt, s.columns);
	return getPixel(s, idx);
}

uint8_t getPixel4Bpp(
		CompactTileSheet const &ts,
		size_t const idx) noexcept {
	oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
	if (idx & 1) {
		return ts.pixels[idx / 2] >> 4;
	} else {
		return ts.pixels[idx / 2] & 0b0000'1111;
	}
}

uint8_t getPixel8Bpp(
		CompactTileSheet const &ts,
		size_t const idx) noexcept {
	oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
	return ts.pixels[idx];
}

ox::Pair<uint8_t> get2Pixels4Bpp(
		CompactTileSheet const &ts,
		size_t const idx) noexcept {
	oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
	auto const out = ts.pixels[idx / 2];
	return {
		static_cast<uint8_t>(out & 0x0f),
		static_cast<uint8_t>(out >> 4),
	};
}

ox::Pair<uint8_t> get2Pixels8Bpp(
		CompactTileSheet const &ts,
		size_t const idx) noexcept {
	oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
	return {
		static_cast<uint8_t>(ts.pixels[idx]),
		static_cast<uint8_t>(ts.pixels[idx + 1]),
	};
}

static ox::Result<SubSheetId> getIdFor(
		TileSheet::SubSheet const &ss,
		ox::SpanView<ox::StringView> const &pNamePath,
		std::size_t const pIt = 0) noexcept {
	for (auto &sub : ss.subsheets) {
		if (sub.name == pNamePath[pIt]) {
			if (pIt == pNamePath.size()) {
				return ss.id;
			}
			return getIdFor(ss, pNamePath, pIt + 1);
		}
	}
	return ox::Error(1, "SubSheet not found");
}

ox::Result<SubSheetId> getIdFor(TileSheet const &ts, ox::StringViewCR path) noexcept {
	return getIdFor(ts.subsheet, ox::split<8>(path, '.'));
}

/**
 * Gets the offset in tiles of the desired subsheet.
 */
static ox::Result<uint32_t> getTileOffset(
		TileSheet::SubSheet const &ss,
		ox::SpanView<ox::StringView> const &pNamePath,
		std::size_t const pIt = 0,
		uint32_t pCurrentTotal = 0) noexcept {
	// pIt == pNamePath.size() - 1 &&
	if (ss.name != pNamePath[pIt]) {
		return ox::Error(2, "Wrong branch");
	}
	if (pIt == pNamePath.size() - 1) {
		return pCurrentTotal;
	}
	for (auto &sub : ss.subsheets) {
		auto [offset, err] = getTileOffset(
				sub, pNamePath, pIt + 1, pCurrentTotal);
		if (!err) {
			return offset;
		}
		// Possible bug? Should this be using a recursive version of
		// pixelCnt will count pixels in subsheets of sub as well.
		pCurrentTotal += pixelCnt(sub) / PixelsPerTile;
	}
	return ox::Error(1, "SubSheet not found");
}

ox::Result<uint32_t> getTileOffset(TileSheet const &ts, ox::StringViewCR pNamePath) noexcept {
	return gfx::getTileOffset(ts.subsheet, ox::split<8>(pNamePath, '.'));
}

ox::Result<ox::StringView> getNameFor(TileSheet &ts, SubSheetId const pId) noexcept {
	return gfx::getNameFor(ts.subsheet, pId);
}

ox::Result<ox::StringView> getNameFor(TileSheet const &ts, SubSheetId const pId) noexcept {
	return gfx::getNameFor(ts.subsheet, pId);
}

static void readPixelsTo(TileSheet::SubSheet &ss, int const pBpp, ox::Vector<uint8_t> &pPixels) noexcept {
	if (!ss.subsheets.empty()) {
		for (auto &s: ss.subsheets) {
			readPixelsTo(s, pBpp, pPixels);
		}
	} else {
		if (pBpp == 4) {
			for (size_t i{}; i < ss.pixels.size() - 1; i += 2) {
				auto const p1 = ss.pixels[i];
				auto const p2 = ss.pixels[i + 1];
				pPixels.emplace_back(static_cast<uint8_t>(p1 | (p2 << 4)));
			}
		} else {
			for (auto const p : ss.pixels) {
				pPixels.emplace_back(p);
			}
		}
	}
}

ox::Vector<uint8_t> pixels(TileSheet &ts) noexcept {
	ox::Vector<uint8_t> out;
	readPixelsTo(ts.subsheet, ts.bpp, out);
	return out;
}


ox::Vector<uint32_t> resizeTileSheetData(
		ox::Vector<uint32_t> const &srcPixels,
		ox::Size const &srcSize,
		int const scale) noexcept {
	ox::Vector<uint32_t> dst;
	auto dstWidth = srcSize.width * scale;
	auto dstHeight = srcSize.height * scale;
	auto const pixelCnt = dstWidth * dstHeight;
	dst.resize(static_cast<std::size_t>(pixelCnt));
	for (auto i = 0; i < pixelCnt; ++i) {
		auto const dstPt = idxToPt(i, 1, scale);
		auto const srcPt = dstPt / ox::Point{scale, scale};
		auto const srcIdx = ptToIdx(srcPt, 1);
		auto const srcPixel = srcPixels[srcIdx];
		dst[static_cast<std::size_t>(i)] = srcPixel;
	}
	return dst;
}

}
