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


#include <ox/claw/read.hpp>
#include <ox/std/algorithm.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>

#include <nostalgia/core/clipboard.hpp>
#include <nostalgia/core/media.hpp>

#include "tilesheeteditormodel.hpp"

namespace nostalgia::core {

const Palette TileSheetEditorModel::s_defaultPalette = {
	.colors = ox::Vector<Color16>(128),
};

class TileSheetClipboard: public ClipboardObject<TileSheetClipboard> {
	public:
		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
		static constexpr auto TypeVersion = 1;

		oxModelFriend(TileSheetClipboard);

		struct Pixel {
			static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard.Pixel";
			static constexpr auto TypeVersion = 1;
			uint16_t colorIdx = 0;
			geo::Point pt;
			constexpr Pixel(uint16_t pColorIdx, geo::Point pPt) noexcept {
				colorIdx = pColorIdx;
				pt = pPt;
			}
		};
	protected:
		ox::Vector<Pixel> m_pixels;

	public:
		constexpr void addPixel(const geo::Point &pt, uint16_t colorIdx) noexcept {
			m_pixels.emplace_back(colorIdx, pt);
		}

		[[nodiscard]]
		constexpr const ox::Vector<Pixel> &pixels() const noexcept {
			return m_pixels;
		}
};

oxModelBegin(TileSheetClipboard::Pixel)
	oxModelField(colorIdx)
	oxModelField(pt)
oxModelEnd()

oxModelBegin(TileSheetClipboard)
	oxModelFieldRename(pixels, m_pixels)
oxModelEnd()


// Command IDs to use with QUndoCommand::id()
enum class CommandId {
	Draw = 1,
	AddSubSheet = 2,
	RmSubSheet  = 3,
	DeleteTile  = 4,
	InsertTile  = 4,
	UpdateSubSheet  = 5,
	Cut = 6,
	Paste = 7,
	PaletteChange = 8,
};

constexpr bool operator==(CommandId c, int i) noexcept {
	return static_cast<int>(c) == i;
}

constexpr bool operator==(int i, CommandId c) noexcept {
	return static_cast<int>(c) == i;
}

class TileSheetCommand: public studio::UndoCommand {
	public:
		[[nodiscard]]
		virtual const TileSheet::SubSheetIdx &subsheetIdx() const noexcept = 0;
};

class DrawCommand: public TileSheetCommand {
	private:
		struct Change {
			uint32_t idx = 0;
			uint16_t oldPalIdx = 0;
			constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
				idx = pIdx;
				oldPalIdx = pOldPalIdx;
			}
		};
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_subSheetIdx;
		ox::Vector<Change, 8> m_changes;
		int m_palIdx = 0;

	public:
		constexpr DrawCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, std::size_t idx, int palIdx) noexcept {
			m_img = img;
			auto &subsheet = m_img->getSubSheet(subSheetIdx);
			m_subSheetIdx = subSheetIdx;
			m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img->bpp, idx));
			m_palIdx = palIdx;
		}

		constexpr DrawCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, const ox::Vector<std::size_t> &idxList, int palIdx) noexcept {
			m_img = img;
			auto &subsheet = m_img->getSubSheet(subSheetIdx);
			m_subSheetIdx = subSheetIdx;
			for (const auto idx : idxList) {
				m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img->bpp, idx));
			}
			m_palIdx = palIdx;
		}

		constexpr auto append(std::size_t idx) noexcept {
			auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
			if (m_changes.back().value.idx != idx && subsheet.getPixel(m_img->bpp, idx) != m_palIdx) {
				// duplicate entries are bad
				auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](const auto &c) {
					return c.idx == idx;
				});
				if (existing == m_changes.cend()) {
					m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img->bpp, idx));
					subsheet.setPixel(m_img->bpp, idx, m_palIdx);
					return true;
				}
			}
			return false;
		}

		constexpr auto append(const ox::Vector<std::size_t> &idxList) noexcept {
			auto out = false;
			for (auto idx : idxList) {
				out = append(idx) || out;
			}
			return out;
		}

		void redo() noexcept final {
			auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
			for (const auto &c : m_changes) {
				subsheet.setPixel(m_img->bpp, c.idx, m_palIdx);
			}
		}

		void undo() noexcept final {
			auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
			for (const auto &c : m_changes) {
				subsheet.setPixel(m_img->bpp, c.idx, c.oldPalIdx);
			}
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::Draw);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_subSheetIdx;
		}

};

