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

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

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

namespace nostalgia::core {

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

uint8_t getPixel4Bpp(TileSheet::SubSheet const&ss, std::size_t idx) noexcept {
	if (idx & 1) {
		return ss.pixels[idx / 2] >> 4;
	} else {
		return ss.pixels[idx / 2] & 0b0000'1111;
	}
}

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

uint8_t getPixel(TileSheet::SubSheet const&ss, int8_t pBpp, std::size_t idx) noexcept {
	if (pBpp == 4) {
		return getPixel4Bpp(ss, idx);
	} else {
		return getPixel8Bpp(ss, idx);
	}
}

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

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

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

void setPixel(TileSheet::SubSheet &ss, int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
	auto &pixel = ss.pixels[static_cast<std::size_t>(idx / 2)];
	if (pBpp == 4) {
		if (idx & 1) {
			pixel = static_cast<uint8_t>((pixel & 0b0000'1111) | (palIdx << 4));
		} else {
			pixel = (pixel & 0b1111'0000) | (palIdx);
		}
	} else {
		pixel = palIdx;
	}
}

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

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

unsigned pixelCnt(TileSheet::SubSheet const&ss, int8_t pBpp) noexcept {
	const auto pixelsSize = static_cast<unsigned>(ss.pixels.size());
	return pBpp == 4 ? pixelsSize * 2 : pixelsSize;
}

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


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

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

const TileSheet::SubSheet &getSubSheet(
		TileSheet::SubSheetIdx const&idx,
		std::size_t idxIt,
		TileSheet::SubSheet const&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]);
}

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

TileSheet::SubSheet const&getSubSheet(TileSheet const&ts, TileSheet::SubSheetIdx const&idx) noexcept {
	return core::getSubSheet(idx, 0, ts.subsheet);
}

TileSheet::SubSheet &getSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const&idx) noexcept {
	return core::getSubSheet(idx, 0, ts.subsheet);
}

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, ts.bpp);
	} else {
		parent.subsheets.emplace_back(++ts.idIt, "Subsheet 0", parent.columns, parent.rows, ts.bpp);
		parent.subsheets.emplace_back(++ts.idIt, "Subsheet 1", 1, 1, ts.bpp);
	}
	return OxError(0);
}

ox::Error rmSubSheet(
		TileSheet &ts,
		TileSheet::SubSheetIdx const&idx,
		std::size_t 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 getPixel4Bpp(
		TileSheet const&ts,
		ox::Point const&pt,
		TileSheet::SubSheetIdx const&subsheetIdx) noexcept {
	oxAssert(ts.bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
	auto &s = getSubSheet(ts, subsheetIdx);
	const auto idx = ptToIdx(pt, s.columns);
	return getPixel4Bpp(s, idx);
}

uint8_t getPixel8Bpp(
		TileSheet const&ts,
		ox::Point const&pt,
		TileSheet::SubSheetIdx const&subsheetIdx) noexcept {
	oxAssert(ts.bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
	auto &s = getSubSheet(ts, subsheetIdx);
	const auto idx = ptToIdx(pt, s.columns);
	return getPixel8Bpp(s, idx);
}

static ox::Result<SubSheetId> getIdFor(
		TileSheet::SubSheet const&ss,
		ox::SpanView<ox::StringView> const&pNamePath,
		std::size_t 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 OxError(1, "SubSheet not found");
}

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

/**
 * Gets the offset in tiles of the desired subsheet.
 */
static ox::Result<unsigned> getTileOffset(
		TileSheet::SubSheet const&ss,
		ox::SpanView<ox::StringView> const&pNamePath,
		int8_t pBpp,
		std::size_t pIt = 0,
		unsigned pCurrentTotal = 0) noexcept {
	// pIt == pNamePath.size() - 1 &&
	if (ss.name != pNamePath[pIt]) {
		return OxError(2, "Wrong branch");
	}
	if (pIt == pNamePath.size() - 1) {
		return pCurrentTotal;
	}
	for (auto &sub : ss.subsheets) {
		auto [offset, err] = getTileOffset(
				sub, pNamePath, pBpp, pIt + 1, pCurrentTotal);
		if (!err) {
			return offset;
		}
		pCurrentTotal += pixelCnt(sub, pBpp) / PixelsPerTile;
	}
	return OxError(1, "SubSheet not found");
}

ox::Result<unsigned> getTileOffset(TileSheet const&ts, ox::CRStringView pNamePath) noexcept {
	return core::getTileOffset(ts.subsheet, ox::split<8>(pNamePath, '.'), ts.bpp);
}

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

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

static void readPixelsTo(TileSheet::SubSheet &ss, ox::Vector<uint8_t> &pPixels) noexcept {
	if (!ss.subsheets.empty()) {
		for (auto &s: ss.subsheets) {
			readPixelsTo(s, pPixels);
		}
	} else {
		for (auto p : ss.pixels) {
			pPixels.emplace_back(p);
		}
	}
}

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


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

}
