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

#pragma once

#include <ox/fs/fs.hpp>
#include <ox/std/point.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>

#include <nostalgia/gfx/ptidxconv.hpp>

#include "palette.hpp"

namespace nostalgia::gfx {

// Predecessor to TileSheet, kept for backward compatibility
struct TileSheetV1 {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
	static constexpr auto TypeVersion = 1;
	int8_t bpp = 0;
	// rows and columns are really only used by TileSheetEditor
	int rows = 1;
	int columns = 1;
	ox::FileAddress defaultPalette;
	PaletteV1 pal;
	ox::Vector<uint8_t> pixels = {};
};

[[nodiscard]]
constexpr bool valid(TileSheetV1 const &ts) noexcept {
	auto const bytes = static_cast<size_t>(ts.columns * ts.rows * PixelsPerTile) / (ts.bpp == 4 ? 2 : 1);
	return (ts.bpp == 4 || ts.bpp == 8) && ts.pixels.size() == bytes;
}

constexpr ox::Error repair(TileSheetV1 &ts, int const bpp) noexcept {
	auto const bytes = static_cast<size_t>(ts.columns * ts.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	ts.pixels.resize(bytes);
	return {};
}


struct TileSheetV2 {
	using SubSheetIdx = ox::Vector<std::size_t, 4>;

	struct SubSheet {
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
		static constexpr auto TypeVersion = 1;
		ox::String name;
		int columns = 0;
		int rows = 0;
		ox::Vector<SubSheet> subsheets;
		ox::Vector<uint8_t> pixels;
		constexpr SubSheet() noexcept = default;
		constexpr SubSheet(ox::StringParam pName, int pColumns, int pRows, int bpp) noexcept:
			name(std::move(pName)),
			columns(pColumns),
			rows(pRows),
			pixels(static_cast<size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
		}
	};

	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
	static constexpr auto TypeVersion = 2;
	int8_t bpp = 4;
	ox::FileAddress defaultPalette;
	SubSheet subsheet{"Root", 1, 1, bpp};

};

[[nodiscard]]
constexpr bool valid(TileSheetV2::SubSheet const &ss, int bpp) noexcept {
	auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	return ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
			[bpp, bytes](TileSheetV2::SubSheet const &s) {
		return bytes == s.pixels.size() && valid(s, bpp);
	});
}

[[nodiscard]]
constexpr bool valid(TileSheetV2 const &ts) noexcept {
	return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}

constexpr void repair(TileSheetV2::SubSheet &ss, int const bpp) noexcept {
	auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	ss.pixels.resize(bytes);
	for (auto &s : ss.subsheets) {
		repair(s, bpp);
	}
}

constexpr ox::Error repair(TileSheetV2 &ts) noexcept {
	if (ts.bpp != 4 && ts.bpp != 8) {
		return ox::Error(1, "Unable to repair TileSheet");
	}
	repair(ts.subsheet, ts.bpp);
	return {};
}


using SubSheetId = int32_t;

struct TileSheetV3 {
	using SubSheetIdx = ox::Vector<std::size_t, 4>;

	struct SubSheet {
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
		static constexpr auto TypeVersion = 3;
		SubSheetId id = 0;
		ox::String name;
		int columns = 0;
		int rows = 0;
		ox::Vector<SubSheet> subsheets;
		ox::Vector<uint8_t> pixels;
		constexpr SubSheet() noexcept = default;
		SubSheet(
				SubSheetId pId,
				ox::StringParam pName,
				int pColumns,
				int pRows,
				int bpp) noexcept:
				id(pId),
				name(std::move(pName)),
				columns(pColumns),
				rows(pRows),
				pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
		}

	};

	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
	static constexpr auto TypeVersion = 3;
	int8_t bpp = 4;
	SubSheetId idIt = 0;
	ox::FileAddress defaultPalette;
	SubSheet subsheet{0, "Root", 1, 1, bpp};

};

[[nodiscard]]
constexpr bool valid(TileSheetV3::SubSheet const &ss, int bpp) noexcept {
	auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	return ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
			[bpp, bytes](TileSheetV3::SubSheet const &s) {
		return bytes == s.pixels.size() && valid(s, bpp);
	});
}

[[nodiscard]]
constexpr bool valid(TileSheetV3 const &ts) noexcept {
	return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}

constexpr void repair(TileSheetV3::SubSheet &ss, int const bpp) noexcept {
	auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	ss.pixels.resize(bytes);
	for (auto &s : ss.subsheets) {
		repair(s, bpp);
	}
}