template<CommandId CommandId>
class CutPasteCommand: public TileSheetCommand {
	private:
		struct Change {
			uint32_t idx = 0;
			uint16_t newPalIdx = 0;
			uint16_t oldPalIdx = 0;
			constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
				idx = pIdx;
				newPalIdx = pNewPalIdx;
				oldPalIdx = pOldPalIdx;
			}
		};
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_subSheetIdx;
		ox::Vector<Change> m_changes;

	public:
		constexpr CutPasteCommand(TileSheet *img, const TileSheet::SubSheetIdx &subSheetIdx, const geo::Point &dstStart, const geo::Point &dstEnd, const TileSheetClipboard &cb) noexcept {
			m_img = img;
			m_subSheetIdx = subSheetIdx;
			const auto &subsheet = m_img->getSubSheet(subSheetIdx);
			for (const auto &p : cb.pixels()) {
				const auto dstPt = p.pt + dstStart;
				if (dstPt.x <= dstEnd.x && dstPt.y <= dstEnd.y) {
					const auto idx = subsheet.idx(dstPt);
					m_changes.emplace_back(static_cast<uint32_t>(idx), p.colorIdx, subsheet.getPixel(m_img->bpp, idx));
				}
			}
		}

		void redo() noexcept final {
			auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
			for (const auto &c : m_changes) {
				subsheet.setPixel(m_img->bpp, c.idx, c.newPalIdx);
			}
		}

		void undo() noexcept final {
			auto &subsheet = m_img->getSubSheet(m_subSheetIdx);
			for (const auto &c : m_changes) {
				subsheet.setPixel(m_img->bpp, c.idx, c.oldPalIdx);
			}
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_subSheetIdx;
		}

};

class AddSubSheetCommand: public TileSheetCommand {
	private:
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_parentIdx;
		ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;

	public:
		constexpr AddSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &parentIdx) noexcept {
			m_img = img;
			m_parentIdx = parentIdx;
			auto &parent = m_img->getSubSheet(m_parentIdx);
			if (parent.subsheets.size()) {
				auto idx = m_parentIdx;
				idx.emplace_back(parent.subsheets.size());
				m_addedSheets.push_back(idx);
			} else {
				auto idx = m_parentIdx;
				idx.emplace_back(0u);
				m_addedSheets.push_back(idx);
				idx.back().value = 1;
				m_addedSheets.push_back(idx);
			}
		}

		void redo() noexcept final {
			auto &parent = m_img->getSubSheet(m_parentIdx);
			if (m_addedSheets.size() < 2) {
				auto i = parent.subsheets.size();
				parent.subsheets.emplace_back(ox::sfmt("Subsheet {}", i).c_str(), 1, 1, m_img->bpp);
			} else {
				parent.subsheets.emplace_back("Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
				parent.rows = 0;
				parent.columns = 0;
				parent.subsheets.emplace_back("Subsheet 1", 1, 1, m_img->bpp);
			}
		}

		void undo() noexcept final {
			auto &parent = m_img->getSubSheet(m_parentIdx);
			if (parent.subsheets.size() == 2) {
				auto s = parent.subsheets[0];
				parent.rows = s.rows;
				parent.columns = s.columns;
				parent.pixels = std::move(s.pixels);
				parent.subsheets.clear();
			} else {
				for (auto idx = m_addedSheets.rbegin(); idx != m_addedSheets.rend(); ++idx) {
					oxLogError(m_img->rmSubSheet(*idx));
				}
			}
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::AddSubSheet);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_parentIdx;
		}

};

class RmSubSheetCommand: public TileSheetCommand {
	private:
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_idx;
		TileSheet::SubSheetIdx m_parentIdx;
		TileSheet::SubSheet m_sheet;

