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

#include <algorithm>
#include <filesystem>

#include <imgui.h>

#include <ox/std/def.hpp>

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

#include <studio/configio.hpp>
#include "clawviewer.hpp"
#include "filedialogmanager.hpp"
#include "font.hpp"
#include "studioui.hpp"

#include <complex>

#ifdef OX_OS_Darwin
#define STUDIO_CTRL "Cmd"
#else
#define STUDIO_CTRL "Ctrl"
#endif

namespace studio {

static bool shutdownHandler(turbine::Context &ctx) {
	auto sctx = turbine::applicationData<Context>(ctx);
	return sctx->ui.handleShutdown();
}

namespace ig {
extern bool s_mainWinHasFocus;
}
static ox::Vector<Module const*> g_modules;

ox::Vector<Module const*> const &modules() noexcept {
	return g_modules;
}

void registerModule(Module const *mod) noexcept {
	if (mod) {
		g_modules.emplace_back(mod);
	}
}


struct StudioConfigV1 {
	static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
	static constexpr auto TypeVersion = 1;
	ox::String projectPath;
	ox::String activeTabItemName;
	ox::Vector<ox::String> openFiles;
	bool showProjectExplorer = true;
};

OX_MODEL_BEGIN(StudioConfigV1)
	OX_MODEL_FIELD_RENAME(activeTabItemName, active_tab_item_name)
	OX_MODEL_FIELD_RENAME(projectPath, project_path)
	OX_MODEL_FIELD_RENAME(openFiles, open_files)
	OX_MODEL_FIELD_RENAME(showProjectExplorer, show_project_explorer)
OX_MODEL_END()


struct StudioConfigV2 {
	static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
	static constexpr auto TypeVersion = 2;
	struct ProjectConfig {
		static constexpr auto TypeName = "net.drinkingtea.studio.ProjectConfig";
		static constexpr auto TypeVersion = 2;
		ox::String projectPath;
		ox::String activeTabItemName;
		ox::Vector<ox::String> openFiles;
	};
	ox::Vector<ProjectConfig> projects;
	bool showProjectExplorer = true;

	[[nodiscard]]
	constexpr ProjectConfig const *project() const {
		return projects.empty() ? nullptr : &projects[0];
	}

	[[nodiscard]]
	constexpr ProjectConfig *project() {
		return projects.empty() ? nullptr : &projects[0];
	}
};

OX_MODEL_BEGIN(StudioConfigV2::ProjectConfig)
	OX_MODEL_FIELD_RENAME(activeTabItemName, active_tab_item_name)
	OX_MODEL_FIELD_RENAME(projectPath, project_path)
	OX_MODEL_FIELD_RENAME(openFiles, open_files)
OX_MODEL_END()

OX_MODEL_BEGIN(StudioConfigV2)
	OX_MODEL_FIELD(projects)
	OX_MODEL_FIELD_RENAME(showProjectExplorer, show_project_explorer)
OX_MODEL_END()

static ox::Error convertStudioConfigV1ToStudioConfigV2(
		keel::Context&,
		StudioConfigV1 &src,
		StudioConfigV2 &dst) noexcept {
	dst.projects.emplace_back(StudioConfigV2::ProjectConfig{
		.projectPath = std::move(src.projectPath),
		.activeTabItemName = std::move(src.activeTabItemName),
		.openFiles = std::move(src.openFiles),
	});
	dst.showProjectExplorer = src.showProjectExplorer;
	return {};
}

using StudioConfig = StudioConfigV2;


StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept:
		m_sctx{*this, tctx},
		m_projectDataDir{std::move(projectDataDir)} {
	auto &kctx = keelCtx(m_tctx);
	kctx.converters.emplace_back(keel::Converter::make<convertStudioConfigV1ToStudioConfigV2>());
	oxLogError(headerizeConfigFile<StudioConfigV1>(kctx));
	turbine::setApplicationData(m_tctx, &m_sctx);
	turbine::setShutdownHandler(m_tctx, shutdownHandler);
	m_sctx.navCallback = [this](ox::StringParam filePath, ox::StringParam navArgs) {
		handleNavigationChange(std::move(filePath), std::move(navArgs));
	};
	m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
	m_projectExplorer.addDir.connect(this, &StudioUI::addDir);
	m_projectExplorer.addItem.connect(this, &StudioUI::addFile);
	m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile);
	m_projectExplorer.getInfo.connect(this, &StudioUI::getFileInfo);
	m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile);
	m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg);
	m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove);
	m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove);
	m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove);
	m_newProject.finished.connect(this, &StudioUI::createOpenProject);
	m_newMenu.finished.connect(this, &StudioUI::openFile);
	m_closeAppConfirm.response.connect(this, &StudioUI::handleCloseAppResponse);
	m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse);
	m_removeRecentProject.dlg.response.connect(this, &StudioUI::handleRemoveRecentProjectResponse);
	loadModules();
	// open project and files
	auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_tctx));
	m_showProjectExplorer = config.showProjectExplorer;
	if (!err) {
		if (auto const pc = config.project()) {
			oxLogError(openProjectPath(pc->projectPath));
		}
	} else {
		if constexpr(!ox::defines::Debug) {
			oxErrf("Could not open studio config file: {}: {}\n", err.errCode, toStr(err));
		} else {
			oxErrf(
					"Could not open studio config file: {}: {} ({}:{})\n",
					err.errCode, toStr(err), err.src.file_name(), err.src.line());
		}
	}
}

