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

#pragma once

#include <ox/fs/fs.hpp>
#include <ox/std/error.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/std/vector.hpp>

#include "worlddoc.hpp"

namespace jasper::world {

constexpr void setTopEdge(uint8_t &layerAttachments, uint8_t const val) noexcept {
	layerAttachments = static_cast<uint8_t>((layerAttachments & 0b11111100) | val);
}
constexpr void setBottomEdge(uint8_t &layerAttachments, uint8_t const val) noexcept {
	layerAttachments = static_cast<uint8_t>((layerAttachments & 0b11110011) | (val << 2));
}
constexpr void setLeftEdge(uint8_t &layerAttachments, uint8_t const val) noexcept {
	layerAttachments = static_cast<uint8_t>((layerAttachments & 0b11001111) | (val << 4));
}
constexpr void setRightEdge(uint8_t &layerAttachments, uint8_t const val) noexcept {
	layerAttachments = static_cast<uint8_t>((layerAttachments & 0b00111111) | (val << 6));
}

[[nodiscard]]
constexpr uint8_t topEdge(uint8_t const layerAttachments) noexcept {
	return layerAttachments & 0b11;
}
[[nodiscard]]
constexpr uint8_t bottomEdge(uint8_t const layerAttachments) noexcept {
	return (layerAttachments >> 2) & 0b11;
}
[[nodiscard]]
constexpr uint8_t leftEdge(uint8_t const layerAttachments) noexcept {
	return (layerAttachments >> 4) & 0b11;
}
[[nodiscard]]
constexpr uint8_t rightEdge(uint8_t const layerAttachments) noexcept {
	return (layerAttachments >> 6) & 0b11;
}


struct TileStatic {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.TileStatic";
	static constexpr auto TypeVersion = 1;
	uint8_t objIdxRefSet{};
	uint8_t tileType{};
	uint8_t layerAttachments{};
};

OX_MODEL_BEGIN(TileStatic)
	OX_MODEL_FIELD(objIdxRefSet)
	OX_MODEL_FIELD(tileType)
	OX_MODEL_FIELD(layerAttachments)
OX_MODEL_END()


struct BgLayer {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.BgLayer";
	static constexpr auto TypeVersion = 1;
	uint8_t cbb{};
	ox::Vector<TileStatic> tiles;
};

OX_MODEL_BEGIN(BgLayer)
	OX_MODEL_FIELD(cbb)
	OX_MODEL_FIELD(tiles)
OX_MODEL_END()


struct ObjTileRefSet {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.ObjTileRefSet";
	static constexpr auto TypeVersion = 1;
	uint16_t tilesheetIdx{};
	uint16_t cbbIdx{};
	uint8_t cbb{};
	uint8_t palBank{};
	// which tilesheet to use
	uint8_t tilesheetId{};
	uint8_t tileCnt{};
	// each successive frame will use tileIdx[i] + tileCnt * frameNo for the tileIdx
	uint8_t frameSets{};
	uint8_t frames{};
	uint16_t intervalMs{};
};

OX_MODEL_BEGIN(ObjTileRefSet)
	OX_MODEL_FIELD(tilesheetIdx)
	OX_MODEL_FIELD(cbbIdx)
	OX_MODEL_FIELD(cbb)
	OX_MODEL_FIELD(palBank)
	OX_MODEL_FIELD(tilesheetId)
	OX_MODEL_FIELD(tileCnt)
	OX_MODEL_FIELD(frameSets)
	OX_MODEL_FIELD(frames)
	OX_MODEL_FIELD(intervalMs)
OX_MODEL_END()

[[nodiscard]]
constexpr bool operator==(ObjTileRefSet const &a, ObjTileRefSet const &b) noexcept {
	return
		a.tilesheetIdx == b.tilesheetIdx &&
		a.cbbIdx == b.cbbIdx &&
		a.cbb == b.cbb &&
		a.palBank == b.palBank &&
		a.tilesheetId == b.tilesheetId &&
		a.tileCnt == b.tileCnt &&
		a.frameSets == b.frameSets &&
		a.frames == b.frames &&
		a.intervalMs == b.intervalMs;
}


struct WorldStatic {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.WorldStatic";
	static constexpr auto TypeVersion = 1;
	static constexpr auto Preloadable = true;
	ox::Vector<ObjTileRefSet> objTileRefSets;
	ox::Vector<ox::FileAddress> tilesheets;
	ox::Vector<ox::FileAddress> palettes;
	int16_t columns{};
	int16_t rows{};
	ox::Array<BgLayer, 3> map;
};

OX_MODEL_BEGIN(WorldStatic)
	OX_MODEL_FIELD(objTileRefSets)
	OX_MODEL_FIELD(tilesheets)
	OX_MODEL_FIELD(palettes)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(map)
OX_MODEL_END()

[[nodiscard]]
constexpr bool valid(WorldStatic const &ws) noexcept {
	auto const tileCnt = static_cast<size_t>(ws.columns * ws.rows);
	return
		!ws.objTileRefSets.empty() &&
		ox::all_of(ws.map.begin(), ws.map.end(), [tileCnt](BgLayer const &v) {
			return v.tiles.size() == tileCnt;
		});
}

[[nodiscard]]
constexpr size_t layers(WorldStatic const &ws) noexcept {
	return ws.map.size();
}

[[nodiscard]]
auto &tile(
		ox::CommonRefWith<WorldStatic> auto&ws,
		size_t const lyr,
		ox::IntegerRange_c<500> auto col,
		ox::IntegerRange_c<500> auto row) noexcept {
	auto const idx = static_cast<size_t>(row) * static_cast<size_t>(ws.columns) + static_cast<size_t>(col);
	return ws.map[lyr].tiles[idx];
}

[[nodiscard]]
auto &cbb(ox::CommonRefWith<WorldStatic> auto&ws, size_t const lyr) noexcept {
	return ws.map[lyr].cbb;
}


class WorldStaticLoader {
	public:
		ox::Signal<ox::Error(uint32_t layer, uint32_t x, uint32_t y)> tileUpdated;
		ox::Signal<ox::Error()> resourcesUpdated;
	private:
		keel::Context &m_kctx;
		WorldStatic &m_worldStatic;
		WorldDoc const &m_doc;
		struct CacheEntry {
			uint8_t value{};
			int refCnt{};
		};
		ox::SmallMap<DocObjRef, CacheEntry> m_cache;
        ox::SmallMap<uint64_t, WorldObjectSet> m_objSets;
		uint16_t m_cbbIt{4};
		bool m_resourcesChanged{};
		bool m_loaded{};

	public:
		WorldStaticLoader(keel::Context &kctx, WorldStatic &worldStatic, WorldDoc const &doc);

		ox::Error loadWorldStatic() noexcept;

        ox::Error updateTile(
                uint32_t layer,
                uint32_t col,
                uint32_t row,
                TileDoc const &oldSrc) noexcept;

        void loadTile(
                TileStatic &dst,
                TileDoc const &src) noexcept;

	private:
		void reset();

		ox::Result<uint8_t> setupTileResrc(DocObjRef const &docObjRef) noexcept;

		void deleteRefSet(uint8_t refIdx, DocObjRef const &oldObjRef) noexcept;
};

}