	public:
		constexpr RmSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx) noexcept {
			m_img = img;
			m_idx = idx;
			m_parentIdx = idx;
			m_parentIdx.pop_back();
			auto &parent = m_img->getSubSheet(m_parentIdx);
			m_sheet = parent.subsheets[idx.back().value];
		}

		void redo() noexcept final {
			auto &parent = m_img->getSubSheet(m_parentIdx);
			oxLogError(parent.subsheets.erase(m_idx.back().value).error);
		}

		void undo() noexcept final {
			auto &parent = m_img->getSubSheet(m_parentIdx);
			auto i = m_idx.back().value;
			parent.subsheets.insert(i, m_sheet);
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::RmSubSheet);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_idx;
		}

};

class InsertTilesCommand: public TileSheetCommand {
	private:
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_idx;
		std::size_t m_insertPos = 0;
		std::size_t m_insertCnt = 0;
		ox::Vector<uint8_t> m_deletedPixels = {};

	public:
		constexpr InsertTilesCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
			const unsigned bytesPerTile = img->bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
			m_img = img;
			m_idx = idx;
			m_insertPos = tileIdx * bytesPerTile;
			m_insertCnt = tileCnt * bytesPerTile;
			m_deletedPixels.resize(m_insertCnt);
			// copy pixels to be erased
			{
				auto &s = m_img->getSubSheet(m_idx);
				auto &p = s.pixels;
				auto dst = m_deletedPixels.data();
				auto src = p.data() + p.size() - m_insertCnt;
				const auto sz = m_insertCnt * sizeof(decltype(p[0]));
				ox_memcpy(dst, src, sz);
			}
		}

		void redo() noexcept final {
			auto &s = m_img->getSubSheet(m_idx);
			auto &p = s.pixels;
			auto dstPos = m_insertPos + m_insertCnt;
			const auto dst = p.data() + dstPos;
			const auto src = p.data() + m_insertPos;
			ox_memmove(dst, src, p.size() - dstPos);
			ox_memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
		}

		void undo() noexcept final {
			auto &s = m_img->getSubSheet(m_idx);
			auto &p = s.pixels;
			const auto srcIdx = m_insertPos + m_insertCnt;
			const auto src = p.data() + srcIdx;
			const auto dst1 = p.data() + m_insertPos;
			const auto dst2 = p.data() + p.size() - m_insertCnt;
			const auto sz = p.size() - srcIdx;
			ox_memmove(dst1, src, sz);
			ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::InsertTile);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_idx;
		}

};

class DeleteTilesCommand: public TileSheetCommand {
	private:
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_idx;
		std::size_t m_deletePos = 0;
		std::size_t m_deleteSz = 0;
		ox::Vector<uint8_t> m_deletedPixels = {};

	public:
		constexpr DeleteTilesCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
			const unsigned bytesPerTile = img->bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
			m_img = img;
			m_idx = idx;
			m_deletePos = tileIdx * bytesPerTile;
			m_deleteSz = tileCnt * bytesPerTile;
			m_deletedPixels.resize(m_deleteSz);
			// copy pixels to be erased
			{
				auto &s = m_img->getSubSheet(m_idx);
				auto &p = s.pixels;
				auto dst = m_deletedPixels.data();
				auto src = p.data() + m_deletePos;
				const auto sz = m_deleteSz * sizeof(decltype(p[0]));
				ox_memcpy(dst, src, sz);
			}
		}

		void redo() noexcept final {
			auto &s = m_img->getSubSheet(m_idx);
			auto &p = s.pixels;
			auto srcPos = m_deletePos + m_deleteSz;
			const auto src = p.data() + srcPos;
			const auto dst1 = p.data() + m_deletePos;
			const auto dst2 = p.data() + (p.size() - m_deleteSz);
			ox_memmove(dst1, src, p.size() - srcPos);
			ox_memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
		}

		void undo() noexcept final {
			auto &s = m_img->getSubSheet(m_idx);
			auto &p = s.pixels;
			const auto src = p.data() + m_deletePos;
			const auto dst1 = p.data() + m_deletePos + m_deleteSz;
			const auto dst2 = src;
			const auto sz = p.size() - m_deletePos - m_deleteSz;
			ox_memmove(dst1, src, sz);
			ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::DeleteTile);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_idx;
		}

};

