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

#include <imgui.h>

#include <nostalgia/core/core.hpp>

#include "lib/configio.hpp"
#include "builtinmodules.hpp"
#include "filedialogmanager.hpp"
#include "studioapp.hpp"

namespace nostalgia {

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

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

StudioUI::StudioUI(core::Context *ctx) noexcept {
	m_ctx = ctx;
	m_projectExplorer = ox::make_unique<ProjectExplorer>(m_ctx);
	m_projectExplorer->fileChosen.connect(this, &StudioUI::openFile);
	loadModules();
	// open project and files
	const auto [config, err] = studio::readConfig<StudioConfig>(ctx);
	m_showProjectExplorer = config.showProjectExplorer;
	if (!err) {
		oxIgnoreError(openProject(config.projectPath));
		for (const auto &f : config.openFiles) {
			auto openFileErr = openFileActiveTab(f, config.activeTabItemName == f);
			if (openFileErr) {
				oxErrorf("Could not open editor: {}\n", 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(core::Key key, bool down) noexcept {
	const auto ctrlDown = core::buttonDown(m_ctx, core::Key::Mod_Ctrl);
	for (auto p : m_popups) {
		if (p->isOpen()) {
			if (key == core::Key::Escape) {
				p->close();
			}
			return;
		}
	}
	if (down && ctrlDown) {
		switch (key) {
			case core::Key::Num_1:
				toggleProjectExplorer();
				break;
			case core::Key::Alpha_C:
				m_activeEditor->copy();
				break;
			case core::Key::Alpha_N:
				m_newMenu.open();
				break;
			case core::Key::Alpha_O:
				m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject));
				break;
			case core::Key::Alpha_Q:
				core::shutdown(m_ctx);
				break;
			case core::Key::Alpha_S:
				save();
				break;
			case core::Key::Alpha_V:
				m_activeEditor->paste();
				break;
			case core::Key::Alpha_X:
				m_activeEditor->cut();
				break;
			case core::Key::Alpha_Y:
				redo();
				break;
			case core::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 {
	drawMenu();
	drawTabBar();
	if (m_showProjectExplorer) {
		m_projectExplorer->draw(m_ctx);
	}
	for (auto &w : m_widgets) {
		w->draw(m_ctx);
	}
	for (auto p : m_popups) {
		p->draw(m_ctx);
	}
}

void StudioUI::drawMenu() noexcept {
	if (ImGui::BeginMainMenuBar()) {
		if (ImGui::BeginMenu("File")) {
			if (ImGui::MenuItem("New...", "Ctrl+N")) {
				m_newMenu.open();
			}
			if (ImGui::MenuItem("Open Project...", "Ctrl+O")) {
				m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject));
			}
			if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) {
				m_activeEditor->save();
			}
			if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
				core::shutdown(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())) {
				 m_activeEditor->undoStack()->undo();
			}
			if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) {
				 m_activeEditor->undoStack()->redo();
			}
			ImGui::Separator();
			if (ImGui::MenuItem("Copy", "Ctrl+C")) {
				m_activeEditor->copy();
			}
			if (ImGui::MenuItem("Cut", "Ctrl+X")) {
				m_activeEditor->cut();
			}
			if (ImGui::MenuItem("Paste", "Ctrl+V")) {
				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 {
	const auto viewport = ImGui::GetMainViewport();
	const auto mod = m_showProjectExplorer ? 316.f : 0.f;
	ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x + mod, viewport->Pos.y + 20));
	ImGui::SetNextWindowSize(ImVec2(viewport->Size.x - mod, viewport->Size.y - 20));
	constexpr auto windowFlags = ImGuiWindowFlags_NoTitleBar
	                           | ImGuiWindowFlags_NoResize
	                           | ImGuiWindowFlags_NoMove
	                           | ImGuiWindowFlags_NoScrollbar
	                           | ImGuiWindowFlags_NoSavedSettings;
	ImGui::Begin("TabWindow##MainWindow", nullptr, windowFlags);
	constexpr auto tabBarFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_TabListPopupButton;
	if (ImGui::BeginTabBar("TabBar##TabWindow##MainWindow", tabBarFlags)) {
		drawTabs();
		ImGui::EndTabBar();
	}
	ImGui::End();
}

void StudioUI::drawTabs() noexcept {
	for (auto it = m_editors.begin(); it != m_editors.end();) {
		auto const &e = *it;
		auto open = true;
		const auto unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
		const auto selected = m_activeEditorUpdatePending == e.get() ?  ImGuiTabItemFlags_SetSelected : 0;
		const auto flags = unsavedChanges | selected;
		if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
			if (m_activeEditor != e.get()) {
				m_activeEditor = e.get();
				studio::editConfig<StudioConfig>(m_ctx, [&](StudioConfig *config) {
					config->activeTabItemName = m_activeEditor->itemName();
				});
			}
			if (m_activeEditorUpdatePending == e.get()) {
				m_activeEditorUpdatePending = nullptr;
			}
			e->draw(m_ctx);
			ImGui::EndTabItem();
		}
		if (!open) {
			e->close();
			try {
				oxThrowError(m_editors.erase(it).moveTo(&it));
			} catch (const ox::Exception &ex) {
				oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.file, ex.line);
			} catch (const std::exception &ex) {
				oxErrf("Editor tab deletion failed: {}\n", ex.what());
			}
		} else {
			++it;
		}
	}
}

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

void StudioUI::loadModule(studio::Module *module) noexcept {
	for (auto &editorMaker : module->editors(m_ctx)) {
		loadEditorMaker(editorMaker);
	}
	for (auto &im : module->itemMakers(m_ctx)) {
		m_newMenu.addItemMaker(std::move(im));
	}
}

void StudioUI::loadModules() noexcept {
	for (auto &moduleMaker : BuiltinModules) {
		const auto module = moduleMaker();
		loadModule(module.get());
	}
}

void StudioUI::toggleProjectExplorer() noexcept {
	m_showProjectExplorer = !m_showProjectExplorer;
	studio::editConfig<StudioConfig>(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::openProject(const ox::String &path) noexcept {
	oxRequireM(fs, core::loadRomFs(path.c_str()));
	m_ctx->rom = std::move(fs);
	core::setWindowTitle(m_ctx, ox::sfmt("Nostalgia Studio - {}", path).c_str());
	m_project = ox::make_unique<studio::Project>(m_ctx->rom.get(), path);
	auto sctx = applicationData<studio::StudioContext>(m_ctx);
	sctx->project = m_project.get();
	m_project->fileAdded.connect(m_projectExplorer.get(), &ProjectExplorer::refreshProjectTreeModel);
	m_project->fileDeleted.connect(m_projectExplorer.get(), &ProjectExplorer::refreshProjectTreeModel);
	m_openFiles.clear();
	m_editors.clear();
	studio::editConfig<StudioConfig>(m_ctx, [&](StudioConfig *config) {
		config->projectPath = path;
		config->openFiles.clear();
	});
	return m_projectExplorer->refreshProjectTreeModel();
}

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

ox::Error StudioUI::openFileActiveTab(const ox::String &path, bool makeActiveTab) noexcept {
	if (m_openFiles.contains(path)) {
		for (auto &e : m_editors) {
			if (makeActiveTab && e->itemName() == path) {
				m_activeEditor = e.get();
				m_activeEditorUpdatePending = e.get();
				break;
			}
		}
		return OxError(0);
	}
	oxRequire(ext, studio::fileExt(path));
	// create Editor
	if (!m_editorMakers.contains(ext)) {
		return OxError(1, "There is no editor for this file extension");
	}
	auto [editor, err] = m_editorMakers[ext](path);
	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>(m_ctx, [&](StudioConfig *config) {
		if (!config->openFiles.contains((path))) {
			config->openFiles.emplace_back(path);
		}
	});
	return OxError(0);
}

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

}
