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

#include <filesystem>

#include <imgui.h>

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

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

namespace studio {

static ox::Vector<studio::Module const*> modules;

void registerModule(studio::Module const*mod) noexcept {
	modules.emplace_back(mod);
}


struct StudioConfig {
	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;
};

oxModelBegin(StudioConfig)
	oxModelFieldRename(activeTabItemName, active_tab_item_name)
	oxModelFieldRename(projectPath, project_path)
	oxModelFieldRename(openFiles, open_files)
	oxModelFieldRename(showProjectExplorer, show_project_explorer)
oxModelEnd()

StudioUI::StudioUI(turbine::Context &ctx, ox::StringView projectDataDir) noexcept:
		m_sctx(*this, ctx),
		m_ctx(ctx),
		m_projectDataDir(projectDataDir),
		m_projectExplorer(m_ctx),
		m_newProject(ox::String(projectDataDir)),
		m_aboutPopup(m_ctx) {
	turbine::setApplicationData(m_ctx, &m_sctx);
	m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
	m_newProject.finished.connect(this, &StudioUI::createOpenProject);
	m_newMenu.finished.connect(this, &StudioUI::openFile);
	ImGui::GetIO().IniFilename = nullptr;
	loadModules();
	// open project and files
	auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_ctx));
	m_showProjectExplorer = config.showProjectExplorer;
	if (!err) {
		auto const openProjErr = openProjectPath(config.projectPath);
		if (!openProjErr) {
			for (auto const&f: config.openFiles) {
				auto openFileErr = openFileActiveTab(f, config.activeTabItemName == f);
				if (openFileErr) {
					oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
				}
			}
		}
	} 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.file, err.line);
		}
	}
}

void StudioUI::update() noexcept {
	m_taskRunner.update(m_ctx);
}

void StudioUI::handleKeyEvent(turbine::Key key, bool down) noexcept {
	auto const ctrlDown = turbine::buttonDown(m_ctx, turbine::Key::Mod_Ctrl);
	for (auto p : m_popups) {
		if (p->isOpen()) {
			if (key == turbine::Key::Escape) {
				p->close();
			}
			return;
		}
	}
	if (down && ctrlDown) {
		switch (key) {
			case turbine::Key::Num_1:
				toggleProjectExplorer();
				break;
			case turbine::Key::Alpha_C:
				if (m_activeEditor && m_activeEditor->copyEnabled()) {
					m_activeEditor->copy();
				}
				break;
			case turbine::Key::Alpha_N:
				if (turbine::buttonDown(m_ctx, turbine::Key::Mod_Shift)) {
					m_newProject.open();
				} else {
					m_newMenu.open();
				}
				break;
			case turbine::Key::Alpha_O:
				m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
				break;
			case turbine::Key::Alpha_Q:
				turbine::requestShutdown(m_ctx);
				break;
			case turbine::Key::Alpha_S:
				save();
				break;
			case turbine::Key::Alpha_V:
				if (m_activeEditor && m_activeEditor->pasteEnabled()) {
					m_activeEditor->paste();
				}
				break;
			case turbine::Key::Alpha_X:
				if (m_activeEditor && m_activeEditor->cutEnabled()) {
					m_activeEditor->cut();
				}
				break;
			case turbine::Key::Alpha_Y:
				redo();
				break;
			case turbine::Key::Alpha_Z:
				undo();
				break;
			default:
				if (m_activeEditor) {
					m_activeEditor->keyStateChanged(key, down);
				}
				break;
		}
	} else if (m_activeEditor && !ctrlDown) {
		m_activeEditor->keyStateChanged(key, down);
	}
}

void StudioUI::draw() noexcept {
	glutils::clearScreen();
	drawMenu();
	auto const viewport = ImGui::GetMainViewport();
	constexpr auto menuHeight = 18;
	ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + menuHeight));
	ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, viewport->Size.y - menuHeight));
	constexpr auto windowFlags = ImGuiWindowFlags_NoTitleBar
	                           | ImGuiWindowFlags_NoResize
	                           | ImGuiWindowFlags_NoMove
	                           | ImGuiWindowFlags_NoScrollbar
	                           | ImGuiWindowFlags_NoSavedSettings;
	ImGui::Begin("MainWindow##Studio", nullptr, windowFlags);
	{
		if (m_showProjectExplorer) {
			m_projectExplorer.draw(m_sctx);
			ImGui::SameLine();
		}
		drawTabBar();
		for (auto &w: m_widgets) {
			w->draw(m_sctx);
		}
		for (auto p: m_popups) {
			p->draw(m_sctx);
		}
	}
	ImGui::End();
}