void StudioUI::handleKeyEvent(turbine::Key const key, bool const down) noexcept {
	if (m_activeEditor && !ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
		m_activeEditor->keyStateChanged(key, down);
	}
}

void StudioUI::handleMouseButtonEvent(int const btn, bool const down) noexcept {
	if (down) {
		if (btn == 3) { // back button
			navigateBack(m_sctx);
		} else if (btn == 4) { // forward button
			navigateForward(m_sctx);
		}
	}
}

void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept {
	m_navAction.emplace(std::move(path), std::move(navArgs));
}

void StudioUI::preDraw() noexcept {
	loadFont();
}

void StudioUI::draw() noexcept {
	if (turbine::isWayland() || ox::defines::OS == ox::OS::Darwin) {
		ig::setDpiScale(1.f);
	} else {
		ig::setDpiScale(ImGui::GetWindowDpiScale());
	}
	glutils::clearScreen();
	drawMenu();
	auto const &viewport = *ImGui::GetMainViewport();
	ImGui::SetNextWindowPos(viewport.WorkPos);
	ImGui::SetNextWindowSize(viewport.WorkSize);
	ImGui::SetNextWindowViewport(viewport.ID);
	auto constexpr windowFlags =
		ImGuiWindowFlags_NoTitleBar |
		ImGuiWindowFlags_NoResize |
		ImGuiWindowFlags_NoMove |
		ImGuiWindowFlags_NoScrollbar |
		ImGuiWindowFlags_NoSavedSettings;
	ImGui::Begin("MainWindow##Studio", nullptr, windowFlags);
	{
		ig::s_mainWinHasFocus = ImGui::IsWindowFocused(
			ImGuiFocusedFlags_RootAndChildWindows | ImGuiFocusedFlags_NoPopupHierarchy);
		if (m_showProjectExplorer) {
			auto const v = ImGui::GetContentRegionAvail();
			m_projectExplorer.draw(m_sctx, {300 * ig::dpiScale(), v.y});
			ImGui::SameLine();
		}
		drawTabBar();
		for (auto const w : m_widgets) {
			w->draw(m_sctx);
		}
	}
	ImGui::End();
	handleKeyInput();
	m_taskRunner.update(m_tctx);
	procFileMoves();
}

void StudioUI::loadFont() noexcept {
	auto const scale = turbine::isWayland() || ox::defines::OS == ox::OS::Darwin ?
		1.f : turbine::scale(m_tctx);
	if (m_fontScale != scale) {
		m_fontScale = scale;
		ImFontConfig fontCfg;
		fontCfg.FontDataOwnedByAtlas = false;
		auto const &io = ImGui::GetIO();
		auto const font = files::RobotoMedium_ttf();
		// const_cast is needed because this data is definitely const,
		// but AddFontFromMemoryTTF requires a mutable buffer.
		// However, setting fontCfg.FontDataOwnedByAtlas ensures
		// that it will still be treated as const.
		// ImGui documentation recognizes that this is a bad design,
		// and hopefully it will change at some point.
		io.Fonts->AddFontFromMemoryTTF(
			const_cast<uint8_t*>(font.data()),
			static_cast<int>(font.size()),
			13 * scale,
			&fontCfg);
	}
}

