/*
 * Copyright 2016 - 2024 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 <turbine/clipboard.hpp>
#include <keel/media.hpp>

#include <nostalgia/core/ptidxconv.hpp>

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

namespace nostalgia::core {

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

// 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();
	}
}

TileSheetEditorModel::TileSheetEditorModel(studio::StudioContext &sctx, ox::StringView path, studio::UndoStack &undoStack):
	m_sctx(sctx),
	m_tctx(m_sctx.tctx),
	m_path(path),
	m_img(*readObj<TileSheet>(keelCtx(m_tctx), 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);
}

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 = ox::Point(x, y);
			const auto idx = core::idx(s, pt);
			const auto c = getPixel(s, 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 == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
	const auto pt2 = ox::Point(s.columns * TileWidth, s.rows * TileHeight);
	turbine::setClipboardObject(m_tctx, std::move(cb));
	pushCommand(ox::make<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 = ox::Point(x, y);
			const auto &s = activeSubSheet();
			const auto idx = core::idx(s, pt);
			const auto c = getPixel(s, m_img.bpp, idx);
			pt.x -= m_selectionBounds.x;
			pt.y -= m_selectionBounds.y;
			cb->addPixel(pt, c);
		}
	}
	turbine::setClipboardObject(m_tctx, std::move(cb));
}

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

ox::StringView TileSheetEditorModel::palPath() const noexcept {
	auto [path, err] = m_img.defaultPalette.getPath();
	if (err) {
		return {};
	}
	constexpr ox::StringView uuidPrefix = "uuid://";
	if (ox::beginsWith(path, uuidPrefix)) {
		auto uuid = ox::StringView(path.data() + uuidPrefix.bytes(), path.bytes() - uuidPrefix.bytes());
		auto out = keelCtx(m_tctx).uuidToPath.at(uuid);
		if (out.error) {
			return {};
		}
		return *out.value;
	} else {
		return path;
	}
}

ox::Error TileSheetEditorModel::setPalette(ox::StringView path) noexcept {
	oxRequire(uuid, keelCtx(m_tctx).pathToUuid.at(path));
	pushCommand(ox::make<PaletteChangeCommand>(activeSubSheetIdx(), m_img, uuid->toString()));
	return {};
}

void TileSheetEditorModel::setPalettePage(size_t 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 palIdx) noexcept {
	const auto &activeSubSheet = getSubSheet(m_img, m_activeSubsSheetIdx);
	if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
		return;
	}
	const auto idx = core::idx(activeSubSheet, pt);
	if (m_ongoingDrawCommand) {
		m_updated = m_updated || m_ongoingDrawCommand->append(idx);
	} else if (getPixel(activeSubSheet, m_img.bpp, idx) != palIdx) {
		pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
	}
}

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

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

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

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

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

ox::Error TileSheetEditorModel::updateSubsheet(TileSheet::SubSheetIdx const&idx, ox::StringView const&name, int cols, int rows) noexcept {
	pushCommand(ox::make<UpdateSubSheetCommand>(m_img, idx, ox::String(name), cols, rows));
	return {};
}

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

void TileSheetEditorModel::fill(ox::Point const&pt, int palIdx) noexcept {
	const auto &s = getSubSheet(m_img, m_activeSubsSheetIdx);
	// build idx list
	ox::Array<bool, PixelsPerTile> updateMap = {};
	const auto oldColor = getPixel(s, 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 = core::idx(s, 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 (getPixel(s, m_img.bpp, pt) != palIdx) {
		pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
	}
}

void TileSheetEditorModel::select(ox::Point const&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(studio::UndoCommand const*cmd) noexcept {
	m_updated = true;
	const auto cmdId = cmd->commandId();
	if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
		oxReturnError(readObj<Palette>(keelCtx(m_tctx), m_img.defaultPalette).moveTo(m_pal));
		m_palettePage = ox::min<size_t>(m_pal->pages.size(), 0);
		paletteChanged.emit();
	}
	auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
	auto 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::Metal);
}

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, ox::Point const&pt, int oldColor) const noexcept {
	const auto &activeSubSheet = this->activeSubSheet();
	const auto tileIdx = [activeSubSheet](const ox::Point &pt) noexcept {
		return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
	};
	// get points
	const auto leftPt = pt + ox::Point(-1, 0);
	const auto rightPt = pt + ox::Point(1, 0);
	const auto topPt = pt + ox::Point(0, -1);
	const auto bottomPt = pt + ox::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) && getPixel(activeSubSheet, m_img.bpp, leftIdx) == oldColor) {
		getFillPixels(pixels, leftPt, oldColor);
	}
	if (!pixels[rightIdx % PixelsPerTile] && tile == tileIdx(rightPt) && getPixel(activeSubSheet, m_img.bpp, rightIdx) == oldColor) {
		getFillPixels(pixels, rightPt, oldColor);
	}
	if (!pixels[topIdx % PixelsPerTile] && tile == tileIdx(topPt) && getPixel(activeSubSheet, m_img.bpp, topIdx) == oldColor) {
		getFillPixels(pixels, topPt, oldColor);
	}
	if (!pixels[bottomIdx % PixelsPerTile] && tile == tileIdx(bottomPt) && getPixel(activeSubSheet, m_img.bpp, bottomIdx) == oldColor) {
		getFillPixels(pixels, bottomPt, oldColor);
	}
}

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

}
