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

#include <imgui.h>

#include <ox/std/conv.hpp>
#include <ox/std/ignore.hpp>

#include <keel/media.hpp>

#include <studio/context.hpp>
#include <studio/imguiutil.hpp>

#include "commands/addrmobjectset.hpp"
#include "commands/editsize.hpp"
#include "commands/modifytiles.hpp"
#include "tileclipboard.hpp"

#include "worldeditor-imgui.hpp"

namespace jasper::world {

namespace ig = studio::ig;

constexpr auto SqrBtnSize = ImVec2{ig::BtnSz.y, ig::BtnSz.y};

struct WorldEditorConfig {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.studio.WorldEditorConfig";
	static constexpr auto TypeVersion = 1;
	bool animateBg{};
};

OX_MODEL_BEGIN(WorldEditorConfig)
	OX_MODEL_FIELD(animateBg);
OX_MODEL_END()


struct WorldTileDragDrop {
	static constexpr auto TypeName = "net.drinkingtea.jasper.world.studio.WorldTileDragDrop";
	static constexpr auto TypeVersion = 1;
	uint64_t setId{};
	ObjectId objId{};
};

OX_MODEL_BEGIN(WorldTileDragDrop)
	OX_MODEL_FIELD(setId)
	OX_MODEL_FIELD(objId)
OX_MODEL_END()


[[nodiscard]]
static ox::Vec2 fbPos(ox::Vec2 fbPos) noexcept {
    auto const winPos = ImGui::GetWindowPos();
    fbPos.x -= winPos.x;
    fbPos.y -= winPos.y;
    return fbPos;
}

[[nodiscard]]
constexpr ox::Point fbPtToTileAddr(
		ox::Vec2 const &fbPt,
		ox::Vec2 const &mapSz) noexcept {
	constexpr auto ColumnsOnScrn = 15;
	constexpr auto RowsOnScrn = 10;
	auto const fbX = static_cast<float>(fbPt.x);
	auto const fbY = static_cast<float>(fbPt.y);
	auto const tw = mapSz.x / static_cast<float>(ColumnsOnScrn);
	auto const th = mapSz.y / static_cast<float>(RowsOnScrn);
	return {
		static_cast<int>(fbX / tw),
		static_cast<int>(fbY / th),
	};
}

WorldEditorImGui::WorldEditorImGui(studio::Context &sctx, ox::StringParam path):
		Editor{sctx, std::move(path)},
		m_sctx{sctx},
		m_objSetPicker{"Object Set Chooser", keelCtx(m_sctx), FileExt_jwob},
		m_doc{*keel::readObj<WorldDoc>(keelCtx(m_sctx), itemPath()).unwrapThrow()} {
	OX_THROW_ERROR(m_loader.loadWorldStatic());
	OX_THROW_ERROR(m_view.setupWorld());
	OX_THROW_ERROR(loadObjectSets());
	m_sctx.project->fileUpdated.connect(this, &WorldEditorImGui::handleDepUpdate);
	m_loader.resourcesUpdated.connect(&m_view, &WorldEditorView::setupWorld);
	m_loader.tileUpdated.connect(&m_view, &WorldEditorView::setupTile);
	oxLogError(studio::headerizeConfigFile<WorldEditorConfig>(keelCtx(m_sctx), itemPath()));
	studio::openConfig<WorldEditorConfig>(
			keelCtx(m_sctx), itemPath(), [this](WorldEditorConfig const &config) {
				m_view.setAnimate(config.animateBg);
			});
	undoStack()->changeTriggered.connect(this, &WorldEditorImGui::handleUndoStackChange);
}

void WorldEditorImGui::draw(studio::Context&) noexcept {
	auto const paneSize = ImGui::GetContentRegionAvail();
	{
		constexpr auto resourcesWidth = 300.f;
		ig::ChildStackItem const worldView{"WorldView", {paneSize.x - resourcesWidth, 0}};
		drawWorldView();
	}
	ImGui::SameLine();
	{
		ig::ChildStackItem const worldView{"RightPane"};
		drawMenu();
		ImGui::Separator();
		drawResources();
	}
	if (auto o = m_objSetPicker.draw(m_sctx)) {
		oxLogError(addObjSet(*o));
	}
	drawPropEditor();
}

void WorldEditorImGui::onActivated() noexcept {
}

void WorldEditorImGui::copy() {
	if (m_selection) {
		auto const assetId = this->assetId().unwrapThrow();
		auto mods = ox::make_unique<TileClipboard>(assetId, m_selection->size());
		studio::iterateSelection(*m_selection, [this, &mods](int32_t x, int32_t y) {
			auto &t = tile(m_doc, m_activeLayer, x, y);
			x -= m_selection->a.x;
			y -= m_selection->a.y;
			mods->addTile({
				  .layer = m_activeLayer,
				  .tileAddr = {x, y},
				  .objId = t.obj.worldObjectId,
				  .setId = t.obj.worldObjectSetId,
			});
		});
		setClipboardObject(m_sctx.tctx, std::move(mods));
	}
}

void WorldEditorImGui::cut() noexcept {
	if (m_selection) {
		copy();
		ox::Vector<ModifyTilesCommand::Mod> mods;
		studio::iterateSelection(*m_selection, [this, &mods](int32_t x, int32_t y) {
			mods.emplace_back(ModifyTilesCommand::Mod{
				  .layer = m_activeLayer,
				  .tileAddr = {x, y},
				  .objId = 0,
				  .setId = 0,
			});
		});
		std::ignore = pushCommand<ModifyTilesCommand>(
				m_loader,
				m_doc,
				std::move(mods));
	}
}

void WorldEditorImGui::paste() {
	if (m_selection) {
		auto const cb = getClipboardObject<TileClipboard>(m_sctx.tctx).unwrapThrow();
		auto const selSz = cb->selectionSize();
		auto &sel = *m_selection;
		sel.b.x = ox::min(sel.a.x + selSz.width, m_doc.columns - 1);
		sel.b.y = ox::min(sel.a.y + selSz.height, m_doc.rows - 1);
		m_view.setSelection(sel);
		ox::Vector<ModifyTilesCommand::Mod> mods;
		for (auto const &m : cb->tiles()) {
			mods.emplace_back(ModifyTilesCommand::Mod{
				.layer = m_activeLayer,
				.tileAddr = m.tileAddr + sel.a,
				.objId = m.objId,
				.setId = m.setId,
			});
		}
		std::ignore = pushCommand<ModifyTilesCommand>(
				m_loader,
				m_doc,
				std::move(mods));
	}
}

bool WorldEditorImGui::acceptsClipboardPayload() const noexcept {
	auto const [cb, noCb] = getClipboardObject<TileClipboard>(m_sctx.tctx);
	if (noCb) {
		return false;
	}
	return !cb->assetId().isNull() && cb->assetId() == assetId().or_value({});
}

ox::Error WorldEditorImGui::saveItem() noexcept {
	return m_sctx.project->writeObj(itemPath(), m_doc, ox::ClawFormat::Organic);
}

void WorldEditorImGui::drawObjSelector() noexcept {
	ig::IDStackItem const idStackItem("ObjSelector");
	if (ig::PushButton("+", SqrBtnSize)) {
		m_objSetPicker.open();
	}
	ImGui::SameLine();
	ImGui::BeginDisabled(m_objMgr.selectedObjSet.size() == 0);
	if (ig::PushButton("-", SqrBtnSize)) {
		rmObjSet();
	}
	ImGui::EndDisabled();
	ImGui::BeginChild("ObjTree");
	{
		for (auto const &[uuidUrl, setId, set] : m_objSets) {
			auto flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
			auto [setName, err] = uuidUrlToPath(keelCtx(m_sctx), uuidUrl);
			auto const uuid = substr(uuidUrl, 7);
			if (err) {
				setName = uuidUrl;
			}
			if (uuid == m_objMgr.selectedObjSet) {
				flags |= ImGuiTreeNodeFlags_Selected;
			}
			if (ImGui::TreeNodeEx(setName.c_str(), flags)) {
				if (ImGui::IsItemClicked()) {
					m_objMgr.selectedObjSet = uuid;
				}
				for (auto const &obj : set->objects) {
					if (ImGui::TreeNodeEx(obj.name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
						if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
							studio::navigateTo(m_sctx, setName, obj.name);
						}
						ImGui::TreePop();
					}
					ig::dragDropSource([&] {
						std::ignore = ig::setDragDropPayload("WorldTile", WorldTileDragDrop{
							.setId = setId,
							.objId = obj.id,
						});
						ImGui::Text("%s", obj.name.c_str());
					});
				}
				ImGui::TreePop();
			} else {
				if (ImGui::IsItemClicked()) {
					m_objMgr.selectedObjSet = uuid;
				}
			}
		}
	}
	ImGui::EndChild();
	if (ig::DragDropTarget const d; d) {
		auto const [fr, err] = ig::getDragDropPayload<studio::FileRef>("FileRef");
		if (!err && endsWith(fr.path, FileExt_jwob)) {
			std::ignore = addObjSet(fr.path);
		}
	}
}

void WorldEditorImGui::drawPropEditor() noexcept {
	if (m_sizeEditor.show) {
		constexpr auto popupSz = ImVec2{285, 0};
		ig::IDStackItem const idStackItem("EditMapSize");
		if (ig::BeginPopup(m_sctx.tctx, "Edit Map Size", m_sizeEditor.show, popupSz)) {
			ImGui::InputInt("Map Width", &m_sizeEditor.columns, 1);
			ImGui::InputInt("Map Height", &m_sizeEditor.rows, 1);
			bool const changed = m_doc.columns != m_sizeEditor.columns
			                  || m_doc.rows != m_sizeEditor.rows;
			if (ig::PopupControlsOkCancel(popupSz.x, m_sizeEditor.show) == ig::PopupResponse::OK) {
				if (changed) {
					std::ignore = pushCommand<EditWorldSizeCommand>(
							m_loader, m_doc, ox::Size{m_sizeEditor.columns, m_sizeEditor.rows});
				}
			}
			ImGui::EndPopup();
		}
	}
}

void WorldEditorImGui::drawMenu() noexcept {
	ig::IDStackItem const idStackItem("Menu");
	if (ImGui::Button("Resize")) {
		m_sizeEditor.show = true;
		m_sizeEditor.columns = m_doc.columns;
		m_sizeEditor.rows = m_doc.rows;
	}
	ImGui::SameLine();
	{
		bool animate = m_view.animate();
		if (ImGui::Checkbox("Animate", &animate) && !animate) {
			oxLogError(m_view.setupWorld());
		}
		m_view.setAnimate(animate);
		studio::editConfig<WorldEditorConfig>(
				keelCtx(m_sctx), itemPath(),
				[animate](WorldEditorConfig &config) {
					config.animateBg = animate;
				});
	}
}

void WorldEditorImGui::drawResources() noexcept {
	ig::IDStackItem const idStackItem("Resources");
	drawObjSelector();
}

void WorldEditorImGui::drawWorldView() noexcept {
	ig::IDStackItem const idStackItem("WorldView");
	auto const paneSize = ImGui::GetContentRegionAvail();
	m_view.draw({static_cast<int>(paneSize.x), static_cast<int>(paneSize.y)});
	auto &fb = m_view.framebuffer();
	auto const fbWidth = static_cast<float>(fb.width);
	auto const fbHeight = static_cast<float>(fb.height);
	auto const srcH = fbHeight / fbWidth;
	auto const dstH = paneSize.y / paneSize.x;
	float xScale{}, yScale{};
	float fbPaneScale{};
	if (dstH > srcH) {
		// crop off width
		xScale = srcH / dstH;
		yScale = 1;
		fbPaneScale = paneSize.y / fbHeight;
	} else {
		auto const srcW = fbWidth / fbHeight;
		auto const dstW = (paneSize.x / paneSize.y);
		xScale = 1;
		yScale = srcW / dstW;
		fbPaneScale = paneSize.x / fbWidth;
	}
	ImGui::Image(
			ig::toImTextureID(fb.color.id),
			paneSize,
			{0, 1},
			{xScale, 1 - yScale});
	std::ignore = ig::dragDropTarget([this, fbPaneScale] {
		return handleDrop(fbPaneScale);
	});
	handleMouseSelection(fbPaneScale);
	if (ig::mainWinHasFocus()) {
		if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
			if (ImGui::IsKeyPressed(ImGuiKey_G)) {
				clearSelection();
			} else if (ImGui::IsKeyPressed(ImGuiKey_A)) {
				m_selection.emplace(ox::Point{0, 0}, ox::Point{m_doc.columns - 1, m_doc.rows - 1});
				m_view.setSelection(*m_selection);
				setCopyEnabled(true);
				setCutEnabled(true);
				setPasteEnabled(true);
			}
		}
	}
}