bool StudioUI::handleShutdown() noexcept {
	auto const out = ox::all_of(m_editors.begin(), m_editors.end(), [](ox::UPtr<BaseEditor> const &e) {
		return !e->unsavedChanges();
	});
	if (!out) {
		m_closeAppConfirm.open();
	}
	return out;
}

void StudioUI::drawMenu() noexcept {
	if (ImGui::BeginMainMenuBar()) {
		if (ImGui::BeginMenu("File")) {
			if (ImGui::MenuItem("New...", STUDIO_CTRL "+N", false, m_project)) {
				m_newMenu.open();
			}
			if (ImGui::MenuItem("New Project...", STUDIO_CTRL "+Shift+N")) {
				m_newProject.open();
			}
			if (ImGui::MenuItem("Open Project...", STUDIO_CTRL "+O")) {
				m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
			}
			if (ImGui::MenuItem("Reload Project")) {
				oxLogError(openProjectPath(m_project->projectPath()));
			}
			if (ImGui::BeginMenu("Recent Projects", m_recentProjects.size() > 1)) {
				for (size_t i = 1; i < m_recentProjects.size(); ++i) {
					auto const &p = m_recentProjects[i];
					if (ImGui::MenuItem(p.c_str())) {
						if (openProjectPath(p)) {
							m_removeRecentProject.idx = i;
							m_removeRecentProject.dlg.open();
						}
					}
				}
				ImGui::EndMenu();
			}
			if (ImGui::MenuItem(
					"Save",
					STUDIO_CTRL "+S",
					false,
					m_activeEditor && m_activeEditor->unsavedChanges())) {
				m_activeEditor->save();
			}
			if (ImGui::MenuItem("Quit", STUDIO_CTRL "+Q")) {
				turbine::requestShutdown(m_tctx);
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("Edit")) {
			auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
			if (ImGui::MenuItem(
					"Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) {
				oxLogError(undoStack->undo());
			}
			if (ImGui::MenuItem(
					"Redo", STUDIO_CTRL "+Y", false, undoStack && undoStack->canRedo())) {
				oxLogError(undoStack->redo());
			}
			ImGui::Separator();
			if (ImGui::MenuItem(
					"Copy",
					STUDIO_CTRL "+C",
					false,
					m_activeEditor && m_activeEditor->copyEnabled())) {
				m_activeEditor->copy();
			}
			if (ImGui::MenuItem(
					"Cut",
					STUDIO_CTRL "+X",
					false,
					m_activeEditor && m_activeEditor->cutEnabled())) {
				m_activeEditor->cut();
			}
			if (ImGui::MenuItem(
					"Paste",
					STUDIO_CTRL "+V",
					false,
					m_activeEditor && m_activeEditor->pasteEnabled())) {
				m_activeEditor->paste();
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("View")) {
			if (ImGui::MenuItem(
					"Project Explorer", STUDIO_CTRL "+Shift+1", m_showProjectExplorer)) {
				toggleProjectExplorer();
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("Navigate")) {
			constexpr auto backShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+[" : "Alt+Left Arrow";
			constexpr auto fwdShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+]" : "Alt+Right Arrow";
			if (ImGui::MenuItem(
				"Back", backShortcut, false,
				m_sctx.navIdx && *m_sctx.navIdx > 1)) {
				navigateBack(m_sctx);
			}
			if (ImGui::MenuItem(
				"Forward", fwdShortcut, false,
				m_sctx.navIdx && *m_sctx.navIdx < m_sctx.navStack.size())) {
				navigateForward(m_sctx);
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("Help")) {
			if (ImGui::MenuItem("About")) {
				m_aboutPopup.open();
			}
			ImGui::EndMenu();
		}
		ImGui::EndMainMenuBar();
	}
}

void StudioUI::drawTabBar() noexcept {
	auto const viewport = ImGui::GetContentRegionAvail();
	ImGui::BeginChild("TabWindow##MainWindow##Studio", ImVec2(viewport.x, viewport.y));
	constexpr auto tabBarFlags = ImGuiTabBarFlags_TabListPopupButton;
	if (ImGui::BeginTabBar("TabBar##TabWindow##MainWindow##Studio", tabBarFlags)) {
		drawTabs();
		ImGui::EndTabBar();
	}
	ImGui::EndChild();
}

void StudioUI::drawTabs() noexcept {
	for (auto it = m_editors.begin(); it != m_editors.end();) {
		auto const &e = *it;
		auto open = true;
		auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
		auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0;
		auto const flags = unsavedChanges | selected | ImGuiTabItemFlags_NoAssumedClosure;
		if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
			if (m_activeEditor != e.get()) [[unlikely]] {
				m_activeEditor = e.get();
				studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
					if (auto const pc = config.project()) {
						pc->activeTabItemName = m_activeEditor->itemPath();
					}
				});
				turbine::requireRefreshWithin(m_tctx, 0);
			} else [[likely]] {
				if (m_activeEditorUpdatePending == e.get()) [[unlikely]] {
					m_activeEditorUpdatePending = nullptr;
				}
				if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
					m_activeEditor->onActivated();
					if (!m_sctx.navIdx ||
						*m_sctx.navIdx == m_sctx.navStack.size() ||
						(*m_sctx.navIdx < m_sctx.navStack.size() &&
							m_sctx.navStack[*m_sctx.navIdx].filePath != m_activeEditor->itemPath())) {
						auto constexpr defaultVal =
							ox::MaxValue<ox::remove_reference_t<decltype(*m_sctx.navIdx)>>;
						auto &navIdx = m_sctx.navIdx.emplace(m_sctx.navIdx.or_value(defaultVal));
						++navIdx;
						m_sctx.navStack.resize(navIdx);
						m_sctx.navStack.emplace_back(
							ox::String{m_activeEditor->itemPath()},
							ox::String{});
					}
				}
				if (m_closeActiveTab) [[unlikely]] {
					ImGui::SetTabItemClosed(e->itemDisplayName().c_str());
				} else if (open) [[likely]] {
					e->draw(m_sctx);
				}
				m_activeEditorOnLastDraw = e.get();
			}
			ImGui::EndTabItem();
		}
		if (!open) {
			if (e->unsavedChanges()) {
				m_closeFileConfirm.open();
				++it;
			} else {
				e->close();
				if (m_activeEditor == (*it).get()) {
					m_activeEditor = nullptr;
				}
				if (auto const err = m_editors.erase(it).moveTo(it)) {
					oxErrf("Editor tab deletion failed: {} ({}:{})\n", toStr(err), err.src.file_name(), err.src.line());
				}
			}
		} else {
			++it;
		}
	}
	if (m_closeActiveTab) [[unlikely]] {
		if (m_activeEditor) {
			auto const idx = find_if(
				m_editors.begin(), m_editors.end(),
				[this](ox::UPtr<BaseEditor> const &e) {
					return m_activeEditor == e.get();
				});
			if (idx != m_editors.end()) {
				oxLogError(m_editors.erase(idx.offset()).error);
			}
			m_activeEditor = nullptr;
		}
		m_closeActiveTab = false;
	}
	if (m_navAction) {
		if (!openFile(m_navAction->path)) {
			m_activeEditor->navigateTo(m_navAction->args);
			m_navAction.reset();
		}
	}
}

void StudioUI::loadEditorMaker(EditorMaker const &editorMaker) noexcept {
	for (auto const &ext : editorMaker.fileTypes) {
		m_editorMakers[ext] = editorMaker.make;
	}
}

void StudioUI::loadModule(Module const &mod) noexcept {
	for (auto const &editorMaker : mod.editors(m_sctx)) {
		loadEditorMaker(editorMaker);
	}
	for (auto &im : mod.itemMakers(m_sctx)) {
		m_newMenu.addItemMaker(std::move(im));
	}
	auto tmplts = mod.itemTemplates(m_sctx);
	for (auto &t : tmplts) {
		m_newMenu.installItemTemplate(std::move(t));
	}
}

void StudioUI::loadModules() noexcept {
	for (auto const mod : g_modules) {
		loadModule(*mod);
	}
}

void StudioUI::toggleProjectExplorer() noexcept {
	m_showProjectExplorer = !m_showProjectExplorer;
	editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
		config.showProjectExplorer = m_showProjectExplorer;
	});
}