class UpdateSubSheetCommand: public TileSheetCommand {
	private:
		TileSheet *m_img = nullptr;
		TileSheet::SubSheetIdx m_idx;
		TileSheet::SubSheet m_sheet;
		ox::String m_newName;
		int m_newCols = 0;
		int m_newRows = 0;

	public:
		constexpr UpdateSubSheetCommand(TileSheet *img, const TileSheet::SubSheetIdx &idx,
		                                const ox::String &name, int cols, int rows) noexcept {
			m_img = img;
			m_idx = idx;
			m_sheet = img->getSubSheet(idx);
			m_newName = name;
			m_newCols = cols;
			m_newRows = rows;
		}

		void redo() noexcept final {
			auto &sheet = m_img->getSubSheet(m_idx);
			sheet.name = m_newName;
			sheet.columns = m_newCols;
			sheet.rows = m_newRows;
			oxLogError(sheet.setPixelCount(m_img->bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
		}

		void undo() noexcept final {
			auto &sheet = m_img->getSubSheet(m_idx);
			sheet = m_sheet;
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::UpdateSubSheet);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_idx;
		}

};

class PaletteChangeCommand: public TileSheetCommand {
	private:
		TileSheet::SubSheetIdx m_idx;
		TileSheet *m_img = nullptr;
		ox::FileAddress m_oldPalette;
		ox::FileAddress m_newPalette;

	public:
		PaletteChangeCommand(const TileSheet::SubSheetIdx &idx, TileSheet *img, const ox::String &newPalette) noexcept {
			m_idx = idx;
			m_img = img;
			m_oldPalette = m_img->defaultPalette;
			m_newPalette = newPalette;
		}

		void redo() noexcept final {
			m_img->defaultPalette = m_newPalette;
		}

		void undo() noexcept final {
			m_img->defaultPalette = m_oldPalette;
		}

		[[nodiscard]]
		int commandId() const noexcept final {
			return static_cast<int>(CommandId::PaletteChange);
		}

		[[nodiscard]]
		const TileSheet::SubSheetIdx &subsheetIdx() const noexcept override {
			return m_idx;
		}

};


TileSheetEditorModel::TileSheetEditorModel(Context *ctx, ox::String path) {
	m_ctx = ctx;
	m_path = std::move(path);
	oxRequireT(img, readObj<TileSheet>(ctx, m_path.c_str()));
	m_img = *img;
	if (m_img.defaultPalette) {
		oxThrowError(readObj<Palette>(ctx, m_img.defaultPalette).moveTo(&m_pal));
	}
	m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
	m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}

void TileSheetEditorModel::cut() {
	TileSheetClipboard blankCb;
	auto cb = ox::make_unique<TileSheetClipboard>();
	const auto s = activeSubSheet();
	for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
		for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
			auto pt = geo::Point(x, y);
			const auto idx = s->idx(pt);
			const auto c = s->getPixel(m_img.bpp, idx);
			pt.x -= m_selectionBounds.x;
			pt.y -= m_selectionBounds.y;
			cb->addPixel(pt, c);
			blankCb.addPixel(pt, 0);
		}
	}
	const auto pt1 = m_selectionOrigin == geo::Point(-1, -1) ? geo::Point(0, 0) : m_selectionOrigin;
	const auto pt2 = geo::Point(s->columns * TileWidth, s->rows * TileHeight);
	setClipboardObject(m_ctx, std::move(cb));
	pushCommand(new CutPasteCommand<CommandId::Cut>(&m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}

void TileSheetEditorModel::copy() {
	auto cb = ox::make_unique<TileSheetClipboard>();
	for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
		for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
			auto pt = geo::Point(x, y);
			const auto s = activeSubSheet();
			const auto idx = s->idx(pt);
			const auto c = s->getPixel(m_img.bpp, idx);
			pt.x -= m_selectionBounds.x;
			pt.y -= m_selectionBounds.y;
			cb->addPixel(pt, c);
		}
	}
	setClipboardObject(m_ctx, std::move(cb));
}