void WorldEditorImGui::handleMouseSelection(float const fbPaneScale) noexcept {
	if (ImGui::IsMouseDown(0)) {
		auto const fbPos = world::fbPos(ox::Vec2{ImGui::GetMousePos()});
		auto const startSel = ImGui::IsMouseClicked(0) && ImGui::IsItemHovered();
		auto const scaledViewSz = ox::Vec2{m_view.drawSize()} * fbPaneScale;
		m_selTracker.updateCursorPoint(fbPtToTileAddr(fbPos, scaledViewSz), startSel);
		if (m_selTracker.selectionOngoing()) {
			m_view.setSelection(m_selTracker.selection());
		}
	} else if (ImGui::IsMouseReleased(0) && m_selTracker.selectionOngoing()) {
		m_selTracker.finishSelection();
		m_selection.emplace(m_selTracker.selection());
		setCopyEnabled(true);
		setCutEnabled(true);
		setPasteEnabled(true);
	}
}

ox::Error WorldEditorImGui::handleDrop(float const fbPaneScale) noexcept {
	OX_REQUIRE(objId, ig::getDragDropPayload<WorldTileDragDrop>("WorldTile"));
	auto const fbPos = world::fbPos(ox::Vec2{ImGui::GetMousePos()});
	auto const viewSz = m_view.drawSize();
	auto const tileAddr = fbPtToTileAddr(
			fbPos,
			{
					static_cast<float>(viewSz.width) * fbPaneScale,
					static_cast<float>(viewSz.height) * fbPaneScale});
	if (tileAddr.x < m_doc.columns && tileAddr.y < m_doc.rows) {
		ox::Vector<ModifyTilesCommand::Mod> mods;
		if (tileSelected(tileAddr)) {
			studio::iterateSelection(*m_selection, [&](int32_t x, int32_t y) {
				mods.emplace_back(ModifyTilesCommand::Mod{
					.layer = m_activeLayer,
					.tileAddr = {x, y},
					.objId = objId.objId,
					.setId = objId.setId,
				});
			});
		} else {
			mods = {
				{
					.layer = m_activeLayer,
					.tileAddr = tileAddr,
					.objId = objId.objId,
					.setId = objId.setId,
				},
			};
		}
		std::ignore = pushCommand<ModifyTilesCommand>(
				m_loader,
				m_doc,
				std::move(mods));
	}
	return {};
}