void StudioUI::redo() noexcept {
	auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
	if (undoStack && undoStack->canRedo()) {
		 oxLogError(m_activeEditor->undoStack()->redo());
	}
}

void StudioUI::undo() noexcept {
	auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
	if (undoStack && undoStack->canUndo()) {
		 oxLogError(m_activeEditor->undoStack()->undo());
	}
}

void StudioUI::save() noexcept {
	if (m_activeEditor && m_activeEditor->unsavedChanges()) {
		m_activeEditor->save();
	}
}

void StudioUI::handleKeyInput() noexcept {
	if (ImGui::IsKeyDown(ImGuiKey_ModCtrl)) {
		if (ImGui::IsKeyPressed(ImGuiKey_C)) {
			if (m_activeEditor && m_activeEditor->copyEnabled()) {
				m_activeEditor->copy();
			}
		} else if (ImGui::IsKeyPressed(ImGuiKey_N)) {
			if (turbine::buttonDown(m_tctx, turbine::Key::Mod_Shift)) {
				m_newProject.open();
			} else {
				m_newMenu.open();
			}
		} else if (ImGui::IsKeyPressed(ImGuiKey_O)) {
			m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
		} else if (ImGui::IsKeyPressed(ImGuiKey_S)) {
			save();
		} else if (ImGui::IsKeyPressed(ImGuiKey_Q)) {
			turbine::requestShutdown(m_tctx);
		} else if (ImGui::IsKeyPressed(ImGuiKey_V)) {
			if (m_activeEditor && m_activeEditor->pasteEnabled()) {
				m_activeEditor->paste();
			}
		} else if (ImGui::IsKeyPressed(ImGuiKey_W)) {
			if (m_activeEditor) {
				if (m_activeEditor->unsavedChanges()) {
					m_closeFileConfirm.open();
				} else {
					oxLogError(closeCurrentFile());
				}
			}
		} else if (ImGui::IsKeyPressed(ImGuiKey_X)) {
			if (m_activeEditor && m_activeEditor->cutEnabled()) {
				m_activeEditor->cut();
			}
		} else if (ImGui::IsKeyPressed(ImGuiKey_Y)) {
			auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
			if (undoStack) { oxLogError(undoStack->redo()); }
		} else if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
			auto const undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
			if (undoStack) { oxLogError(undoStack->undo()); }
		} else if (ImGui::IsKeyPressed(ImGuiKey_1) && ImGui::IsKeyDown(ImGuiKey_ModShift)) {
			toggleProjectExplorer();
		} else {
			if (!m_editors.empty()) {
				auto const range = ox::min<size_t>(9u, m_editors.size()) - 1;
				for (auto i = 0u; i < 9; ++i) {
					if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(static_cast<int>(ImGuiKey_1) + i))) {
						m_activeEditor = m_editors[i < m_editors.size() ? i : range].get();
						m_activeEditorUpdatePending = m_activeEditor;
					}
				}
			}
		}
		if constexpr (ox::defines::OS == ox::OS::Darwin) {
			if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) {
				navigateBack(m_sctx);
			} else if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) {
				navigateForward(m_sctx);
			}
		}
	}
	if constexpr (ox::defines::OS != ox::OS::Darwin) {
		if (ImGui::IsKeyDown(ImGuiKey_ModAlt)) {
			if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
				navigateBack(m_sctx);
			} else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
				navigateForward(m_sctx);
			}
		}
	}
}

