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

#include <keel/media.hpp>

#include <nostalgia/gfx/gfx.hpp>

#include <jasper/world/worldobject.hpp>
#include <jasper/world/worldstatic.hpp>

namespace jasper::world {

static void ensureInVec(ox::Vector<ox::FileAddress> &vec, ox::FileAddress const &val) {
	if (!vec.contains(val)) {
		vec.emplace_back(val);
	}
}

// If the root subsheet is not a leaf node, it will return the
// tile count of the first leaf node that it finds.
[[nodiscard]]
static uint8_t subsheetTileCnt(ngfx::TileSheet::SubSheet const &ss) noexcept {
	if (ss.subsheets.empty()) {
		return static_cast<uint8_t>(ss.size());
	}
    return subsheetTileCnt(ss.subsheets[0]);
}


WorldStaticLoader::WorldStaticLoader(keel::Context &kctx, WorldStatic &worldStatic, WorldDoc const &doc):
	m_kctx{kctx},
	m_worldStatic{worldStatic},
	m_doc{doc} {}

ox::Error WorldStaticLoader::loadWorldStatic() noexcept {
	reset();
	auto const tilesPerLayer =
		static_cast<size_t>(m_doc.columns) * static_cast<size_t>(m_doc.rows);
	for (auto const &setRef : m_doc.objSets) {
		auto const [set, err] =
			keel::readObj<WorldObjectSet>(m_kctx, setRef.path);
		if (!err) {
			m_objSets[setRef.id] = *set;
			ensureInVec(m_worldStatic.tilesheets, set->tilesheet);
			for (auto const &pal : set->palettes) {
				ensureInVec(m_worldStatic.palettes, pal);
			}
		}
	}
    m_worldStatic.columns = static_cast<int16_t>(m_doc.columns),
    m_worldStatic.rows = static_cast<int16_t>(m_doc.rows),
    m_worldStatic.map = {
		{.tiles = ox::Vector<TileStatic>{tilesPerLayer},},
		{.tiles = ox::Vector<TileStatic>{tilesPerLayer},},
		{.tiles = ox::Vector<TileStatic>{tilesPerLayer},},
	};
	// tiles
	for (auto lyr = 0u; lyr < layers(m_worldStatic); ++lyr) {
		for (auto x = 0u; x < static_cast<size_t>(m_worldStatic.columns); ++x) {
			for (auto y = 0u; y < static_cast<size_t>(m_worldStatic.rows); ++y) {
				auto &dst = tile(m_worldStatic, lyr, x, y);
				auto &src = tile(m_doc, lyr, x, y);;
				loadTile(dst, src);
			}
		}
	}
	m_loaded = true;
	OX_RETURN_ERROR(keel::ensureValid(m_worldStatic));
	resourcesUpdated.emit();
	return {};
}

ox::Error WorldStaticLoader::updateTile(
		uint32_t const layer,
		uint32_t const col,
		uint32_t const row,
		TileDoc const &oldSrc) noexcept {
	auto &dst = tile(m_worldStatic, layer, col, row);
	auto &newSrc = tile(m_doc, layer, col, row);
	if (oldSrc.obj.unique) {
		deleteRefSet(dst.objIdxRefSet, oldSrc.obj);
	}
	auto const current = m_cache.at(oldSrc.obj);
	if (current.ok()) {
		--current.value->refCnt;
		if (current.value->refCnt == 0) {
			auto const rs = dst.objIdxRefSet;
			dst.objIdxRefSet = 0;
			deleteRefSet(rs, oldSrc.obj);
		}
	}
	loadTile(dst, newSrc);
	if (m_resourcesChanged) {
		m_resourcesChanged = false;
		resourcesUpdated.emit();
	} else {
        tileUpdated.emit(layer, col, row);
	}
	return {};
}

void WorldStaticLoader::loadTile(TileStatic &dst, TileDoc const &src) noexcept {
	dst.tileType = src.type;
	setTopEdge(dst.layerAttachments, src.topLayerAttachment);
	setBottomEdge(dst.layerAttachments, src.bottomLayerAttachment);
	setLeftEdge(dst.layerAttachments, src.leftLayerAttachment);
	setRightEdge(dst.layerAttachments, src.rightLayerAttachment);
	dst.objIdxRefSet = setupTileResrc(src.obj).or_value(0);;
}

void WorldStaticLoader::reset() {
	m_worldStatic = {
		.objTileRefSets{
			ObjTileRefSet{
				.cbb = 0,
			},
		},
        .tilesheets{},
        .palettes{},
        .columns{},
        .rows{},
        .map{},
	};
	m_cache.clear();
	m_objSets.clear();
	m_cbbIt = 4;
	m_loaded = false;
}

/**
 * Returns the index in rsrcs for the given docObjRef.
 */
ox::Result<uint8_t> WorldStaticLoader::setupTileResrc(DocObjRef const &docObjRef) noexcept {
	if (!docObjRef.unique) {
		auto const [out, err] = m_cache.at(docObjRef);
		if (!err) {
			++out->refCnt;
			return out->value;
		}
	}
	OX_REQUIRE(objSet, m_objSets.at(docObjRef.worldObjectSetId));
	auto const obj = ox::find_if(
		objSet->objects.begin(), objSet->objects.end(),
		[&docObjRef](WorldObject const &o) {
			return o.id == docObjRef.worldObjectId;
		});
	if (obj == objSet->objects.end()) {
		return ox::Error{obj == objSet->objects.end(), "could not find WorldObject in WorldObjectSet"};
	}
	OX_REQUIRE(ts, keel::readObj<ngfx::TileSheet>(m_kctx, objSet->tilesheet));
	OX_REQUIRE(tsIdx, ox::findIdx(
		m_worldStatic.tilesheets.begin(), m_worldStatic.tilesheets.end(), objSet->tilesheet).to<uint8_t>());
	auto const subsheetOffset = ngfx::getTileIdx(*ts, obj->subsheetId);
	if (!subsheetOffset) {
		return ox::Error{1, "invalid subsheet idx"};
	}
	auto const subsheet = ngfx::getSubsheet(*ts, obj->subsheetId);
	if (!subsheet) {
		return ox::Error{1, "could not find subsheet"};
	}
	auto const out = static_cast<uint8_t>(m_worldStatic.objTileRefSets.size());
	auto const &refSet = m_worldStatic.objTileRefSets.emplace_back(ObjTileRefSet{
		.tilesheetIdx = static_cast<uint16_t>(*subsheetOffset),
		.cbbIdx = m_cbbIt,
		.palBank = obj->palBank,
		.tilesheetId = tsIdx,
		.tileCnt = subsheetTileCnt(*subsheet),
		.frames = obj->frames,
		.intervalMs = obj->intervalMs,
	});
	m_cbbIt += refSet.tileCnt;
	m_resourcesChanged = true;
	if (!docObjRef.unique) {
		m_cache[docObjRef] = {.value = out, .refCnt = 1};
	}
	return out;
}

void WorldStaticLoader::deleteRefSet(uint8_t const refIdx, DocObjRef const &oldObjRef) noexcept {
	for (auto lyr = 0u; lyr < layers(m_worldStatic); ++lyr) {
		for (auto x = 0u; x < static_cast<size_t>(m_worldStatic.columns); ++x) {
			for (auto y = 0u; y < static_cast<size_t>(m_worldStatic.rows); ++y) {
				auto &dst = tile(m_worldStatic, lyr, x, y);
				if (dst.objIdxRefSet > refIdx) {
					--dst.objIdxRefSet;
				}
				oxAssert(dst.objIdxRefSet < m_worldStatic.objTileRefSets.size() - 1, "Invalid objIdxRefSet");
			}
		}
	}
	for (auto &p : m_cache.pairs()) {
		if (p.value.value > refIdx) {
			--p.value.value;
		}
	}
	auto const &removedRefSet = m_worldStatic.objTileRefSets[refIdx];
	for (size_t i = refIdx + 1; i < m_worldStatic.objTileRefSets.size(); ++i) {
		auto &refSet = m_worldStatic.objTileRefSets[i];
		refSet.cbbIdx -= removedRefSet.frames;
	}
	std::ignore = m_worldStatic.objTileRefSets.erase(refIdx);
	m_cache.erase(oldObjRef);
	m_resourcesChanged = true;
}

}