ox::Error WorldEditorImGui::addObjSet(ox::StringViewCR path) noexcept {
	OX_REQUIRE(uuid, getUuid(keelCtx(m_sctx), path));
	std::ignore = pushCommand<AddObjectSet>(m_doc, uuid);
	return loadObjectSets();
}

void WorldEditorImGui::rmObjSet() noexcept {
	auto const idx = ox::find_if(
		m_doc.objSets.begin(), m_doc.objSets.end(), [this](ObjectSetEntry const &e) {
			return substr(e.path, 7) == m_objMgr.selectedObjSet;
		});
	if (idx != m_doc.objSets.end()) {
		std::ignore = pushCommand<RmObjectSet>(m_doc, idx.offset());
	}
}

ox::Error WorldEditorImGui::handleDepUpdate(ox::StringViewCR, ox::UUID const &uuid) noexcept {
	auto &kctx = keelCtx(m_sctx);
	auto const objSetMatches = [&uuid, &kctx](ObjectSetEntry const &set) {
		auto const [setUuid, err] = getUuid(kctx, set.path);
		return !err && setUuid == uuid;
	};
	auto const depMatches = [&uuid](ox::SmallMap<ox::UUID, bool>::Pair const &set) {
		return set.key == uuid;
	};
	auto const depUpdated = ox::any_of(m_doc.objSets.begin(), m_doc.objSets.end(), objSetMatches)
	                   || ox::any_of(m_dependencies.pairs().begin(), m_dependencies.pairs().end(), depMatches);
	if (depUpdated) {
		OX_RETURN_ERROR(m_loader.loadWorldStatic());
		OX_RETURN_ERROR(loadObjectSets());
	}
	return {};
}