ox::Error StudioUI::addDir(ox::StringViewCR path) noexcept {
	m_newDirDialog.openPath(path);
	return {};
}

ox::Error StudioUI::addFile(ox::StringViewCR path) noexcept {
	m_newMenu.openPath(path);
	return {};
}

ox::Error StudioUI::deleteFile(ox::StringViewCR path) noexcept {
	m_deleteConfirmation.openPath(path);
	return {};
}

ox::Error StudioUI::getFileInfo(ox::StringViewCR path) noexcept {
	return m_fileInfo.open(path);
}

ox::Error StudioUI::renameFile(ox::StringViewCR path) noexcept {
	return m_renameFile.openPath(path);
}

ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR newPath, ox::UUID const &) noexcept {
	for (auto &f : m_openFiles) {
		if (f == oldPath) {
			f = newPath;
			editConfig<StudioConfig>(keelCtx(m_sctx), [&](StudioConfig &cfg) {
				if (auto const pc = cfg.project()) {
					auto p = find(pc->openFiles.begin(), pc->openFiles.end(), oldPath);
					*p = newPath;
					pc->activeTabItemName = newPath;
				}
			});
			break;
		}
	}
	// needed to keep this tab open, ImGui loses track when the name changes
	if (m_activeEditor) {
		m_activeEditorUpdatePending = m_activeEditor;
	}
	return m_projectExplorer.refreshProjectTreeModel();
}