void StudioUI::drawMenu() noexcept {
	if (ImGui::BeginMainMenuBar()) {
		if (ImGui::BeginMenu("File")) {
			if (ImGui::MenuItem("New...", "Ctrl+N")) {
				m_newMenu.open();
			}
			if (ImGui::MenuItem("New Project...", "Ctrl+Shift+N")) {
				m_newProject.open();
			}
			if (ImGui::MenuItem("Open Project...", "Ctrl+O")) {
				m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
			}
			if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) {
				m_activeEditor->save();
			}
			if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
				turbine::requestShutdown(m_ctx);
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("Edit")) {
			auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
			if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) {
				undoStack->undo();
			}
			if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) {
				undoStack->redo();
			}
			ImGui::Separator();
			if (ImGui::MenuItem("Copy", "Ctrl+C", false, m_activeEditor && m_activeEditor->copyEnabled())) {
				m_activeEditor->copy();
			}
			if (ImGui::MenuItem("Cut", "Ctrl+X", false, m_activeEditor && m_activeEditor->cutEnabled())) {
				m_activeEditor->cut();
			}
			if (ImGui::MenuItem("Paste", "Ctrl+V", false, m_activeEditor && m_activeEditor->pasteEnabled()) && m_activeEditor) {
				m_activeEditor->paste();
			}
			ImGui::EndMenu();
		}
		if (ImGui::BeginMenu("View")) {
			if (ImGui::MenuItem("Project Explorer", "Ctrl+1", m_showProjectExplorer)) {
				toggleProjectExplorer();
			}
			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_Reorderable | 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;
		if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
			if (m_activeEditor != e.get()) {
				m_activeEditor = e.get();
				studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
					config->activeTabItemName = m_activeEditor->itemPath();
				});
			}
			if (m_activeEditorUpdatePending == e.get()) {
				m_activeEditorUpdatePending = nullptr;
			}
			if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
				m_activeEditor->onActivated();
				turbine::setConstantRefresh(m_ctx, m_activeEditor->requiresConstantRefresh());
			}
			e->draw(m_sctx);
			m_activeEditorOnLastDraw = e.get();
			ImGui::EndTabItem();
		}
		if (!open) {
			e->close();
			if (m_activeEditor == (*it).get()) {
				m_activeEditor = nullptr;
			}
			try {
				oxThrowError(m_editors.erase(it).moveTo(it));
			} catch (ox::Exception const&ex) {
				oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.file, ex.line);
			} catch (std::exception const&ex) {
				oxErrf("Editor tab deletion failed: {}\n", ex.what());
			}
		} else {
			++it;
		}
	}
}

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

void StudioUI::loadModule(studio::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));
	}
}

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

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

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

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

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

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

ox::Error StudioUI::openProjectPath(ox::CRStringView path) noexcept {
	oxRequireM(fs, keel::loadRomFs(path));
	oxReturnError(keel::setRomFs(keelCtx(m_ctx), std::move(fs)));
	oxReturnError(
			ox::make_unique_catch<studio::Project>(keelCtx(m_ctx), ox::String(path), m_projectDataDir)
			        .moveTo(m_project));
	auto const sctx = applicationData<studio::StudioContext>(m_ctx);
	sctx->project = m_project.get();
	turbine::setWindowTitle(m_ctx, ox::sfmt("{} - {}", keelCtx(m_ctx).appName, m_project->projectPath()));
	m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
	m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
	m_openFiles.clear();
	m_editors.clear();
	studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
		config->projectPath = ox::String(m_project->projectPath());
		config->openFiles.clear();
	});
	return m_projectExplorer.refreshProjectTreeModel();
}

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

ox::Error StudioUI::openFileActiveTab(ox::CRStringView path, bool makeActiveTab) noexcept {
	if (!m_project) {
		return OxError(1, "No project open to open a file from");
	}
	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 {};
	}
	oxRequire(ext, studio::fileExt(path).to<ox::String>([](auto const&v) {return ox::String(v);}));
	// create Editor
	studio::BaseEditor *editor = nullptr;
	if (!m_editorMakers.contains(ext)) {
		auto [obj, err] = m_project->loadObj<ox::ModelObject>(path);
		if (err) {
			return OxError(1, "There is no editor for this file extension");
		}
		editor = ox::make<ClawEditor>(path, std::move(obj));
	} else {
		auto const err = m_editorMakers[ext](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.file, err.line);
			}
			return err;
		}
	}
	editor->closed.connect(this, &StudioUI::closeFile);
	m_editors.emplace_back(editor);
	m_openFiles.emplace_back(path);
	if (makeActiveTab) {
		m_activeEditor = m_editors.back().value->get();
		m_activeEditorUpdatePending = editor;
	}
	// save to config
	studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
		if (!config->openFiles.contains(path)) {
			config->openFiles.emplace_back(path);
		}
	});
	return {};
}

ox::Error StudioUI::closeFile(ox::CRStringView path) noexcept {
	if (!m_openFiles.contains(path)) {
		return {};
	}
	oxIgnoreError(m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path)));
	// save to config
	studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
		oxIgnoreError(config->openFiles.erase(std::remove(config->openFiles.begin(), config->openFiles.end(), path)));
	});
	return {};
}

}