ox::Error WorldEditorImGui::loadObjectSets() noexcept {
	m_objSets.clear();
	m_dependencies.clear();
	auto &kctx = keelCtx(m_sctx);
	for (auto const &set : m_doc.objSets) {
		OX_REQUIRE_M(os, readObj<WorldObjectSet>(kctx, set.path));
		oxLogError(addDependency(os->tilesheet));
		for (auto const &pal : os->palettes) {
			oxLogError(addDependency(pal));
		}
		m_objSets.emplace_back(ox::String(set.path), set.id, std::move(os));
	}
	return {};
}

bool WorldEditorImGui::tileSelected(ox::Point const &pt) const noexcept {
	return m_selection && m_selection->contains(pt);
}

void WorldEditorImGui::clearSelection() noexcept {
	setCopyEnabled(false);
	setCutEnabled(false);
	setPasteEnabled(false);
	m_selection.reset();
	m_view.clearSelection();
}

ox::Result<ox::UUID> WorldEditorImGui::assetId() const noexcept {
	return getUuid(keelCtx(m_sctx), itemPath());
}

ox::Error WorldEditorImGui::addDependency(ox::FileAddress const &fileAddr) noexcept {
	auto &kctx = keelCtx(m_sctx);
	OX_REQUIRE(uuid, getUuid(kctx, fileAddr));
	m_dependencies[uuid] = true;
	return {};
}

ox::Error WorldEditorImGui::handleUndoStackChange(studio::UndoCommand const*cmd) noexcept {
	if (dynamic_cast<AddObjectSet const*>(cmd)) {
		return loadObjectSets();
	}
	if (dynamic_cast<RmObjectSet const*>(cmd)) {
		return loadObjectSets();
	}
	return {};
}

}
