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


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

#include <turbine/clipboard.hpp>
#include <keel/media.hpp>

#include <nostalgia/gfx/ptidxconv.hpp>

#include "commands/addsubsheetcommand.hpp"
#include "commands/commands.hpp"
#include "commands/cutpastecommand.hpp"
#include "commands/deletetilescommand.hpp"
#include "commands/drawcommand.hpp"
#include "commands/flipcommand.hpp"
#include "commands/inserttilescommand.hpp"
#include "commands/movesubsheetcommand.hpp"
#include "commands/palettechangecommand.hpp"
#include "commands/rmsubsheetcommand.hpp"
#include "commands/rotatecommand.hpp"
#include "commands/updatesubsheetcommand.hpp"
#include "tilesheeteditormodel.hpp"

namespace nostalgia::gfx {

// delete pixels of all non-leaf nodes
static void normalizeSubsheets(TileSheet::SubSheet &ss) noexcept {
	if (ss.subsheets.empty()) {
		for (auto &child : ss.subsheets) {
			normalizeSubsheets(child);
		}
	} else {
		ss.pixels.clear();
	}
}


Palette const TileSheetEditorModel::s_defaultPalette = {
	.colorNames = {ox::Vector<ox::String>{{}}},
	.pages = {{"Page 1", ox::Vector<Color16>(128)}},
};

TileSheetEditorModel::TileSheetEditorModel(
		studio::Context &sctx, ox::StringParam path, studio::UndoStack &undoStack):
		m_sctx{sctx},
		m_tctx{m_sctx.tctx},
		m_path{std::move(path)},
		m_img{m_sctx.project->loadObj<TileSheet>(m_path).unwrapThrow()},
		// ignore failure to load palette
		m_pal{readObj<Palette>(keelCtx(m_tctx), m_img.defaultPalette).value},
		m_undoStack{undoStack} {
	normalizeSubsheets(m_img.subsheet);
	m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
	m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
	setPalPath();
	m_sctx.project->fileMoved.connect(this, &TileSheetEditorModel::handleFileRename);
}

void TileSheetEditorModel::cut() {
	if (!m_selection) {
		return;
	}
	TileSheetClipboard blankCb;
	auto cb = ox::make_unique<TileSheetClipboard>();
	auto const &s = activeSubSheet();
	if (iterateSelectionRows(*m_selection, [&](int const x, int const y) {
		auto pt = ox::Point{x, y};
		auto const idx = gfx::idx(s, pt);
		if (idx >= s.pixels.size()) {
			return ox::Error{1, "invalid idx"};
		}
		auto const c = s.pixels[idx];
		pt -= m_selection->a;
		cb->addPixel(pt, c);
		blankCb.addPixel(pt, 0);
		return ox::Error{};
	})) {
		return;
	}
	auto const pt1 = m_selection->a;
	auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight};
	turbine::setClipboardObject(m_tctx, std::move(cb));
	if (auto cmd = ox::make_unique_catch<CutPasteCommand>(
		CommandId::Cut, m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb); cmd.ok()) {
		std::ignore = pushCommand(std::move(cmd.value));
	}
}

void TileSheetEditorModel::copy() {
	if (!m_selection) {
		return;
	}
	auto cb = ox::make_unique<TileSheetClipboard>();
	if (iterateSelectionRows(*m_selection, [&](int const x, int const y) {
		auto pt = ox::Point{x, y};
		auto const &s = activeSubSheet();
		auto const idx = gfx::idx(s, pt);
		if (idx >= s.pixels.size()) {
			return ox::Error{1, "invalid idx"};
		}
		auto const c = s.pixels[idx];
		pt -= m_selection->a;
		cb->addPixel(pt, c);
		return ox::Error{};
	})) {
		return;
	}
	turbine::setClipboardObject(m_tctx, std::move(cb));
}

void TileSheetEditorModel::paste() {
	if (!m_selection) {
		return;
	}
	auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(m_tctx);
	if (err) {
		oxLogError(err);
		oxErrf("Could not read clipboard: {}", toStr(err));
		return;
	}
	auto const &s = activeSubSheet();
	auto const pt1 = m_selection->a;
	auto const pt2 = ox::Point{s.columns * TileWidth, s.rows * TileHeight};
	if (auto cmd = ox::make_unique_catch<CutPasteCommand>(
		CommandId::Paste, m_img, m_activeSubsSheetIdx, pt1, pt2, *cb); cmd.value) {
		std::ignore = pushCommand(std::move(cmd.value));
	}
}

bool TileSheetEditorModel::acceptsClipboardPayload() const noexcept {
	auto const cb = getClipboardObject<TileSheetClipboard>(m_tctx);
	return cb.ok();
}

ox::String const &TileSheetEditorModel::palPath() const & noexcept {
	return m_palPath;
}

ox::Error TileSheetEditorModel::setPalette(ox::StringViewCR path) noexcept {
	OX_REQUIRE(uuid, keelCtx(m_tctx).pathToUuid.at(path));
	std::ignore = pushCommand(ox::make_unique<PaletteChangeCommand>(
		activeSubSheetIdx(), m_img, uuid->toString()));
	return {};
}