constexpr ox::Error repair(TileSheetV3 &ts) noexcept {
	if (ts.bpp != 4 && ts.bpp != 8) {
		return ox::Error(1, "Unable to repair TileSheet");
	}
	repair(ts.subsheet, ts.bpp);
	return {};
}


struct TileSheetV4 {
	using SubSheetIdx = ox::Vector<std::size_t, 4>;

	struct SubSheet {
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
		static constexpr auto TypeVersion = 4;
		SubSheetId id = 0;
		ox::String name;
		int columns = 0;
		int rows = 0;
		ox::Vector<SubSheet> subsheets;
		ox::Vector<uint8_t> pixels;

		constexpr SubSheet() noexcept = default;
		SubSheet(
				SubSheetId pId,
				ox::StringParam pName,
				int pColumns,
				int pRows,
				int bpp) noexcept:
				id(pId),
				name(std::move(pName)),
				columns(pColumns),
				rows(pRows),
				pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
		}
		SubSheet(
				SubSheetId pId,
				ox::StringParam pName,
				int pColumns,
				int pRows,
				ox::Vector<uint8_t> pPixels) noexcept:
				id(pId),
				name(std::move(pName)),
				columns(pColumns),
				rows(pRows),
				pixels(std::move(pPixels)) {
		}

		/**
		 *
		 * @return the dimensional size of the SubSheet (e.g. width * height)
		 */
		[[nodiscard]]
		constexpr std::size_t size() const noexcept {
			return static_cast<std::size_t>(columns) * static_cast<std::size_t>(rows);
		}

	};

	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
	static constexpr auto TypeVersion = 4;
	int8_t bpp = 4;
	SubSheetId idIt = 0;
	ox::FileAddress defaultPalette;
	SubSheet subsheet{0, "Root", 1, 1, bpp};

	constexpr TileSheetV4() noexcept = default;

};

[[nodiscard]]
constexpr bool valid(TileSheetV4::SubSheet const &ss, int bpp) noexcept {
	auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
	return
		(ss.pixels.empty() || ss.subsheets.empty()) &&
		ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
				[bpp, bytes](TileSheetV4::SubSheet const &s) {
			return bytes == s.pixels.size() && valid(s, bpp);
		});
}

[[nodiscard]]
constexpr bool valid(TileSheetV4 const &ts) noexcept {
	return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet, ts.bpp);
}

constexpr void repair(TileSheetV4::SubSheet &ss, int const bpp) noexcept {
	if (ss.subsheets.empty()) {
		auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile) / (bpp == 4 ? 2 : 1);
		ss.pixels.resize(bytes);
	} else {
		ss.pixels.clear();
		ss.columns = -1;
		ss.rows = -1;
	}
	for (auto &s : ss.subsheets) {
		repair(s, bpp);
	}
}

constexpr ox::Error repair(TileSheetV4 &ts) noexcept {
	if (ts.bpp != 4 && ts.bpp != 8) {
		return ox::Error(1, "Unable to repair TileSheet");
	}
	repair(ts.subsheet, ts.bpp);
	return {};
}


struct TileSheetV5 {
	using SubSheetIdx = ox::Vector<uint32_t, 4>;

	struct SubSheet {
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.TileSheet.SubSheet";
		static constexpr auto TypeVersion = 5;
		SubSheetId id = 0;
		ox::String name;
		int columns = 0;
		int rows = 0;
		ox::Vector<SubSheet> subsheets;
		ox::Vector<uint8_t> pixels;

		constexpr SubSheet() noexcept = default;
		SubSheet(
				SubSheetId const pId,
				ox::StringParam pName,
				int const pColumns,
				int const pRows) noexcept:
				id(pId),
				name(std::move(pName)),
				columns(pColumns),
				rows(pRows),
				pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile)) {
		}
		SubSheet(
				SubSheetId const pId,
				ox::StringParam pName,
				int const pColumns,
				int const pRows,
				ox::Vector<uint8_t> pPixels) noexcept:
				id(pId),
				name(std::move(pName)),
				columns(pColumns),
				rows(pRows),
				pixels(std::move(pPixels)) {
		}

		/**
		 *
		 * @return the dimensional size of the SubSheet (e.g. width * height)
		 */
		[[nodiscard]]
		constexpr std::size_t size() const noexcept {
			return static_cast<std::size_t>(columns) * static_cast<std::size_t>(rows);
		}

	};

	static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.TileSheet";
	static constexpr auto TypeVersion = 5;
	/**
	 * bpp is unused for TileSheet, but it does get used in CompactTileSheet.
	 * All pixel in TileSheet are 8 bpp, regardless of what the bpp field says.
	 */
	int8_t bpp = 4;
	SubSheetId idIt = 0;
	ox::String defaultPalette;
	SubSheet subsheet{0, "Root", 1, 1};

};