ox::Error StudioUI::handleDeleteDir(ox::StringViewCR path) noexcept {
	auto const p = sfmt("{}/", path);
	std::ignore = m_editors.erase(
		std::remove_if(
			m_editors.begin(), m_editors.end(),
			[&](ox::UPtr<BaseEditor> const &e) {
				if (beginsWith(e->itemPath(), p)) {
					oxLogError(closeFile(path));
					if (e.get() != m_activeEditor) {
						return true;
					}
					m_closeActiveTab = true;
				}
				return false;
			}));
	return m_projectExplorer.refreshProjectTreeModel();
}

ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept {
	std::ignore = m_editors.erase(
		std::remove_if(
			m_editors.begin(), m_editors.end(),
			[&](ox::UPtr<BaseEditor> const &e) {
				if (path == e->itemPath()) {
					oxLogError(closeFile(path));
					if (e.get() != m_activeEditor) {
						return true;
					}
					m_closeActiveTab = true;
				}
				return false;
			}));
	return m_projectExplorer.refreshProjectTreeModel();
}

ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept {
	std::error_code ec;
	std::filesystem::create_directories(toStdStringView(path), ec);
	OX_RETURN_ERROR(ox::Error(ec.value() != 0, "Could not create project directory"));
	OX_RETURN_ERROR(openProjectPath(path));
	return m_project->writeTypeStore();
}

ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
	OX_REQUIRE_M(fs, keel::loadRomFs(path.view()));
	m_project.reset();
	m_openFiles.clear();
	m_editors.clear();
	keel::DuplicateSet ds;
	OX_RETURN_ERROR(keel::setRomFs(keelCtx(m_tctx), std::move(fs), ds));
	if (ds.size()) {
		ox::String msg{"Multiple files have the same UUID:\n"};
		for (auto const &k : ds.keys()) {
			msg += ox::sfmt("\n\t{}:\n", k.toString());
			for (auto const &v : ds[k]) {
				msg += ox::sfmt("\t\t - {}\n", v);
			}
		}
		m_messagePopup.show(msg);
	}
	OX_RETURN_ERROR(
		ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir)
				.moveTo(m_project));
	m_sctx.project = m_project.get();
	m_activeEditor = nullptr;
	m_activeEditorOnLastDraw = nullptr;
	m_activeEditorUpdatePending = nullptr;
	turbine::setWindowTitle(
		m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath()));
	m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
	m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem);
	m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);
	m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
	m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
	m_project->dirDeleted.connect(this, &StudioUI::handleDeleteDir);
	m_project->fileDeleted.connect(this, &StudioUI::handleDeleteFile);
	m_project->fileMoved.connect(this, &StudioUI::handleMoveFile);
	studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
		auto const pcIt = std::find_if(
			config.projects.begin(), config.projects.end(),
			[this](StudioConfig::ProjectConfig const &pc) {
				return pc.projectPath == m_project->projectPath();
			});
		if (pcIt != config.projects.end()) {
			auto p = std::move(*pcIt);
			std::ignore = config.projects.erase(pcIt);
			auto &pc = *config.projects.emplace(0, std::move(p));
			std::ignore = pc.openFiles.erase(
				std::remove_if(
					pc.openFiles.begin(), pc.openFiles.end(),
					[&](ox::String const &f) {
						auto const openFileErr = openFileActiveTab(f, pc.activeTabItemName == f);
						if (openFileErr) {
							oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
							return true;
						}
						m_activeEditor = m_editors.back().value->get();
						return false;
					}));
		} else {
			config.projects.emplace(0, StudioConfig::ProjectConfig{
				.projectPath = ox::String{m_project->projectPath()},
				.activeTabItemName = {},
				.openFiles = {},
			});
		}
		config.projects.resize(ox::min<size_t>(10, config.projects.size()));
		m_recentProjects.clear();
		m_recentProjects.reserve(config.projects.size());
		for (auto const &p : config.projects) {
			m_recentProjects.emplace_back(p.projectPath);
		}
	});
	return m_projectExplorer.refreshProjectTreeModel();
}