void TileSheetEditorModel::setPalettePage(size_t const pg) noexcept {
	m_palettePage = ox::clamp<size_t>(pg, 0, m_pal->pages.size() - 1);
	m_updated = true;
}

size_t TileSheetEditorModel::palettePage() const noexcept {
	return m_palettePage;
}

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

void TileSheetEditorModel::drawLineCommand(ox::Point const &pt, std::size_t const palIdx) noexcept {
	if (m_lastDrawUpdatePt == pt) {
		return;
	}
	auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
	if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
		return;
	}
	m_lastDrawUpdatePt = pt;
	auto const idx = gfx::idx(activeSubSheet, pt);
	if (m_ongoingDrawCommand) {
		m_ongoingDrawCommand->lineUpdate(m_lineStartPt, pt);
		m_updated = true;
	} else {
		std::ignore = pushCommand(ox::make_unique<DrawCommand>(
			m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
		m_lineStartPt = pt;
	}
}

void TileSheetEditorModel::endDrawCommand() noexcept {
	if (m_ongoingDrawCommand) {
		m_ongoingDrawCommand->finish();
		m_ongoingDrawCommand = nullptr;
		m_lastDrawUpdatePt = {-1, -1};
	}
}

void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const &parentIdx) noexcept {
	std::ignore = pushCommand(ox::make_unique<AddSubSheetCommand>(m_img, parentIdx));
}

void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const &idx) noexcept {
	std::ignore = pushCommand(ox::make_unique<RmSubSheetCommand>(m_img, idx));
}