void TileSheetEditorModel::paste() {
	auto [cb, err] = getClipboardObject<TileSheetClipboard>(m_ctx);
	if (err) {
		oxLogError(err);
		oxErrf("Could not read clipboard: {}", toStr(err));
		return;
	}
	const auto s = activeSubSheet();
	const auto pt1 = m_selectionOrigin == geo::Point(-1, -1) ? geo::Point(0, 0) : m_selectionOrigin;
	const auto pt2 = geo::Point(s->columns * TileWidth, s->rows * TileHeight);
	pushCommand(new CutPasteCommand<CommandId::Paste>(&m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}

const ox::FileAddress &TileSheetEditorModel::palPath() const noexcept {
	return m_img.defaultPalette;
}

ox::Error TileSheetEditorModel::setPalette(const ox::String &path) noexcept {
	pushCommand(new PaletteChangeCommand(activeSubSheetIdx(), &m_img, path));
	return OxError(0);
}

void TileSheetEditorModel::drawCommand(const geo::Point &pt, std::size_t palIdx) noexcept {
	const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
	if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
		return;
	}
	const auto idx = activeSubSheet.idx(pt);
	if (m_ongoingDrawCommand) {
		m_updated = m_updated || m_ongoingDrawCommand->append(idx);
	} else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
		pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
	}
}

void TileSheetEditorModel::endDrawCommand() noexcept {
	m_ongoingDrawCommand = nullptr;
}

void TileSheetEditorModel::addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept {
	pushCommand(new AddSubSheetCommand(&m_img, parentIdx));
}

void TileSheetEditorModel::rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept {
	pushCommand(new RmSubSheetCommand(&m_img, idx));
}

void TileSheetEditorModel::insertTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
	pushCommand(new InsertTilesCommand(&m_img, idx, tileIdx, tileCnt));
}

void TileSheetEditorModel::deleteTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
	pushCommand(new DeleteTilesCommand(&m_img, idx, tileIdx, tileCnt));
}

ox::Error TileSheetEditorModel::updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::String &name, int cols, int rows) noexcept {
	pushCommand(new UpdateSubSheetCommand(&m_img, idx, name, cols, rows));
	return OxError(0);
}

void TileSheetEditorModel::setActiveSubsheet(const TileSheet::SubSheetIdx &idx) noexcept {
	m_activeSubsSheetIdx = idx;
	this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}

void TileSheetEditorModel::fill(const geo::Point &pt, int palIdx) noexcept {
	const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx);
	// build idx list
	ox::Array<bool, PixelsPerTile> updateMap = {};
	const auto oldColor = s.getPixel(m_img.bpp, pt);
	if (pt.x >= s.columns * TileWidth || pt.y >= s.rows * TileHeight) {
		return;
	}
	getFillPixels(updateMap.data(), pt, oldColor);
	ox::Vector<std::size_t> idxList;
	auto i = s.idx(pt) / PixelsPerTile * PixelsPerTile;
	for (auto u : updateMap) {
		if (u) {
			idxList.emplace_back(i);
		}
		++i;
	}
	// do updates to sheet
	if (m_ongoingDrawCommand) {
		m_updated = m_updated || m_ongoingDrawCommand->append(idxList);
	} else if (s.getPixel(m_img.bpp, pt) != palIdx) {
		pushCommand(new DrawCommand(&m_img, m_activeSubsSheetIdx, idxList, palIdx));
	}
}

void TileSheetEditorModel::select(const geo::Point &pt) noexcept {
	if (!m_selectionOngoing) {
		m_selectionOrigin = pt;
		m_selectionOngoing = true;
		m_selectionBounds = {pt, pt};
		m_updated = true;
	} else if (m_selectionBounds.pt2() != pt) {
		m_selectionBounds = {m_selectionOrigin, pt};
		m_updated = true;
	}
}