[[nodiscard]]
constexpr bool valid(TileSheetV5::SubSheet const &ss) noexcept {
	if (ss.subsheets.empty()) {
		return std::cmp_equal(ss.columns * ss.rows * PixelsPerTile, ss.pixels.size());
	} else {
		return ss.pixels.empty() && ox::all_of(ss.subsheets.begin(), ss.subsheets.end(),
				[](TileSheetV5::SubSheet const &s) {
			return valid(s);
		});
	}
}

[[nodiscard]]
constexpr bool valid(TileSheetV5 const &ts) noexcept {
	return (ts.bpp == 4 || ts.bpp == 8) && valid(ts.subsheet);
}

constexpr void repair(TileSheetV5::SubSheet &ss) noexcept {
	if (ss.subsheets.empty()) {
		auto const bytes = static_cast<size_t>(ss.columns * ss.rows * PixelsPerTile);
		ss.pixels.resize(bytes);
	} else {
		ss.pixels.clear();
		ss.columns = -1;
		ss.rows = -1;
	}
	for (auto &s : ss.subsheets) {
		repair(s);
	}
}

constexpr ox::Error repair(TileSheetV5 &ts) noexcept {
	if (ts.bpp != 4 && ts.bpp != 8) {
		return ox::Error{1, "Unable to repair TileSheet"};
	}
	repair(ts.subsheet);
	return {};
}


using TileSheet = TileSheetV5;

[[nodiscard]]
std::size_t idx(TileSheet::SubSheet const &ss, ox::Point const &pt) noexcept;

[[nodiscard]]
size_t getTileCnt(TileSheet const &ts) noexcept;

[[nodiscard]]
TileSheet::SubSheet const*getSubsheet(TileSheet const &ts, SubSheetId id) noexcept;

[[nodiscard]]
ox::Optional<size_t> getTileIdx(TileSheet const &ts, SubSheetId id) noexcept;

[[nodiscard]]
uint8_t getPixel(TileSheet::SubSheet const &ss, std::size_t idx) noexcept;

[[nodiscard]]
uint8_t getPixel(TileSheet::SubSheet const &ss, ox::Point const &pt) noexcept;

void setPixel(TileSheet::SubSheet &ss, ox::Point const &pt, uint8_t palIdx) noexcept;

ox::Error setPixelCount(TileSheet::SubSheet &ss, std::size_t cnt) noexcept;

void flipX(TileSheet::SubSheet &ss, ox::Point const &a, ox::Point const &b) noexcept;

void flipY(TileSheet::SubSheet &ss, ox::Point const &a, ox::Point const &b) noexcept;

/**
 * Gets a count of the pixels in this sheet, and not that of its children.
 * @return a count of the pixels in this sheet
 */
[[nodiscard]]
unsigned pixelCnt(TileSheet::SubSheet const &ss) noexcept;

/**
 *
 * @param ss
 * @param sz size of Subsheet in tiles (not pixels)
 */
ox::Error resizeSubsheet(TileSheet::SubSheet &ss, ox::Size const &sz) noexcept;

/**
 * validateSubSheetIdx takes a SubSheetIdx and moves the index to the
 * preceding or parent sheet if the current corresponding sheet does
 * not exist.
 * @param ts
 * @param idx SubSheetIdx to validate and correct
 * @return a valid version of idx
 */
[[nodiscard]]
TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const &ts, TileSheet::SubSheetIdx idx) noexcept;

ox::Result<TileSheet::SubSheetIdx> getSubSheetIdx(TileSheet const &ts, SubSheetId pId) noexcept;

ox::Result<TileSheet::SubSheet*> getSubSheet(
		ox::SpanView<ox::StringView> const &idx,
		TileSheet &ts) noexcept;

ox::Result<TileSheet::SubSheet const*> getSubSheet(
		ox::SpanView<ox::StringView> const &idx,
		TileSheet const &ts) noexcept;

[[nodiscard]]
TileSheet::SubSheet &getSubSheet(
		ox::SpanView<uint32_t> const &idx,
		std::size_t idxIt,
		TileSheet::SubSheet &pSubsheet) noexcept;