ox::Error StudioUI::openFile(ox::StringViewCR path) noexcept {
	return openFileActiveTab(path, true);
}

ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActiveTab) noexcept {
	if (!m_project) {
		return ox::Error(1, "no project open to open a file from");
	}
	if (!m_project->romFs().exists(path)) {
		return ox::Error{1, "file does note exist"};
	}
	if (m_openFiles.contains(path)) {
		for (auto &e : m_editors) {
			if (makeActiveTab && e->itemPath() == path) {
				m_activeEditor = e.get();
				m_activeEditorUpdatePending = e.get();
				break;
			}
		}
		return {};
	}
	OX_REQUIRE(ext, fileExt(path));
	// create Editor
	ox::UPtr<BaseEditor> editor;
	auto const err = m_editorMakers.contains(ext) ?
		m_editorMakers[ext](path).to<ox::UPtr<BaseEditor>>().moveTo(editor) :
		ox::make_unique_catch<ClawEditor>(m_sctx, path).moveTo(editor);
	if (err) {
		if constexpr(!ox::defines::Debug) {
			oxErrf("Could not open Editor: {}\n", toStr(err));
		} else {
			oxErrf("Could not open Editor: {} ({}:{})\n", err.errCode, err.src.file_name(), err.src.line());
		}
		return err;
	}
	editor->closed.connect(this, &StudioUI::closeFile);
	auto const &e = m_editors.emplace_back(std::move(editor));
	m_openFiles.emplace_back(path);
	if (makeActiveTab) {
		m_activeEditor = m_editors.back().value->get();
		m_activeEditorUpdatePending = e.get();
	}
	// save to config
	studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) {
		if (auto const pc = config.project()) {
			if (!pc->openFiles.contains(path)) {
				pc->openFiles.emplace_back(path);
			}
		}
	});
	return {};
}

ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept {
	return m_copyFilePopup.open(path);
}

ox::Error StudioUI::handleCloseAppResponse(ig::PopupResponse const response) noexcept {
	if (response == ig::PopupResponse::OK && m_activeEditor) {
		turbine::requestShutdown(m_tctx, true);
	}
	return {};
}

ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept {
	if (response == ig::PopupResponse::OK && m_activeEditor) {
		return closeCurrentFile();
	}
	return {};
}

ox::Error StudioUI::handleRemoveRecentProjectResponse(ig::PopupResponse const response) noexcept {
	if (response == ig::PopupResponse::OK) {
		auto const p = std::move(m_recentProjects[m_removeRecentProject.idx]);
		studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&p](StudioConfig &config) {
			std::ignore = config.projects.erase(
				std::remove_if(
					config.projects.begin(), config.projects.end(),
					[&p](StudioConfig::ProjectConfig const &pc) {
						return pc.projectPath == p;
					}));
		});
		return m_recentProjects.erase(m_removeRecentProject.idx).error;
	}
	return {};
}

ox::Error StudioUI::closeCurrentFile() noexcept {
	for (auto &e : m_editors) {
		if (m_activeEditor == e.get()) {
			oxLogError(closeFile(e->itemPath()));
			m_closeActiveTab = true;
			break;
		}
	}
	return {};
}

ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept {
	if (!m_openFiles.contains(path)) {
		return {};
	}
	std::ignore = m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path));
	// save to config
	studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
		if (auto const pc = config.project()) {
			std::ignore = pc->openFiles.erase(std::remove(pc->openFiles.begin(), pc->openFiles.end(), path));
		}
	});
	return {};
}

ox::Error StudioUI::queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept {
	m_queuedDirMoves.emplace_back(std::move(src), std::move(dst));
	return {};
}

ox::Error StudioUI::queueFileMove(ox::StringParam src, ox::StringParam dst) noexcept {
	m_queuedMoves.emplace_back(std::move(src), std::move(dst));
	return {};
}

void StudioUI::procFileMoves() noexcept {
	for (auto const &m : m_queuedMoves) {
		if (!m_project->exists(m.b)) {
			oxLogError(m_project->moveItem(m.a, m.b));
		}
	}
	m_queuedMoves.clear();
	for (auto const &m : m_queuedDirMoves) {
		if (!m_project->exists(m.b)) {
			oxLogError(m_project->moveDir(m.a, m.b));
		}
	}
	m_queuedDirMoves.clear();
}

}