void TileSheetEditorModel::completeSelection() noexcept {
	m_selectionOngoing = false;
	auto s = activeSubSheet();
	auto pt = m_selectionBounds.pt2();
	pt.x = ox::min(s->columns * TileWidth, pt.x);
	pt.y = ox::min(s->rows * TileHeight, pt.y);
	m_selectionBounds.setPt2(pt);
}

void TileSheetEditorModel::clearSelection() noexcept {
	m_updated = true;
	m_selectionOrigin = {-1, -1};
	m_selectionBounds = {{-1, -1}, {-1, -1}};
}

bool TileSheetEditorModel::updated() const noexcept {
	return m_updated;
}

ox::Error TileSheetEditorModel::markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept {
	m_updated = true;
	const auto cmdId = cmd->commandId();
	if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
		oxReturnError(readObj<Palette>(m_ctx, m_img.defaultPalette.getPath().value).moveTo(&m_pal));
	}
	auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
	auto idx = m_img.validateSubSheetIdx(tsCmd->subsheetIdx());
	if (idx != m_activeSubsSheetIdx) {
		setActiveSubsheet(idx);
	}
	return OxError(0);
}

ox::Error TileSheetEditorModel::markUpdated() noexcept {
	m_updated = true;
	return OxError(0);
}

void TileSheetEditorModel::ackUpdate() noexcept {
	m_updated = false;
}

ox::Error TileSheetEditorModel::saveFile() noexcept {
	const auto sctx = applicationData<studio::StudioContext>(m_ctx);
	oxReturnError(sctx->project->writeObj(m_path, &m_img));
	return m_ctx->assetManager.setAsset(m_path, m_img).error;
}

bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
	const auto s = activeSubSheet();
	const auto pt = idxToPt(static_cast<int>(idx), s->columns);
	return m_selectionBounds.contains(pt);
}

void TileSheetEditorModel::getFillPixels(bool *pixels, const geo::Point &pt, int oldColor) const noexcept {
	const auto &activeSubSheet = *this->activeSubSheet();
	const auto tileIdx = [activeSubSheet](const geo::Point &pt) noexcept {
		return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
	};
	// get points
	const auto leftPt = pt + geo::Point(-1, 0);
	const auto rightPt = pt + geo::Point(1, 0);
	const auto topPt = pt + geo::Point(0, -1);
	const auto bottomPt = pt + geo::Point(0, 1);
	// calculate indices
	const auto idx = ptToIdx(pt, activeSubSheet.columns);
	const auto leftIdx = ptToIdx(leftPt, activeSubSheet.columns);
	const auto rightIdx = ptToIdx(rightPt, activeSubSheet.columns);
	const auto topIdx = ptToIdx(topPt, activeSubSheet.columns);
	const auto bottomIdx = ptToIdx(bottomPt, activeSubSheet.columns);
	const auto tile = tileIdx(pt);
	// mark pixels to update
	pixels[idx % PixelsPerTile] = true;
	if (!pixels[leftIdx % PixelsPerTile] && tile == tileIdx(leftPt) && activeSubSheet.getPixel(m_img.bpp, leftIdx) == oldColor) {
		getFillPixels(pixels, leftPt, oldColor);
	}
	if (!pixels[rightIdx % PixelsPerTile] && tile == tileIdx(rightPt) && activeSubSheet.getPixel(m_img.bpp, rightIdx) == oldColor) {
		getFillPixels(pixels, rightPt, oldColor);
	}
	if (!pixels[topIdx % PixelsPerTile] && tile == tileIdx(topPt) && activeSubSheet.getPixel(m_img.bpp, topIdx) == oldColor) {
		getFillPixels(pixels, topPt, oldColor);
	}
	if (!pixels[bottomIdx % PixelsPerTile] && tile == tileIdx(bottomPt) && activeSubSheet.getPixel(m_img.bpp, bottomIdx) == oldColor) {
		getFillPixels(pixels, bottomPt, oldColor);
	}
}

void TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept {
	m_undoStack.push(cmd);
	m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd);
	m_updated = true;
}

}
