/*
 * Copyright 2016 - 2023 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 {

TileSheet::SubSheet::SubSheet(SubSheet &&other) noexcept:
	id       (other.id),
	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 = {};
	other.rows = {};
}

TileSheet::SubSheet::SubSheet(
		SubSheetId pId,
		ox::CRStringView pName,
		int pColumns,
		int pRows,
		int bpp) noexcept:
	id(pId),
	name(pName),
	columns(pColumns),
	rows(pRows),
	pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}

TileSheet::SubSheet::SubSheet(
		SubSheetId pId,
		ox::CRStringView pName,
		int pColumns,
		int pRows,
		ox::Vector<uint8_t> pPixels) noexcept:
	id(pId),
	name(pName),
	columns(pColumns),
	rows(pRows),
	pixels(std::move(pPixels)) {
}

TileSheet::SubSheet &TileSheet::SubSheet::operator=(TileSheet::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;
}

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

void TileSheet::SubSheet::readPixelsTo(ox::Vector<uint8_t> *pPixels, int8_t pBpp) const noexcept {
	if (!subsheets.empty()) {
		for (auto &s: subsheets) {
			s.readPixelsTo(pPixels);
		}
	} else {
		if (pBpp == 4) {
			for (auto p: this->pixels) {
				pPixels->emplace_back(static_cast<uint8_t>(p & 0b1111));
				pPixels->emplace_back(static_cast<uint8_t>(p >> 4));
			}
		} else {
			for (auto p: this->pixels) {
				pPixels->emplace_back(p);
			}
		}
	}
}

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

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

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

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

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

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

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

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

void TileSheet::SubSheet::setPixel(int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
	auto &pixel = this->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 TileSheet::SubSheet::setPixel(int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept {
	const auto idx = ptToIdx(pt, columns);
	setPixel(pBpp, idx, palIdx);
}

ox::Error TileSheet::SubSheet::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");
	}
}

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

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

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

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


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

TileSheet &TileSheet::operator=(TileSheet &&other) noexcept {
	bpp = other.bpp;
	idIt = other.idIt;
	defaultPalette = std::move(other.defaultPalette);
	subsheet = std::move(other.subsheet);
	return *this;
}

TileSheet::SubSheetIdx TileSheet::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.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 TileSheet::validateSubSheetIdx(const SubSheetIdx &idx) noexcept {
	return validateSubSheetIdx(idx, 0, &subsheet);
}

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

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

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

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

ox::Error TileSheet::rmSubSheet(
		SubSheetIdx const&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]]);
}

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

uint8_t TileSheet::getPixel4Bpp(
		ox::Point const&pt,
		TileSheet::SubSheetIdx const&subsheetIdx) const noexcept {
	oxAssert(bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
	auto &s = this->getSubSheet(subsheetIdx);
	const auto idx = ptToIdx(pt, s.columns);
	return s.getPixel4Bpp(idx);
}

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

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

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

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

ox::Vector<uint8_t> TileSheet::pixels() const noexcept {
	ox::Vector<uint8_t> out;
	subsheet.readPixelsTo(&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;
}

}