#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdangling-reference"
#endif
[[nodiscard]]
TileSheet::SubSheet const &getSubSheet(TileSheet const &ts, ox::SpanView<uint32_t> const &idx) noexcept;

[[nodiscard]]
TileSheet::SubSheet &getSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx) noexcept;
#if defined(__GNUC__) && __GNUC__ >= 13
#pragma GCC diagnostic pop
#endif

ox::Error addSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept;

ox::Error insertSubSheet(TileSheet &ts, ox::SpanView<uint32_t> const &idx, TileSheet::SubSheet ss) noexcept;

ox::Error rmSubSheet(
		TileSheet &ts,
		TileSheet::SubSheetIdx const &idx,
		std::size_t idxIt,
		TileSheet::SubSheet &pSubsheet) noexcept;

ox::Error rmSubSheet(TileSheet &ts, TileSheet::SubSheetIdx const &idx) noexcept;

[[nodiscard]]
uint8_t getPixel(
		TileSheet const &ts,
		ox::Point const &pt,
		TileSheet::SubSheetIdx const &subsheetIdx) noexcept;

ox::Result<SubSheetId> getIdFor(TileSheet const &ts, ox::StringViewCR path) noexcept;

ox::Result<unsigned> getTileOffset(TileSheet const &ts, ox::StringViewCR pNamePath) noexcept;

ox::Result<uint32_t> getTileOffset(TileSheet const &ts, SubSheetId pId) noexcept;

ox::Result<ox::StringView> getNameFor(TileSheet::SubSheet const &ss, SubSheetId pId) noexcept;

ox::Result<ox::StringView> getNameFor(TileSheet &ts, SubSheetId pId) noexcept;

ox::Result<ox::StringView> getNameFor(TileSheet const &ts, SubSheetId pId) noexcept;

[[nodiscard]]
ox::Vector<uint8_t> pixels(TileSheet &ts) noexcept;


struct CompactTileSheetV1 {
	static constexpr auto TypeName = "net.drinkingtea.nostalgia.gfx.CompactTileSheet";
	static constexpr auto TypeVersion = 1;
	static constexpr auto Preloadable = true;
	int8_t bpp = 0;
	ox::FileAddress defaultPalette;
	ox::Vector<uint8_t> pixels;
};

[[nodiscard]]
constexpr bool valid(CompactTileSheetV1 const &ts) noexcept {
	return ts.bpp == 4 || ts.bpp == 8;
}


using CompactTileSheet = CompactTileSheetV1;

[[nodiscard]]
uint8_t getPixel4Bpp(
		CompactTileSheet const &ts,
		size_t idx) noexcept;

[[nodiscard]]
uint8_t getPixel8Bpp(
		CompactTileSheet const &ts,
		size_t idx) noexcept;

[[nodiscard]]
ox::Pair<uint8_t> get2Pixels4Bpp(
		CompactTileSheet const &ts,
		size_t idx) noexcept;

[[nodiscard]]
ox::Pair<uint8_t> get2Pixels8Bpp(
		CompactTileSheet const &ts,
		size_t idx) noexcept;

OX_MODEL_BEGIN(TileSheetV1)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(pal)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV2::SubSheet)
	OX_MODEL_FIELD(name)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(subsheets)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV2)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(subsheet)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV3::SubSheet)
	OX_MODEL_FIELD(name)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(subsheets)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV3)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(idIt)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(subsheet)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV4::SubSheet)
	OX_MODEL_FIELD(id)
	OX_MODEL_FIELD(name)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(subsheets)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV4)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(idIt)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(subsheet)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV5::SubSheet)
	OX_MODEL_FIELD(id)
	OX_MODEL_FIELD(name)
	OX_MODEL_FIELD(rows)
	OX_MODEL_FIELD(columns)
	OX_MODEL_FIELD(subsheets)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

OX_MODEL_BEGIN(TileSheetV5)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(idIt)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(subsheet)
OX_MODEL_END()

OX_MODEL_BEGIN(CompactTileSheetV1)
	OX_MODEL_FIELD(bpp)
	OX_MODEL_FIELD(defaultPalette)
	OX_MODEL_FIELD(pixels)
OX_MODEL_END()

ox::Vector<uint32_t> resizeTileSheetData(
		ox::Vector<uint32_t> const &srcPixels,
		ox::Size const &srcSize,
		int scale = 2) noexcept;

}