void TileSheetEditorModel::insertTiles(
	TileSheet::SubSheetIdx const &idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept {
	std::ignore = pushCommand(ox::make_unique<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}

void TileSheetEditorModel::deleteTiles(
	TileSheet::SubSheetIdx const &idx, std::size_t const tileIdx, std::size_t const tileCnt) noexcept {
	std::ignore = pushCommand(ox::make_unique<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}

ox::Error TileSheetEditorModel::updateSubsheet(
	TileSheet::SubSheetIdx const &idx, ox::StringViewCR name, int const cols, int const rows) noexcept {
	OX_REQUIRE_M(cmd, ox::make_unique_catch<UpdateSubSheetCommand>(m_img, idx, name, cols, rows));
	std::ignore = pushCommand(std::move(cmd));
	return {};
}

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

void TileSheetEditorModel::fill(ox::Point const &pt, uint8_t const palIdx) noexcept {
	auto const &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
	// build idx list
	if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
		return;
	}
	ox::Array<bool, PixelsPerTile> updateMap = {};
	auto const oldColor = getPixel(activeSubSheet, pt);
	getFillPixels(activeSubSheet, updateMap, pt, oldColor);
	ox::Vector<std::size_t> idxList;
	auto i = gfx::idx(activeSubSheet, pt) / PixelsPerTile * PixelsPerTile;
	for (auto const 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 (getPixel(activeSubSheet, pt) != palIdx) {
		std::ignore = pushCommand(ox::make_unique<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
	}
}

ox::Error TileSheetEditorModel::rotateLeft() noexcept {
	auto const &ss = activeSubSheet();
    ox::Point pt1, pt2{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1};
	if (m_selection) {
		pt1 = m_selection->a;
		pt2 = m_selection->b;
	}
	return pushCommand(ox::make_unique<RotateCommand>(
		m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Left));
}

ox::Error TileSheetEditorModel::rotateRight() noexcept {
	auto const &ss = activeSubSheet();
    ox::Point pt1, pt2{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1};
	if (m_selection) {
		pt1 = m_selection->a;
		pt2 = m_selection->b;
	}
	return pushCommand(ox::make_unique<RotateCommand>(
		m_img, m_activeSubsSheetIdx, pt1, pt2, RotateCommand::Direction::Right));
}

void TileSheetEditorModel::setSelection(studio::Selection const &sel) noexcept {
	auto const &ss = activeSubSheet();
	if (sel.a.x < ss.columns * TileWidth && sel.a.y < ss.rows * TileHeight) {
		m_selection.emplace(sel);
	} else {
		m_selTracker.finishSelection();
		m_selection.reset();
	}
	m_updated = true;
}

void TileSheetEditorModel::select(ox::Point const &pt) noexcept {
	if (m_selTracker.updateCursorPoint(pt)) {
		setSelection(m_selTracker.selection());
	}
}

void TileSheetEditorModel::completeSelection() noexcept {
	if (m_selTracker.selectionOngoing()) {
		m_selTracker.finishSelection();
		m_selection.emplace(m_selTracker.selection());
		auto&pt = m_selection->b;
		auto const &s = activeSubSheet();
		pt.x = ox::min(s.columns * TileWidth - 1, pt.x);
		pt.y = ox::min(s.rows * TileHeight - 1, pt.y);
	}
}

void TileSheetEditorModel::clearSelection() noexcept {
	m_updated = true;
	m_selTracker.reset();
	m_selection.reset();
}

bool TileSheetEditorModel::hasSelection() const noexcept {
	return m_selection.has_value();
}

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

ox::Error TileSheetEditorModel::markUpdatedCmdId(studio::UndoCommand const *cmd) noexcept {
	m_updated = true;
	auto const cmdId = cmd->commandId();
	if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
		if (readObj<Palette>(keelCtx(m_tctx), m_img.defaultPalette).moveTo(m_pal)) {
			m_pal = keel::AssetRef<Palette>{};
		}
		m_palettePage = ox::min<size_t>(pal().pages.size(), 0);
		setPalPath();
		paletteChanged.emit();
	}
	auto const tsCmd = dynamic_cast<TileSheetCommand const*>(cmd);
	auto const idx = validateSubSheetIdx(m_img, tsCmd->subsheetIdx());
	if (idx != m_activeSubsSheetIdx) {
		setActiveSubsheet(idx);
	}
	return {};
}

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

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

ox::Error TileSheetEditorModel::saveFile() noexcept {
	return m_sctx.project->writeObj(m_path, m_img, ox::ClawFormat::Organic);
}

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

ox::Error TileSheetEditorModel::flipX() noexcept {
	auto const &ss = activeSubSheet();
	ox::Point a;
	ox::Point b{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1};
	if (m_selection) {
		a = m_selection->a;
		b = m_selection->b;
	}
	return pushCommand(ox::make_unique<FlipXCommand>(m_img, m_activeSubsSheetIdx, a, b));
}

ox::Error TileSheetEditorModel::flipY() noexcept {
	auto const &ss = activeSubSheet();
	ox::Point a;
	ox::Point b{ss.columns * TileWidth - 1, ss.rows * TileHeight - 1};
	if (m_selection) {
		a = m_selection->a;
		b = m_selection->b;
	}
	return pushCommand(ox::make_unique<FlipYCommand>(m_img, m_activeSubsSheetIdx, a, b));
}

bool TileSheetEditorModel::rotateEligible() const noexcept {
	if (m_selection) {
		auto const w = m_selection->b.x - m_selection->a.x;
		auto const h = m_selection->b.y - m_selection->a.y;
		return w == h;
	}
	auto const &ss = activeSubSheet();
	return ss.rows == ss.columns;
}

ox::Error TileSheetEditorModel::moveSubSheet(TileSheet::SubSheetIdx src, TileSheet::SubSheetIdx dst) noexcept {
	return pushCommand(ox::make_unique<MoveSubSheetCommand>(m_img, std::move(src), std::move(dst)));
}

void TileSheetEditorModel::getFillPixels(
		TileSheet::SubSheet const &activeSubSheet,
		ox::Span<bool> const pixels,
		ox::Point const &pt,
		uint8_t const oldColor) noexcept {
	auto const idx = ptToIdx(pt, activeSubSheet.columns);
	auto const relIdx = idx % PixelsPerTile;
	if (pixels[relIdx] || activeSubSheet.pixels[idx] != oldColor) {
		return;
	}
	// mark pixels to update
	pixels[relIdx] = true;
	if (pt.x % TileWidth != 0) {
		auto const leftPt = pt + ox::Point{-1, 0};
		getFillPixels(activeSubSheet, pixels, leftPt, oldColor);
	}
	if (pt.x % TileWidth != TileWidth - 1) {
		auto const rightPt = pt + ox::Point{1, 0};
		getFillPixels(activeSubSheet, pixels, rightPt, oldColor);
	}
	if (pt.y % TileHeight != 0) {
		auto const topPt = pt + ox::Point{0, -1};
		getFillPixels(activeSubSheet, pixels, topPt, oldColor);
	}
	if (pt.y % TileHeight != TileHeight - 1) {
		auto const bottomPt = pt + ox::Point{0, 1};
		getFillPixels(activeSubSheet, pixels, bottomPt, oldColor);
	}
}

void TileSheetEditorModel::setPalPath() noexcept {
	auto &path = m_img.defaultPalette;
	constexpr ox::StringView uuidPrefix = "uuid://";
	if (ox::beginsWith(path, uuidPrefix)) {
		auto const uuid = substr(path, uuidPrefix.bytes());
		auto const out = keelCtx(m_tctx).uuidToPath.at(uuid);
		if (!out.error) {
			m_palPath = *out.value;
		}
	} else {
		m_palPath = path;
	}
}

ox::Error TileSheetEditorModel::pushCommand(ox::UPtr<studio::UndoCommand> &&cmd) noexcept {
	m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd.get());
	std::ignore = m_undoStack.push(std::move(cmd));
	m_updated = true;
	return {};
}

ox::Error TileSheetEditorModel::handleFileRename(ox::StringViewCR, ox::StringViewCR newPath, ox::UUID const &id) noexcept {
	if ((beginsWith(m_img.defaultPalette, "uuid://") &&
			substr(m_img.defaultPalette, 7) == id.toString()) ||
		m_img.defaultPalette == newPath) {
		m_palPath = newPath;
	}
	return {};
}

}
