code:
#include "Log.hpp"

#include <spdlog/common.h>
#include <spdlog/logger.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>

#include <memory>
#include <vector>

namespace App {

Log::Log() {
  std::vector<spdlog::sink_ptr> log_sinks;

  const spdlog::level::level_enum level{spdlog::level::debug};

  log_sinks.emplace_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
  log_sinks.emplace_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>("app.log", true));

  log_sinks[0]->set_pattern("%^[%T] %n(%l): %v%$");
  log_sinks[1]->set_pattern("[%T] [%l] %n(%l): %v");

  m_logger = std::make_shared<spdlog::logger>("APP", begin(log_sinks), end(log_sinks));
  spdlog::register_logger(m_logger);
  spdlog::set_default_logger(m_logger);
  m_logger->set_level(level);
  m_logger->flush_on(level);
}

} // namespace App


code:
#pragma once

#include <spdlog/fmt/ostr.h>
#include <spdlog/spdlog.h>

#include <memory>

namespace App {

class Log {
public:
  Log(const Log&) = delete;
  Log(const Log&&) = delete;
  Log& operator=(const Log&) = delete;
  Log& operator=(const Log&&) = delete;
  ~Log() = default;

  static std::shared_ptr<spdlog::logger>& logger() { return get().m_logger; }

private:
  // The constructor shall not be deleted but used to bootstrap the logger. Ignoring
  // the lint warning is ignoring doing `Log() = delete`.
  // NOLINTNEXTLINE
  Log();

  static Log& get() {
    static Log instance{};
    return instance;
  }

  std::shared_ptr<spdlog::logger> m_logger;
};

} // namespace App

#ifndef APP_DEACTIVATE_LOGGING

#if DEBUG
#define APP_TRACE(...) ::App::Log::logger()->trace(__VA_ARGS__)
#define APP_DEBUG(...) ::App::Log::logger()->debug(__VA_ARGS__)
#else
#define APP_TRACE(...)
#define APP_DEBUG(...)
#endif

#define APP_INFO(...) ::App::Log::logger()->info(__VA_ARGS__)
#define APP_WARN(...) ::App::Log::logger()->warn(__VA_ARGS__)
#define APP_ERROR(...) ::App::Log::logger()->error(__VA_ARGS__)
#define APP_FATAL(...) ::App::Log::logger()->critical(__VA_ARGS__)

#else

#define APP_TRACE(...)
#define APP_DEBUG(...)
#define APP_INFO(...)
#define APP_WARN(...)
#define APP_ERROR(...)
#define APP_FATAL(...)

#endif

repo_name: Antidote/openrpgeditor

license: mit


code:
#include "Core/MainWindow.hpp"

#include "Settings.hpp"

#include "Core/Application.hpp"
#include "Core/ImGuiExt/ImGuiNotify.hpp"
#include "Core/ImGuiExt/ImGuiUtils.hpp"
#include "Core/Log.hpp"
#include "Core/SettingsDialog/GeneralSettingsTab.hpp"
#include "Core/Utils.hpp"
#include "Database/Serializable/FileQueue.hpp"
#include "Database/Versions.hpp"
#include "IconsFontAwesome6.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_stacklayout.h"

#include "EditorPlugin/EditorPluginManager.hpp"
#include "LCF_Importer/LCF_Importer.hpp"
#include "Script/ScriptEngine.hpp"
#include "SettingsDialog/RPGMakerLocationAndVersionTab.hpp"
#include "SettingsDialog/UISettingsTab.hpp"

#include "Core/Window.hpp"

#include <array>
#include <clip.h>
#include <fstream>
#include <string_view>

using namespace std::literals::string_view_literals;

MainWindow* MainWindow::m_instance = nullptr;
MainWindow::MainWindow()
: m_mapListView(this)
, m_mapEditor(this)
, m_eventListView(this)
, m_tilesetPicker(this)
, m_nwjsVersionManager("https://dl.nwjs.io")
, m_eventSearcher(this)
, m_libLCF(this) {
  m_settingsDialog.addTab(new GeneralSettingsTab());
  m_settingsDialog.addTab(new UISettingsTab());
  m_settingsDialog.addTab(new RPGMakerLocationAndVersionTab());
  m_instance = this;
}

MainWindow::ToolbarButton::ToolbarButton(const std::string& id, const ToolbarCategory category, asIScriptFunction* func)
: m_id(id)
, m_category(category)
, m_func(func) {

  if (func && func->GetFuncType() == asFUNC_DELEGATE) {
    m_callbackObject = func->GetDelegateObject();
    m_callbackObjectType = func->GetDelegateObjectType();
    m_func = func->GetDelegateFunction();
    ScriptEngine::instance()->engine()->AddRefScriptObject(m_callbackObject, m_callbackObjectType);
    (void)m_func->AddRef();
    (void)func->Release();
  } else {
    m_func = func;
  }
}

MainWindow::ToolbarButton::~ToolbarButton() {
  if (m_func) {
    if (m_callbackObject && m_callbackObjectType) {
      ScriptEngine::instance()->engine()->ReleaseScriptObject(m_callbackObject, m_callbackObjectType);
    }
    (void)m_func->Release();
  }
}

void MainWindow::ToolbarButton::callOnClicked() const {
  if (!m_func) {
    return;
  }

  if (const auto ctx = ScriptEngine::instance()->prepareContextFromPool(m_func)) {
    if (m_callbackObject != nullptr) {
      ctx->SetObject(m_callbackObject);
    }
    ScriptEngine::executeCall(ctx);
    ScriptEngine::instance()->returnContextToPool(ctx);
  }
}

bool MainWindow::load(std::string_view filePath, std::string_view basePath) {
  if (!std::filesystem::exists(filePath)) {
    if (const auto it = std::ranges::find_if(Settings::instance()->mru, [&filePath](const auto& pair) { return pair.first == filePath; }); it != Settings::instance()->mru.end()) {
      ImGui::InsertNotification({ImGuiToastType::Error, trNOOP("Unable to open project:\n%s"), filePath.data()});
      Settings::instance()->mru.erase(it);
    }
    return false;
  }
  close();

  std::string version;
  std::ifstream file(filePath.data());
  std::getline(file, version);
  Utils::trim(version);

  bool isMZ = false;
  m_isValid = std::ranges::find(KnownRPGMVVersions, version) != KnownRPGMVVersions.end();
  if (!m_isValid) {
    m_isValid = std::ranges::find(KnownRPGMZVersions, version) != KnownRPGMZVersions.end();
    isMZ = true;
  }
  if (!m_isValid) {
    APP_ERROR("Invalid project version {}", version);
    return false;
  }
  APP_INFO("Got project for {}", version);
  FileQueue::instance().reset();
  FileQueue::instance().setBasepath(basePath);
  ResourceManager::instance()->setBasepath(basePath);
  m_databaseEditor.emplace(this);
  m_databaseEditor->onReady.connect<&MainWindow::onDatabaseReady>(this);

  m_database.emplace(basePath, filePath, version, isMZ);
  m_database->actorsLoaded().connect<&MainWindow::onActorsLoaded>(this);
  m_database->classesLoaded().connect<&MainWindow::onClassesLoaded>(this);
  m_database->skillsLoaded().connect<&MainWindow::onSkillsLoaded>(this);
  m_database->itemsLoaded().connect<&MainWindow::onItemsLoaded>(this);
  m_database->weaponsLoaded().connect<&MainWindow::onWeaponsLoaded>(this);
  m_database->armorsLoaded().connect<&MainWindow::onArmorsLoaded>(this);
  m_database->enemiesLoaded().connect<&MainWindow::onEnemiesLoaded>(this);
  m_database->troopsLoaded().connect<&MainWindow::onTroopsLoaded>(this);
  m_database->statesLoaded().connect<&MainWindow::onStatesLoaded>(this);
  m_database->animationsLoaded().connect<&MainWindow::onAnimationsLoaded>(this);
  m_database->tilesetsLoaded().connect<&MainWindow::onTilesetsLoaded>(this);
  m_database->commonEventsLoaded().connect<&MainWindow::onCommonEventsLoaded>(this);
  m_database->systemLoaded().connect<&MainWindow::onSystemLoaded>(this);
  m_database->gameConstantsLoaded().connect<&MainWindow::onGameConstantsLoaded>(this);
  m_database->templatesLoaded().connect<&MainWindow::onTemplatesLoaded>(this);
  m_database->docsLoaded().connect<&MainWindow::onDocsLoaded>(this);
  m_database->localesLoaded().connect<&MainWindow::onLocalesLoaded>(this);
  m_database->databaseModified().connect<&MainWindow::onDatabaseModified>(this);
  m_database->load();

  /* Restore the user's stored session */
  if (!m_database->transient.imguiState.empty()) {
    ImGui::LoadIniSettingsFromMemory(m_database->transient.imguiState.c_str(), m_database->transient.imguiState.length());
  }

  return true;
}

void MainWindow::save() {
  size_t len = 0;
  const auto state = ImGui::SaveIniSettingsToMemory(&len);
  m_database->transient.imguiState = std::string(state, len);
  m_database->system.setVersionId(floor(rand() * 100000000));
  m_database->serializeProject();
}

std::tuple<bool, bool, bool> MainWindow::close(const bool promptSave) {
  if (promptSave && m_database && m_database->isModified()) {
    ImGui::OpenPopup("###save_changes");

    ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Size / 2, ImGuiCond_Appearing, {.5f, .5f});
    int ret = -1;
    if (ImGui::BeginPopupModal(std::format("{}###save_changes", trNOOP("Save changes?")).c_str(), nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
      ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
      ImGui::BeginVertical("##save_changes_inner", ImGui::GetContentRegionAvail());
      {
        ImGui::TextUnformatted(trFormat("\"{0}\" at \"{1}\"\n"
                                        "Has unsaved changes, would you like to save?",
                                        m_database->system.gameTitle().empty() ? trNOOP("Untitled Project") : m_database->system.gameTitle(), m_database->basePath)
                                   .c_str());
        ImGui::BeginHorizontal("###save_changes_buttons_outer");
        {
          ImGui::Checkbox(trNOOP("Generate `js/Constants.js`"), &m_database->gameConstants.generateJS);
          ImGui::SetItemTooltip("%s", trNOOP("Generates a Javascript file with constants for any element defined\n"
                                             "`Database Editor -> Game Constants`\n"
                                             "This could take a few minutes."));
          ImGui::Spring();
          ret = ImGui::ButtonGroup("###save_changes_buttons", {trNOOP("Yes"), trNOOP("No"), trNOOP("Cancel")});
        }
        ImGui::EndHorizontal();
      }
      ImGui::EndVertical();
      ImGui::EndPopup();
    }

    if (ret == 0) { // Yes
      if (m_database && m_database->gameConstants.generateJS) {
        /* Clear any loading files */
        FileQueue::instance().reset();
        m_database->mapInfos.loadAllMaps();
      }
      save();
      return {true, true, true};
    }
    if (ret == 1) { // No
      return {true, true, false};
    }
    if (ret == 2) { // Cancel
      return {true, false, false};
    }
    if (ret == -1) { // No input this frame
      return {false, false, false};
    }
  }

  FileQueue::instance().reset();

  m_undoStack.clear();
  m_redoStack.clear();

  /* Default initialize all of these */
  m_databaseEditor.reset();

  m_mapEditor.setMap(nullptr);
  m_mapListView.reset();
  m_database.reset();

  m_isLoaded = false;

  App::APP->getWindow()->setTitle(kApplicationTitle);

  ImGui::ClearIniSettings();
  ImGui::LoadIniSettingsFromMemory(Settings::instance()->imguiState.c_str(), Settings::instance()->imguiState.length());
  return {true, true, false};
}

void MainWindow::setupDocking() {
  ImGuiID mainWindowGroup = ImGui::GetID("MainWindowGroup");

  // Save off menu bar height for later.
  m_menuBarHeight = ImGui::GetCurrentWindow()->MenuBarHeight;

  if (ImGui::DockBuilderGetNode(mainWindowGroup) == nullptr) {
    ImVec2 workPos = ImGui::GetMainViewport()->WorkPos;
    ImVec2 workSize = ImGui::GetMainViewport()->WorkSize;

    ImGui::DockBuilderRemoveNode(mainWindowGroup);
    ImGui::DockBuilderAddNode(mainWindowGroup);

    ImGui::DockBuilderSetNodeSize(mainWindowGroup, workSize);
    ImGui::DockBuilderSetNodePos(mainWindowGroup, workPos);
    ImGuiID upperDock;
    ImGuiID lowerDock = ImGui::DockBuilderSplitNode(mainWindowGroup, ImGuiDir_Down, 0.10f, nullptr, &upperDock);
    ImGuiID leftDock;
    ImGuiID middleDock = ImGui::DockBuilderSplitNode(upperDock, ImGuiDir_Right, .90f, nullptr, &leftDock);
    ImGuiID leftLowerDock = ImGui::DockBuilderSplitNode(leftDock, ImGuiDir_Down, 0.60f, nullptr, &leftDock);
    ImGuiID rightDock = ImGui::DockBuilderSplitNode(middleDock, ImGuiDir_Right, .20f, nullptr, &middleDock);
    // 6. Add windows to each docking space:
    ImGui::DockBuilderDockWindow("###events", leftDock);
    ImGui::DockBuilderDockWindow("###tilesets", leftDock);
    ImGui::DockBuilderDockWindow("###maps", leftLowerDock);
    ImGui::DockBuilderDockWindow("###mapeditor", middleDock);
    ImGui::DockBuilderDockWindow("###databaseeditor", middleDock);
    ImGui::DockBuilderDockWindow("###mapproperties", rightDock);
    ImGui::DockBuilderDockWindow("###filequeuestatus", lowerDock);
    ImGui::DockBuilderFinish(mainWindowGroup);
  }
  ImGui::DockSpace(mainWindowGroup);
}

void MainWindow::drawToolbar() {
  m_toolbarSize = ImGui::GetFrameHeightWithSpacing();
  m_toolbarButtonSize = ImGui::GetFrameHeightWithSpacing();
  const auto ButtonSize = ImVec2{m_toolbarButtonSize, m_toolbarButtonSize};
  const ImGuiViewport* viewport = ImGui::GetMainViewport();
  ImGui::SetNextWindowPos(viewport->Pos + ImVec2{ImGui::GetStyle().FramePadding.x, viewport->Pos.y + m_menuBarHeight + ImGui::GetStyle().ItemSpacing.y});
  ImGui::SetNextWindowSize(ImVec2{viewport->Size.x - ImGui::GetStyle().FramePadding.x * 2, m_toolbarSize + (ImGui::GetStyle().ItemSpacing.y * 2) + (ImGui::GetStyle().FramePadding.y * 2)});
  ImGui::SetNextWindowViewport(viewport->ID);

  if (ImGui::Begin("##ore_toolbar", nullptr,
                   ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings |
                       ImGuiWindowFlags_NoBringToFrontOnFocus)) {
    if (ImGui::IsWindowAppearing()) {
      ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
    }
    ORE_DISABLE_EXPERIMENTAL_BEGIN();
    if (ImGui::Button(ICON_FA_FILE, ButtonSize)) {
      handleCreateNewProject();
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("New Project"), trNOOP("Creates a new project. [EXPERIMENTAL]"));
    }
    ORE_DISABLE_EXPERIMENTAL_END();
    ImGui::SameLine();
    if (ImGui::Button(ICON_FA_FOLDER_OPEN, ButtonSize)) {
      handleOpenFile();
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Open Project"), trNOOP("Opens an existing project."));
    }
    ImGui::SameLine();
    ORE_DISABLE_EXPERIMENTAL_BEGIN();
    if (ImGui::Button(ICON_FA_FLOPPY_DISK, ButtonSize)) {
      save();
    }
    ImGui::SameLine();
    if (ImGui::IsItemHovered()) {
      ImGui::ActionTooltip(trNOOP("Save Project"), trNOOP("Saves the project. [EXPERIMENTAL]"));
    }
    ORE_DISABLE_EXPERIMENTAL_END();

    ORE_CHECK_EXPERIMENTAL_BEGIN()
    for (const auto& button : m_toolbarButtons[ToolbarCategory::File]) {
      ImGui::SameLine();
      if (ImGui::Button(button.id().c_str())) {
        button.callOnClicked();
      }
    }
    ORE_CHECK_EXPERIMENTAL_END()

    ImGui::SameLine();
    ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
    ImGui::SameLine();
    if (ImGui::Button(ICON_FA_SCISSORS, ButtonSize)) {}
    ImGui::SameLine();
    if (ImGui::Button(ICON_FA_COPY, ButtonSize)) {}
    ImGui::SameLine();
    if (ImGui::Button(ICON_FA_PASTE, ButtonSize)) {}
    ImGui::SameLine();
    ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
    ImGui::SameLine();
    ImGui::BeginDisabled(!m_undoStack.hasCommands());
    {
      if (ImGui::Button(ICON_FA_ARROW_ROTATE_LEFT, ButtonSize)) {
        handleUndo();
      }
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(!m_redoStack.hasCommands());
    {
      if (ImGui::Button(ICON_FA_ARROW_ROTATE_RIGHT, ButtonSize)) {
        handleRedo();
      }
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
    ImGui::SameLine();
    ImGui::BeginDisabled(m_editMode == EditMode::Map);
    {
      if (ImGui::Button(ICON_FA_MAP, ButtonSize)) {
        enterMapEditMode();
      }
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Map"), trNOOP("Switch to Map Edit mode."));
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(m_editMode == EditMode::Event);
    {
      if (ImGui::Button(ICON_FA_CHESS_PAWN, ButtonSize)) {
        enterEventEditMode();
      }
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Event"), trNOOP("Switch to Event Edit mode."));
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    if (ImGui::Button(m_mapEditor.prisonMode() ? ICON_FA_EYE : ICON_FA_EYE_SLASH, ButtonSize)) {
      m_mapEditor.setPrisonMode(!m_mapEditor.prisonMode());
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Game Preview"), trNOOP("Toggles Game Preview mode."));
    }
    ImGui::SameLine();
    ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
    ImGui::SameLine();
    ImGui::BeginDisabled(editMode() != EditMode::Map || m_drawTool == DrawTool::Pencil);
    {
      if (ImGui::Button(ICON_FA_PENCIL, ButtonSize)) {
        setDrawTool(DrawTool::Pencil);
      }
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(editMode() != EditMode::Map || m_drawTool == DrawTool::Rectangle);
    {
      if (ImGui::Button(ICON_FA_SQUARE, ButtonSize)) {
        setDrawTool(DrawTool::Rectangle);
      }
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(editMode() != EditMode::Map || m_drawTool == DrawTool::Flood_Fill);
    {
      if (ImGui::Button(ICON_FA_BUCKET, ButtonSize)) {
        setDrawTool(DrawTool::Flood_Fill);
      }
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(editMode() != EditMode::Map || m_drawTool == DrawTool::Shadow_Pen);
    {
      if (ImGui::Button(ICON_FA_PEN, ButtonSize)) {
        setDrawTool(DrawTool::Shadow_Pen);
      }
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Shadow Pen"), trNOOP("Adds or removes shadows of walls"));
    }
    ImGui::EndDisabled();
    ImGui::SameLine();
    ImGui::BeginDisabled(editMode() != EditMode::Map || m_drawTool == DrawTool::Eraser);
    {
      if (ImGui::Button(ICON_FA_ERASER, ButtonSize)) {
        setDrawTool(DrawTool::Eraser);
      }
    }
    if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
      ImGui::ActionTooltip(trNOOP("Eraser"), trNOOP("Erases tiles at a given point"));
    }
    ImGui::EndDisabled();
  }
  ImGui::End();
}

void MainWindow::drawQueueStatus(const bool shuttingDown) {
  ImGui::BeginVertical("##filequeue_status_layout");
  {
    ImGui::BeginHorizontal("##filequeue_status_text");
    {
      if (shuttingDown) {
        ImGui::Spring(0.5f);
      }
      std::string message;
      if (FileQueue::instance().operationType() == ISerializable::Operation::Read && !shuttingDown) {
        message = trFormat("Loading {0} ({1} of {2})....", FileQueue::instance().currentTaskName(), FileQueue::instance().currentTaskNumber(), FileQueue::instance().totalTasks());
      } else if (FileQueue::instance().operationType() == ISerializable::Operation::Read) {
        message = trFormat("Proccessing {0} ({1} of {2}) for constants....", FileQueue::instance().currentTaskName(), FileQueue::instance().currentTaskNumber(), FileQueue::instance().totalTasks());
      } else if (FileQueue::instance().operationType() == ISerializable::Operation::Write) {
        message = trFormat("Saving {0} ({1} of {2})....", FileQueue::instance().currentTaskName(), FileQueue::instance().currentTaskNumber(), FileQueue::instance().totalTasks());
      }
      ImGui::TextUnformatted(message.c_str());
      if (shuttingDown) {
        ImGui::Spring(0.5f);
      }
    }
    ImGui::EndHorizontal();
    ImGui::BeginHorizontal("##filequeue_status_progress");
    {
      if (shuttingDown) {
        ImGui::Spring(0.5f);
      }
      ImGui::ProgressBar(FileQueue::instance().progress() / 100.f, {-FLT_MIN, 0}, "");
      if (shuttingDown) {
        ImGui::Spring(0.5f);
      }
    }
    ImGui::EndHorizontal();
    if (!shuttingDown) {
      if (ImGui::Button(trNOOP("Cancel"))) {
        FileQueue::instance().reset();
        // If we haven't finished loading the database, close the project
        if (FileQueue::instance().operationType() == ISerializable::Operation::Read && (!m_databaseEditor || !m_databaseEditor->isReady())) {
          close();
        }
      }
    }
  }
  ImGui::EndVertical();
}
void MainWindow::drawShutdownSplash(const bool shuttingDown) {
  if (!shuttingDown) {
    return;
  }

  ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_PopupBg));
  ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Size / 2, ImGuiCond_Always, ImVec2(0.5, 0.5));
  ImGui::SetNextWindowSize(ImGui::GetDPIScaledSize(512, 512), ImGuiCond_Appearing);
  ImGui::SetNextWindowSizeConstraints(ImGui::GetDPIScaledSize(512, 512), {FLT_MAX, FLT_MAX});
  ImGui::Begin(trNOOP("##shutdownsplash"), nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove);
  {
    ImGui::BeginVertical("##shutdownsplash_layout", ImGui::GetContentRegionAvail());
    {
      ImGui::Spring(.5);
      ImGui::BeginHorizontal("##shutdownsplash_layout_text");
      {
        ImGui::Spring(.5);
        ImGui::TextUnformatted(trNOOP("Saving and shutting down please wait...."));
        ImGui::Spring(.5);
      }
      ImGui::EndHorizontal();
      ImGui::BeginHorizontal("##shutdownsplash_layout");
      {
        ImGui::Spring(0.5f);
        drawQueueStatus(shuttingDown);
      }
      ImGui::EndHorizontal();
      ImGui::BeginHorizontal("##shutdownsplash_layout_inner");
      {
        ImGui::Spring(.5);
        if (ImGui::Button(trNOOP("Cancel"))) {
          FileQueue::instance().reset();
          App::APP->cancelShutdown();
        }
        ImGui::Spring(.5);
      }
      ImGui::EndHorizontal();
      ImGui::Spring(.5);
    }
    ImGui::EndVertical();
  }
  ImGui::End();
  ImGui::PopStyleColor();
}
void MainWindow::draw(const bool shuttingDown, const bool closeRequested) {

  ImGuiViewport* viewport = ImGui::GetMainViewport();
  ImGui::SetNextWindowPos(viewport->Pos + ImVec2(0, m_menuBarHeight + m_toolbarSize));
  ImGui::SetNextWindowSize(viewport->Size - ImVec2(0, m_menuBarHeight + m_toolbarSize));
  ImGui::SetNextWindowViewport(viewport->ID);

  if (ImGui::Begin("MasterDockSpace", nullptr,
                   ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
                       ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground)) {
    setupDocking();
    ImGui::BeginDisabled(shuttingDown);
    drawToolbar();
    drawMenu();
    if (!closeRequested) {
      m_settingsDialog.draw();
      m_eventSearcher.draw();
      m_libLCF.draw();
    }

    m_mapEditor.draw(closeRequested);

    if (m_databaseEditor && !closeRequested) {
      m_databaseEditor->draw();
    }

    if (m_editMode == EditMode::Map) {
      m_tilesetPicker.draw();
    } else {
      m_eventListView.draw();
    }

    m_mapListView.draw();

    if (const auto [closed, confirmed] = m_mapProperties.draw(); closed) {
      // TODO: handle revert?
    }
    ORE_CHECK_DEBUG_BEGIN()
    drawTileDebugger();

    if (m_showDemoWindow) {
      ImGui::ShowDemoWindow(&m_showDemoWindow);
    }
    ORE_CHECK_DEBUG_END()

    if (m_showAboutWindow) {
      ImGui::ShowAboutWindow(&m_showAboutWindow);
    }

    if (!closeRequested) {
      m_nwjsVersionManager.draw();
      EditorPluginManager::instance()->draw();
    }

    if (FileQueue::instance().hasTasks() && !shuttingDown) {
      ImGui::Begin(std::format("{}###filequeuestatus", FileQueue::instance().operationType() == ISerializable::Operation::Read ? trNOOP("Loading Project....") : trNOOP("Saving Project....")).c_str(),
                   nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize);
      drawQueueStatus(false);
      ImGui::End();
    } else if (FileQueue::instance().progress() >= 100.f) {
      FileQueue::instance().reset();
    }

    ImGui::RenderNotifications();

    if (!closeRequested) {
      drawCreateNewProjectPopup();
      EditorPluginManager::instance()->callDraws();
    }
    ImGui::EndDisabled();

    drawShutdownSplash(shuttingDown);
  }
  ImGui::End();
}

void MainWindow::drawCreateNewProjectPopup() {
  static constexpr std::array<std::string_view, 23> directoryList{{
      "data"sv,           "fonts"sv,       "audio/bgm"sv, "audio/bgs"sv,      "audio/me"sv,     "audio/se"sv,      "img/animations"sv, "img/battlebacks1"sv, "img/battlebacks2"sv,
      "img/characters"sv, "img/enemies"sv, "img/faces"sv, "img/parallaxes"sv, "img/pictures"sv, "img/sv_actors"sv, "img/sv_enemies"sv, "img/system"sv,       "img/tilesets"sv,
      "img/titles1"sv,    "img/titles2"sv, "js/libs"sv,   "js/plugins"sv,     "movies"sv,
  }};

  const auto [closed, confirmed] = m_createNewProject.draw();
  if (closed) {
    if (confirmed) {
      // TODO: Add RPGMaker MZ example copy
      const auto isMZ = m_createNewProject.projectType() == CreateNewProjectDialog::ProjectType::RPGMZ;
      const auto projectName = m_createNewProject.projectName();
      const auto gameTitle = m_createNewProject.gameTitle();
      const auto copyExampleProject = m_createNewProject.copyExample();

      const auto projectPath = Settings::instance()->projectBaseDirectory / std::filesystem::path(projectName);

      if (create_directories(projectPath)) {
        for (const auto& directory : directoryList) {
          create_directories(projectPath / directory);
        }

        auto projectFilePath = projectPath / (isMZ ? std::filesystem::path("Game.rmmzproject") : std::filesystem::path("Game.rpgproject"));
        std::ofstream projectFile(projectFilePath);
        std::string version;
        if (isMZ) {
          version = Settings::instance()->rpgMakerMZVersion != -1 ? KnownRPGMZVersions[Settings::instance()->rpgMakerMZVersion] : KnownRPGMZVersions[KnownRPGMZVersions.size() - 1];
        } else {
          version = Settings::instance()->rpgMakerMVVersion != -1 ? KnownRPGMVVersions[Settings::instance()->rpgMakerMVVersion] : KnownRPGMVVersions[KnownRPGMVVersions.size() - 1];
        }
        projectFile << version << std::endl;
        FileQueue::instance().reset();
        FileQueue::instance().setBasepath(projectPath.generic_string());
        if (copyExampleProject) {
          // TODO: Locale
          std::filesystem::path examplePath;
          if (!isMZ) {
            examplePath = std::filesystem::path(Settings::instance()->rpgMakerMVLocation) / "NewData";
          } else {
            // This newdata needs to be lowercase when copying an RPG Maker MZ example project
            examplePath = std::filesystem::path(Settings::instance()->rpgMakerMZLocation) / "newdata";
          }
          // TODO: MZ corescripts selection

          examplePath.make_preferred();
          if (exists(examplePath)) {
            for (const auto& entry : std::filesystem::recursive_directory_iterator(examplePath)) {
              const auto tmpPath = entry.path().generic_string().substr(examplePath.generic_string().size() + 1);
              const auto destPath = projectPath / std::filesystem::path(tmpPath);
              if (entry.is_directory() && !exists(destPath)) {
                create_directories(destPath);
              } else if (entry.is_regular_file()) {
                std::cout << destPath.generic_string() << std::endl;
                std::filesystem::copy_file(entry.path(), destPath);
              }
            }
          }
        }
        if (!copyExampleProject) {
          m_database.emplace(projectPath.generic_string(), projectFilePath.generic_string(), version, isMZ);
          m_database->system.setGameTitle(gameTitle.data());
          m_database->system.setLocale("en_US");
          m_database->system.setVersionId(floor(rand() * 100000000));
          m_database->serializeProject();
        }
        while (FileQueue::instance().hasTasks()) {
          FileQueue::instance().proc();
        }
        load(projectFilePath.generic_string(), projectPath.generic_string());
      }
    }
  }
}

void MainWindow::drawTileInfo(MapRenderer::MapLayer& mapLayer, int z) {
  static constexpr std::array LayerNames{"A1"sv, "A2"sv, "A3"sv, "A4"sv, "A5"sv, "B"sv, "C"sv, "D"sv, "E"sv};
  for (const auto& layer : mapLayer.tileLayers) {
    for (const auto& tile : layer.rects) {
      if ((tile.x >= m_mapEditor.tileAbsoluteX() && tile.x < m_mapEditor.tileAbsoluteX() + m_mapEditor.tileSize()) &&
          (tile.y >= m_mapEditor.tileAbsoluteY() && tile.y < m_mapEditor.tileAbsoluteY() + m_mapEditor.tileSize())) {
        float u0 = tile.u;
        float v0 = tile.v;
        float u1 = u0 + tile.tileWidth;
        float v1 = v0 + tile.tileHeight;

        u0 /= layer.tex.width();
        v0 /= layer.tex.height();
        u1 /= layer.tex.width();
        v1 /= layer.tex.height();
        ImGui::Image(layer.tex, {static_cast<float>(m_mapEditor.tileSize()), static_cast<float>(m_mapEditor.tileSize())}, ImVec2{u0, v0}, ImVec2{u1, v1});
        std::string info = trFormat("Sheet: {} ({}), ID: {}, x: {}, y: {}, z: {},  width: {}, height: {}, u: {}, v: {}", m_mapEditor.mapRenderer().tileset()->tilesetName(tile.tileSheet),
                                    LayerNames[tile.tileSheet], tile.tileId, tile.x, tile.y, z, tile.tileWidth, tile.tileHeight, tile.u, tile.v);
        if (TileHelper::isAutoTile(tile.tileId)) {
          info += trFormat(
              "\n"
              "AutoTile Shape: {}, Kind: {}\n"
              "WaterTile: {}, WaterfallTile: {}, GroundTile {}, RoofTile: {}, ShadowingTile: {},\n"
              "WallSide: {}, WallTop: {}, FloorTypeAutoTile: {}, WallTypeAutoTile: {}\n",
              TileHelper::getAutoTileShape(tile.tileId), TileHelper::getAutoTileKind(tile.tileId), TileHelper::isWaterTile(tile.tileId), TileHelper::isWaterfallTile(tile.tileId),
              TileHelper::isGroundTile(tile.tileId), TileHelper::isRoofTile(tile.tileId), TileHelper::isShadowingTile(tile.tileId), TileHelper::isWallTopTile(tile.tileId),
              TileHelper::isWallSideTile(tile.tileId), TileHelper::isFloorTypeAutotile(tile.tileId), TileHelper::isWallTypeAutotile(tile.tileId));
        }
        ImGui::Text("%s", info.c_str());
      }
    }
  }
}

void MainWindow::drawTileDebugger() {
  if (!m_showTileDebug || !m_mapEditor.map()) {
    return;
  }

  if (ImGui::Begin(trNOOP("Tile Debug"), &m_showTileDebug)) {
    if (ImGui::BeginTabBar("##tiledebugger")) {
      if (ImGui::BeginTabItem(trNOOP("Lower"))) {
        for (int z = 0; z < 6; ++z) {
          drawTileInfo(m_mapEditor.mapRenderer().m_lowerLayers[z], z);
        }
        ImGui::EndTabItem();
      }
      if (ImGui::BeginTabItem(trNOOP("Upper"))) {
        for (int z = 0; z < 6; ++z) {
          drawTileInfo(m_mapEditor.mapRenderer().m_upperLayers[z], z);
        }
        ImGui::EndTabItem();
      }
      ImGui::EndTabBar();
    }
  }
  ImGui::End();
}

void MainWindow::handleOpenFile() {
  static constexpr SDL_DialogFileFilter filters{
      "RPG Maker Projects",
      "rpgproject;rmmzproject",
  };
  const std::string directory = Settings::instance()->lastDirectory.empty() ? std::filesystem::current_path().generic_string() : Settings::instance()->lastDirectory;
  SDL_ShowOpenFileDialog(
      [](void* userdata, const char* const* filelist, int filter) {
        if (!filelist || !*filelist || filter >= 1) {
          return;
        }
        auto* window = static_cast<MainWindow*>(userdata);
        const std::filesystem::path path{filelist[0]};
        Settings::instance()->lastDirectory = absolute(path).remove_filename().generic_string();
        window->load(absolute(path).generic_string(), Settings::instance()->lastDirectory);
      },
      this, App::APP->getWindow()->getNativeWindow(), &filters, 1, directory.c_str(), false);
}

void MainWindow::handleCreateNewProject() { m_createNewProject.setOpen(true); }

void MainWindow::handleUndo() {
  if (!m_undoStack.hasCommands()) {
    return;
  }

  auto cmd = m_undoStack.pop();
  if (cmd) {
    cmd->undo();
    cmd->setIsRedo(true);
    m_redoStack.push(cmd);
  }
}

void MainWindow::handleRedo() {
  if (!m_redoStack.hasCommands()) {
    return;
  }

  auto cmd = m_redoStack.pop();
  if (cmd) {
    cmd->undo();
    cmd->setIsRedo(false);
    m_undoStack.push(cmd);
  }
}
void MainWindow::drawMenu() {
  std::string loadFilepath;
  if (ImGui::BeginMainMenuBar()) {
    if (ImGui::BeginMenu(trNOOP("File"))) {
      if (ImGui::MenuItemEx(trNOOP("New Project..."), ICON_FA_FILE, "Ctrl+N")) {

        handleCreateNewProject();
      }
      if (ImGui::MenuItemEx(trNOOP("Open Project..."), ICON_FA_FOLDER, "Ctrl+O")) {
        handleOpenFile();
      }
      if (ImGui::MenuItemEx(trNOOP("Close Project..."), ICON_FA_FOLDER_CLOSED)) {
        App::APP->closeProject();
      }
      if (ImGui::MenuItemEx(trNOOP("Save Project..."), ICON_FA_FLOPPY_DISK, "Ctrl+S")) {
        save();
      }
      ImGui::Separator();
      if (ImGui::BeginMenuEx(trNOOP("Recent Projects"), "", !Settings::instance()->mru.empty())) {
        if (ImGui::MenuItemNoCheck(tr("Clear").c_str(), nullptr, ICON_FA_DELETE_LEFT)) {
          Settings::instance()->mru.clear();
        }
        if (!Settings::instance()->mru.empty()) {
          ImGui::Separator();
        }
        for (const auto& s : Settings::instance()->mru) {
          ImGui::PushID(s.first.c_str());
          if (ImGui::MenuItem(s.second.c_str())) {
            loadFilepath = s.first;
          }
          ImGui::PopID();
          if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("%s", s.first.c_str());
          }
        }
        ImGui::EndMenu();
      }
      ImGui::Separator();
      // TL-NOTE: Use ellipses character appropriate for your language
      // Deployment is referring to preparing a game for distribution to end users, exporting all related assets into an easily distributable form
      if (ImGui::MenuItemEx(trNOOP("Deployment..."), ICON_FA_BOXES_PACKING " ")) {
        // TODO: Implement game deployment
      }
      /*
      if (ImGui::MenuItem("Steam Management")) {
        // TODO: Debate on implementing this, it may not be wise, or worthwhile
      }
      */
      ImGui::Separator();
      // We're using std::format here instead of trFORMAT because we don't want to risk someone localizing the icon after the text
      if (ImGui::MenuItemEx(trNOOP("Exit"), ICON_FA_DOOR_CLOSED, "Ctrl+Q")) {
        App::APP->stop();
      }
      ImGui::EndMenu();
    }

    /* TODO: Implement shortcut handling, ImGui doesn't handle that for various reasons
     * so we have to do that ourselves
     */
    if (ImGui::BeginMenu(trNOOP("Edit"))) {
      if (ImGui::MenuItem(trNOOP("Undo"), "Ctrl+Z", false, m_undoStack.hasCommands())) {
        handleUndo();
      }
      if (ImGui::IsItemHovered() && m_undoStack.hasCommands()) {
        ImGui::SetTooltip("%s", m_undoStack.top()->description().c_str());
      }

      if (ImGui::MenuItem(trNOOP("Redo"), "Ctrl+Shift+Z", false, m_redoStack.hasCommands())) {
        handleRedo();
      }
      if (ImGui::IsItemHovered() && m_redoStack.hasCommands()) {
        ImGui::SetTooltip("%s", m_redoStack.top()->description().c_str());
      }
      ImGui::Separator();
      if (ImGui::MenuItem(trNOOP("Cut"), "Ctrl+X")) {
        // TODO: Implement clipboard
      }
      if (ImGui::MenuItem(trNOOP("Copy"), "Ctrl+C")) {
        // TODO: Implement clipboard
      }
      if (ImGui::MenuItem(trNOOP("Paste"), "Ctrl+V")) {
        // TODO: Implement clipboard
      }
      if (ImGui::MenuItem(trNOOP("Delete"), "Del")) {
        // TODO: Implement
      }
      ImGui::Separator();
      if (ImGui::MenuItem(trNOOP("Find"), "Ctrl+F")) {
        // TODO: Implement
      }
      if (ImGui::MenuItem(trNOOP("Find Next"), "F3")) {
        // TODO: Implement
      }
      if (ImGui::MenuItem(trNOOP("Find Previous"), "Shift+F3")) {
        // TODO: Implement
      }
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu(trNOOP("Mode"))) {
      if (ImGui::MenuItem(trNOOP("Map"), "F5", editMode() == EditMode::Map)) {
        enterMapEditMode();
      }
      if (ImGui::MenuItem(trNOOP("Event"), "F6", editMode() == EditMode::Event)) {
        enterEventEditMode();
      }
      ImGui::Separator();
      if (ImGui::MenuItem(trNOOP("Game Preview"), "F7", !m_mapEditor.prisonMode())) {
        m_mapEditor.togglePrisonMode();
      }
      if (ImGui::IsItemHovered()) {
        // TL-NOTE: Refers to Game Preview mode and is in reference to how events are drawn to the screen.
        // Displays as a tooltip.
        ImGui::SetTooltip("%s", trNOOP("Toggles event borders, layer render order, and animations."));
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu(trNOOP("Draw"))) {
      for (auto v : magic_enum::enum_values<DrawTool>()) {
        if (ImGui::MenuItem(DecodeEnumName(magic_enum::enum_name(v)).data(), nullptr, drawTool() == v, editMode() == EditMode::Map)) {
          setDrawTool(v);
        }
      }
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu(trNOOP("Scale"))) {
      if (ImGui::MenuItem(trNOOP("Zoom In"), "Ctrl++", false, m_mapEditor.zoom() > 0.25f)) {
        m_mapEditor.scale(0.25f);
      }
      if (ImGui::MenuItem(trNOOP("Zoom Out"), "Ctrl+-", false, m_mapEditor.zoom() < 4.f)) {
        m_mapEditor.scale(-0.25f);
      }
      if (ImGui::MenuItem(trNOOP("Actual Size"), "Ctrl+0", false, m_mapEditor.zoom() != 1.f)) {
        m_mapEditor.setScale(1.f);
      }
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu(trNOOP("Tools"))) {
      if (ImGui::MenuItem(trNOOP("Settings"), "F8")) {
        m_settingsDialog.setOpen(true);
      }
      if (ImGui::MenuItem(trNOOP("Database"), "F9", false, m_databaseEditor != std::nullopt && m_databaseEditor->isReady())) {
        m_databaseEditor->open();
      }
      if (ImGui::MenuItem(trNOOP("NWJS Version Manager"), "F10")) {
        m_nwjsVersionManager.open();
      }
      if (ImGui::MenuItem(trNOOP("Event Searcher..."), "F11", false, m_databaseEditor != std::nullopt && m_databaseEditor->isReady())) {
        m_eventSearcher.open();
      }
      if (ImGui::MenuItem(trNOOP("Editor Plugins..."), "F12")) {
        EditorPluginManager::instance()->setOpen(true);
      }
      if (ImGui::MenuItem(trNOOP("LibLCF..."), "", false, m_databaseEditor != std::nullopt && m_databaseEditor->isReady())) {
        m_libLCF.open();
      }

      /* Add tools above this */
      ImGui::Separator();
      if (ImGui::MenuItem(trNOOP("Reset Window Layout"))) {
        ImGui::ClearIniSettings();
      }
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu(trNOOP("Game"))) {
      if (ImGui::MenuItem(trNOOP("Play Test"), "Ctrl+R", false, m_databaseEditor != std::nullopt)) {
        // TODO: Implement
      }
      ImGui::Separator();
      if (ImGui::MenuItem(trNOOP("Open Folder"), nullptr, false, m_databaseEditor != std::nullopt)) {
        SDL_OpenURL(std::format("file://{}", m_database->basePath).c_str());
      }
      ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Help")) {
      if (ImGui::MenuItem("Contents", "F1")) {}
      if (ImGui::MenuItem("Tutorials...")) {}
      ImGui::Separator();
      if (ImGui::MenuItem("About", nullptr, &m_showAboutWindow)) {}
      ORE_CHECK_DEBUG_BEGIN()
      if (ImGui::MenuItem("Show Tile Debugger", nullptr, &m_showTileDebug)) {}
      if (ImGui::MenuItem("Show Demo", nullptr, &m_showDemoWindow)) {}
      ORE_CHECK_DEBUG_END()
      ImGui::EndMenu();
    }

    ImGui::EndMainMenuBar();
  }

  if (!loadFilepath.empty()) {
    load(loadFilepath, std::filesystem::absolute(loadFilepath).remove_filename().generic_string());
  }
  handleKeyboardShortcuts();
}

void MainWindow::handleKeyboardShortcuts() {
  /* File Menu */
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_N)) {
    // TODO: implement new project creation
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_O)) {
    handleOpenFile();
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_S)) {
    // TODO: implement saving
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_Q)) {
    App::APP->stop();
  }

  /* Edit Menu */
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && !(ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) &&
      ImGui::IsKeyPressed(ImGuiKey_Z)) {
    handleUndo();
  }
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) &&
      ImGui::IsKeyPressed(ImGuiKey_Z)) {
    handleRedo();
  }
  /* TODO Clipboard */
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_X)) {
    // TODO: Implement clipboard cut
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_C)) {
    // TODO: Implement clipboard copy
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_V)) {
    // TODO: Implement clipboard paste
  }

  if (ImGui::IsKeyReleased(ImGuiKey_Delete)) {
    // TODO: Implement Delete
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && ImGui::IsKeyPressed(ImGuiKey_F)) {
    // TODO: Implement Find
  }
  if (ImGui::IsKeyPressed(ImGuiKey_F3)) {
    // TODO: Implement Find Next
  }

  if ((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) && ImGui::IsKeyPressed(ImGuiKey_F3)) {
    // TODO: Implement Find Previous
  }

  /* Mode */
  if (ImGui::IsKeyPressed(ImGuiKey_F5)) {
    enterMapEditMode();
  }

  if (ImGui::IsKeyPressed(ImGuiKey_F6)) {
    enterEventEditMode();
  }
  if (ImGui::IsKeyPressed(ImGuiKey_F7)) {
    m_mapEditor.togglePrisonMode();
  }

  /* Scale */
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && (ImGui::IsKeyPressed(ImGuiKey_Equal) || ImGui::IsKeyPressed(ImGuiKey_KeypadAdd))) {
    m_mapEditor.scale(0.25f);
  }
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && (ImGui::IsKeyPressed(ImGuiKey_Minus) || ImGui::IsKeyPressed(ImGuiKey_KeypadSubtract))) {
    m_mapEditor.scale(-0.25f);
  }
  if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) && (ImGui::IsKeyPressed(ImGuiKey_Keypad0) || ImGui::IsKeyPressed(ImGuiKey_0))) {
    m_mapEditor.setScale(1.f);
  }

  /* Tools */
  if (ImGui::IsKeyReleased(ImGuiKey_F8)) {
    m_settingsDialog.setOpen(true);
  }
  if (ImGui::IsKeyReleased(ImGuiKey_F9)) {
    m_databaseEditor->open();
  }
  if (ImGui::IsKeyReleased(ImGuiKey_F10)) {
    m_nwjsVersionManager.open();
  }
  if (ImGui::IsKeyReleased(ImGuiKey_F11)) {
    m_eventSearcher.open();
  }
  if (ImGui::IsKeyReleased(ImGuiKey_F12)) {
    EditorPluginManager::instance()->setOpen();
  }

  // TODO: Add missing tools
  /* Game */
  if ((ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) && ImGui::IsKeyPressed(ImGuiKey_R)) {
    // TODO: Implement Play Test
  }

  /* Help */
  if (ImGui::IsKeyPressed(ImGuiKey_F1)) {
    // TODO: Help
  }
}

void MainWindow::setMap(MapInfo& in) {
  if (currentMapInfo()) {
    currentMapInfo()->closeIfNotModified();
  }

  if (in.id() == 0) {
    m_mapListView.setCurrentMapId(0);
    m_mapEditor.setMap(nullptr);
    m_mapProperties.setMapInfo(nullptr);
    return;
  }

  m_mapListView.setCurrentMapId(in.id(), true);
  if (m_mapListView.currentMapInfo()) {
    m_mapEditor.setMap(&in);
    m_database->mapInfos.setCurrentMap(&in);
    m_mapProperties.setMapInfo(&in);
  } else {
    m_mapEditor.setMap(nullptr);
    m_database->mapInfos.setCurrentMap(nullptr);
    m_mapProperties.setMapInfo(nullptr);
  }

  m_database->system.setEditMapId(in.id());

  SDL_SetCursor(SDL_GetDefaultCursor());
}

void MainWindow::onActorsLoaded() { m_databaseEditor->setActors(m_database->actors); }
void MainWindow::onClassesLoaded() { m_databaseEditor->setClasses(m_database->classes); }
void MainWindow::onSkillsLoaded() { m_databaseEditor->setSkills(m_database->skills); }
void MainWindow::onItemsLoaded() { m_databaseEditor->setItems(m_database->items); }
void MainWindow::onWeaponsLoaded() { m_databaseEditor->setWeapons(m_database->weapons); }
void MainWindow::onArmorsLoaded() { m_databaseEditor->setArmors(m_database->armors); }
void MainWindow::onEnemiesLoaded() { m_databaseEditor->setEnemies(m_database->enemies); }
void MainWindow::onTroopsLoaded() { m_databaseEditor->setTroops(m_database->troops); }
void MainWindow::onStatesLoaded() { m_databaseEditor->setStates(m_database->states); }
void MainWindow::onAnimationsLoaded() { m_databaseEditor->setAnimations(m_database->animations); }
void MainWindow::onTilesetsLoaded() { m_databaseEditor->setTilesets(m_database->tilesets); }
void MainWindow::onCommonEventsLoaded() { m_databaseEditor->setCommonEvents(m_database->commonEvents); }
void MainWindow::onSystemLoaded() { m_databaseEditor->setSystem(m_database->system); }
void MainWindow::onGameConstantsLoaded() { m_databaseEditor->setGameConstants(m_database->gameConstants); }
void MainWindow::onTemplatesLoaded() { m_databaseEditor->setTemplates(m_database->templates); }
void MainWindow::onDocsLoaded() { m_databaseEditor->setDocs(m_database->docs); }
void MainWindow::onLocalesLoaded() { m_databaseEditor->setLocales(m_database->locales); }

void MainWindow::onDatabaseReady() {
  MapInfo* info = m_database->mapInfos.map(0);
  Settings::instance()->lastProject = m_database->projectFilePath;
  info->setExpanded(true);
  info->setName(m_database->system.gameTitle().empty() ? std::filesystem::path(m_database->projectFilePath).filename().generic_string() : m_database->system.gameTitle());
  APP_INFO("Loaded project!");
  // Load the previously loaded map
  m_mapEditor.clearLayers();
  m_mapListView.setMapInfos(&m_database->mapInfos);

  if (MapInfo* m = m_database->mapInfos.map(m_database->system.editMapId()); m != nullptr) {
    setMap(*m);
  }

  if (const auto fileIt = std::ranges::find_if(Settings::instance()->mru, [this](const auto& t) { return t.first == m_database->projectFilePath; }); fileIt == Settings::instance()->mru.end()) {
    Settings::instance()->mru.emplace_front(m_database->projectFilePath,
                                            m_database->system.gameTitle().empty() ? std::filesystem::path(m_database->projectFilePath).filename().generic_string() : m_database->system.gameTitle());
  } else {
    Settings::instance()->mru.erase(fileIt);
    Settings::instance()->mru.emplace_front(m_database->projectFilePath,
                                            m_database->system.gameTitle().empty() ? std::filesystem::path(m_database->projectFilePath).filename().generic_string() : m_database->system.gameTitle());
  }
  while (Settings::instance()->mru.size() > Settings::instance()->maxMru) {
    Settings::instance()->mru.pop_back();
  }
  m_isLoaded = true;
  /* Disconnect the signal so we don't get spammed */
  m_databaseEditor->onReady.disconnect<&MainWindow::onDatabaseReady>(this);

  const auto gameTitle =
      Database::instance()->system.gameTitle().empty() ? std::filesystem::path(m_database->projectFilePath).filename().replace_extension().generic_string() : m_database->system.gameTitle();
  App::APP->getWindow()->setTitle(std::format("{} - [{}]", kApplicationTitle, gameTitle));
}

void MainWindow::onDatabaseModified() {
  const auto gameTitle =
      Database::instance()->system.gameTitle().empty() ? std::filesystem::path(m_database->projectFilePath).filename().replace_extension().generic_string() : m_database->system.gameTitle();
  const auto title = std::format("{} - [{}*]", kApplicationTitle, gameTitle);
  if (title != App::APP->getWindow()->getTitle()) {
    App::APP->getWindow()->setTitle(title);
  }
}

void MainWindow::addToolbarButton(const std::string& id, ToolbarCategory category, asIScriptFunction* func) { m_toolbarButtons[category].emplace_back(id, category, func); }

void AddToolbarButton(asIScriptGeneric* generic) {
  const std::string name = *static_cast<std::string*>(generic->GetArgAddress(0));
  const auto category = static_cast<ToolbarCategory>(generic->GetArgDWord(1));
  const auto func = static_cast<asIScriptFunction*>(generic->GetArgAddress(2));
  MainWindow::instance()->addToolbarButton(name, category, func);
}

void AddCallback(asIScriptFunction* func) {
  //
}
void MainWindow::RegisterBindings() {
  auto engine = ScriptEngine::instance()->engine();
  engine->RegisterEnum("ToolbarCategory");

  int r;
  for (const auto& category : magic_enum::enum_values<ToolbarCategory>()) {
    r = engine->RegisterEnumValue("ToolbarCategory", magic_enum::enum_name(category).data(), static_cast<int>(category));
    assert(r >= 0);
  }
  r = engine->RegisterFuncdef("void ToolBarButtonClicked()");
  assert(r >= 0);
  r = engine->RegisterGlobalFunction("void addToolbarButton(const string& in, ToolbarCategory category, ToolBarButtonClicked @const cb)", asFUNCTION(AddToolbarButton), asCALL_GENERIC);
  assert(r >= 0);
}


code:
#pragma once

#include "angelscript.h"
#include "CommonUI/CreateNewProjectDialog.hpp"
#include "Core/DatabaseEditor.hpp"
#include "Core/EventEditor.hpp"
#include "Core/EventListView.hpp"
#include "Core/MapEditor.hpp"
#include "Core/MapListView.hpp"
#include "Core/NWJSVersionManager.hpp"
#include "Core/ResourceManager.hpp"
#include "Core/UndoStack.hpp"
#include "EventSearcher.hpp"
#include "LibLCF.hpp"
#include "SettingsDialog.hpp"

#include "Core/CommonUI/ObjectPicker.hpp"
#include "Core/CommonUI/TextEditor.hpp"
#include "Core/CommonUI/TilesetPicker.hpp"

#include "Database/Database.hpp"
#include "Database/MapInfos.hpp"

#include <string>
#include <string_view>

class CreateNewProjectDialog;
enum class EditMode {
  Map,
  Event,
};

enum class DrawTool {
  Pencil,
  Rectangle,
  Ellipse,
  Flood_Fill,
  Shadow_Pen,
  Eraser,
};

enum class ToolbarCategory {
  File,
  UI,
  MapOperation,
};

class asIScriptFunction;
class MainWindow {
public:
  class ToolbarButton {
  public:
    ToolbarButton(const std::string& id, const ToolbarCategory category, asIScriptFunction* func);
    ~ToolbarButton();
    const std::string& id() const { return m_id; }

    void callOnClicked() const;

  private:
    std::string m_id;
    ToolbarCategory m_category;
    asIScriptFunction* m_func;
    void* m_callbackObject = nullptr;
    asITypeInfo* m_callbackObjectType = nullptr;
  };
  MainWindow();
  ~MainWindow() { m_instance = nullptr; }

  bool load(std::string_view filePath, std::string_view basePath);
  void save();
  std::tuple<bool, bool, bool> close(bool promptSave = false);
  void setupDocking();

  [[nodiscard]] bool isLoaded() const { return m_isLoaded; }
  [[nodiscard]] bool isValid() const { return m_isValid; }
  [[nodiscard]] const std::string& version() const { return m_database->projectVersion; }

  void draw(bool shuttingDown = false, bool closeRequested = false);
  void drawTileInfo(MapRenderer::MapLayer& mapLayer, int z);
  void handleOpenFile();
  void handleCreateNewProject();
  void handleUndo();
  void handleRedo();

  EditMode editMode() const { return m_editMode; }

  void enterMapEditMode() {
    if (m_editMode == EditMode::Map) {
      return;
    }

    // TODO: Fully implement
    m_editMode = EditMode::Map;
  }

  void enterEventEditMode() {
    if (m_editMode == EditMode::Event) {
      return;
    }

    // TODO: Fully implement
    m_editMode = EditMode::Event;
  }

  MapEditor* mapEditor() { return &m_mapEditor; }
  void setDrawTool(DrawTool tool) { m_drawTool = tool; }
  DrawTool drawTool() const { return m_drawTool; }

  MapInfo* currentMapInfo() { return m_mapListView.currentMapInfo(); }
  const MapInfo* currentMapInfo() const { return m_mapListView.currentMapInfo(); }

  Troop* troop(int id) { return m_database->troops.troop(id); }
  const Tileset* tileset(int id) const { return m_database->tilesets.tileset(id); }

  void setMap(MapInfo& in);

  Map* currentMap() { return m_mapEditor.map(); }

  const Map* currentMap() const { return m_mapEditor.map(); }

  DatabaseEditor* databaseEditor() { return m_databaseEditor.has_value() ? &m_databaseEditor.value() : nullptr; }
  const DatabaseEditor* databaseEditor() const { return m_databaseEditor.has_value() ? &m_databaseEditor.value() : nullptr; }

  void addUndo(const std::weak_ptr<IUndoCommand>& cmd) {
    m_undoStack.push(cmd);
    /* Clear the redo stack since adding a new undo command invalidates the redo state */
    m_redoStack.clear();
  }

  std::vector<std::optional<Event>> events() { return currentMap()->events(); }
  std::vector<std::optional<Event>> events() const { return currentMap()->events(); }

  Database& database() { return m_database.value(); }
  const Database& database() const { return m_database.value(); }

  const TilesetPicker& tilesetPicker() const { return m_tilesetPicker; }

  void openMapProperties(MapInfo* mapInfo) {
    m_mapProperties.setMapInfo(mapInfo);
    m_mapProperties.setOpen(true);
  }

  void addToolbarButton(const std::string& id, ToolbarCategory category, asIScriptFunction* func);
  static void RegisterBindings();

  static MainWindow* instance() { return m_instance; }

private:
  void onActorsLoaded();
  void onClassesLoaded();
  void onSkillsLoaded();
  void onItemsLoaded();
  void onWeaponsLoaded();
  void onArmorsLoaded();
  void onEnemiesLoaded();
  void onTroopsLoaded();
  void onStatesLoaded();
  void onAnimationsLoaded();
  void onTilesetsLoaded();
  void onCommonEventsLoaded();
  void onSystemLoaded();
  void onGameConstantsLoaded();
  void onTemplatesLoaded();
  void onDocsLoaded();
  void onLocalesLoaded();
  void onDatabaseReady();
  void onDatabaseModified();

  void drawMenu();
  void drawToolbar();
  void drawQueueStatus(bool shuttingDown);
  void drawShutdownSplash(bool shuttingDown);
  void drawCreateNewProjectPopup();
  void drawTileDebugger();
  void handleKeyboardShortcuts();
  SettingsDialog m_settingsDialog;
  MapListView m_mapListView;
  MapProperties m_mapProperties;
  MapEditor m_mapEditor;
  EventListView m_eventListView;
  TilesetPicker m_tilesetPicker;
  NWJSVersionManager m_nwjsVersionManager;
  EventSearcher m_eventSearcher;
  LibLCF m_libLCF;
  UndoStack m_undoStack;
  UndoStack m_redoStack;
  bool m_isValid = false;
  bool m_isLoaded = false;
  bool m_isModified = false;

  std::unordered_map<ToolbarCategory, std::vector<ToolbarButton>> m_toolbarButtons;
  CreateNewProjectDialog m_createNewProject = CreateNewProjectDialog("Create New Project");
  std::optional<Database> m_database;

  std::optional<DatabaseEditor> m_databaseEditor;

  EditMode m_editMode = EditMode::Event;
  DrawTool m_drawTool = DrawTool::Pencil;

  bool m_showDemoWindow{};
  bool m_showAboutWindow{};
  bool m_showTileDebug{false};

  float m_menuBarHeight{};
  float m_toolbarButtonSize{};
  float m_toolbarSize = 32;
  static MainWindow* m_instance;
};


code:
#include "Core/MapEditor.hpp"

#include "Core/MainWindow.hpp"

#include "Core/ImGuiExt/ImGuiNotify.hpp"
#include "Core/Tilemap/TilePalette.hpp"

#include "Database/EventParser.hpp"
#include "Utils/JavaMath.hpp"

#include "imgui.h"
#include "imgui_internal.h"
#include "ImGuiExt/ImGuiUtils.hpp"

#include <clip.h>

#include <format>

static clip::format OREMapEventFormat = -1;

struct EventMoveUndoCommand : IUndoCommand {
  EventMoveUndoCommand() = delete;

  EventMoveUndoCommand(Event* event, int x, int y)
  : m_event(event)
  , m_x(x)
  , m_y(y) {}

  int type() const override { return -1; }

  bool undo() override {
    // if (m_event) {
    //   std::swap(m_event->x, m_x);
    //   std::swap(m_event->y, m_y);
    //   return true;
    // }

    return false;
  }

  std::string description() override {
    return {};
    // if (!m_event) {
    //   return "Invalid move command!";
    // }
    // if (!m_isRedo) {
    //   return std::format("Move {} from {},{} to {},{}", m_event->name.empty() ? std::to_string(m_event->id) : m_event->name, m_x, m_y, m_event->x, m_event->y);
    // }
    // return std::format("Move {} from {},{} to {},{}", m_event->name.empty() ? std::to_string(m_event->id) : m_event->name, m_event->x, m_event->y, m_x, m_y);
  }

private:
  Event* m_event = nullptr;
  int m_x = -1;
  int m_y = -1;
};

void MapEditor::setMap(MapInfo* info) {
  if (m_mapInfo && m_mapInfo->mapLoaded()) {
    m_mapInfo->map()->parallaxNameModified().disconnect<&MapEditor::onParallaxNameModified>(this);
  }
  m_mapRenderer.setMap(nullptr, nullptr);
  m_mapInfo = info;
  m_parallaxTexture = ParallaxTexture();
}

int MapEditor::tileSize() const { return Database::instance()->system.tileSize(); }

void MapEditor::onParallaxNameModified(Map* info, const std::string& name) { m_parallaxTexture.setTexture(name); }

void MapEditor::drawParallax(ImGuiWindow* win) {
  if (!map()) {
    return;
  }

  if (map()->parallaxShow() && !map()->parallaxName().empty() && !m_parallaxTexture) {
    m_parallaxTexture = ParallaxTexture(map()->parallaxName(), {map()->width(), map()->height()}, {map()->parallaxSx(), map()->parallaxSy()}, map()->parallaxLoopX(), map()->parallaxLoopY());
    map()->parallaxNameModified().connect<&MapEditor::onParallaxNameModified>(this);
    // m_parallaxTexture.enableUpdates();
  } else if (map() && (!map()->parallaxShow() || map()->parallaxName().empty())) {
    m_parallaxTexture = ParallaxTexture();
  }

  if (!m_parallaxTexture) {
    return;
  }

  if (m_prisonMode != !m_parallaxTexture.areUpdatesEnabled()) {
    m_parallaxTexture.setUpdatesEnabled(!m_prisonMode);
  }

  if (m_parallaxTexture.width() != map()->width() || m_parallaxTexture.height() != map()->height()) {
    m_parallaxTexture.setSize(map()->width(), map()->height());
  }

  if (m_parallaxTexture.scroll().x() != map()->parallaxSx() || m_parallaxTexture.scroll().y() != map()->parallaxSy()) {
    m_parallaxTexture.setScroll(map()->parallaxSx(), map()->parallaxSy());
  }

  if (m_parallaxTexture.loopX() != map()->parallaxLoopX() || m_parallaxTexture.loopY() != map()->parallaxLoopY()) {
    m_parallaxTexture.setLoopX(map()->parallaxLoopX());
    m_parallaxTexture.setLoopY(map()->parallaxLoopY());
  }

  // TODO: Proper parallax implementation
  win->DrawList->AddImage(static_cast<ImTextureID>(m_parallaxTexture), win->ContentRegionRect.Min + ImVec2{0.f, 0.f},
                          win->ContentRegionRect.Min + (ImVec2{static_cast<float>(m_parallaxTexture.pixelWidth()), static_cast<float>(m_parallaxTexture.pixelHeight())} * m_mapScale));
}

void MapEditor::drawGrid(ImGuiWindow* win) {
  // for (int y = tileSize() * m_mapScale; y < (map()->height() * tileSize()) * m_mapScale; y += tileSize() * m_mapScale) {
  //   win->DrawList->AddLine(win->ContentRegionRect.Min + ImVec2{0.f, static_cast<float>(y)}, win->ContentRegionRect.Min + ImVec2{(map()->width() * tileSize()) * m_mapScale, static_cast<float>(y)},
  //                          IM_COL32(10, 10, 10, 240), ImGui::GetDPIScaledValue(1.5f));
  // }
  //
  // for (int x = tileSize() * m_mapScale; x < (map()->width() * tileSize()) * m_mapScale; x += tileSize() * m_mapScale) {
  //   win->DrawList->AddLine(win->ContentRegionRect.Min + ImVec2{static_cast<float>(x), 0.f}, win->ContentRegionRect.Min + ImVec2{static_cast<float>(x), (map()->height() * tileSize()) *
  //   m_mapScale},
  //                          IM_COL32(10, 10, 10, 240), ImGui::GetDPIScaledValue(1.5f));
  // }
}

float roundToNearestQuarter(float num) { return static_cast<float>(static_cast<int>(num * 4)) / 4; }

void MapEditor::handleEventDrag() {
  if (m_movingEvent) {
    /* For now we'll prevent events from occupying the same tile */
    /* TODO(phil): Implement some way to sort through events on the same tile */
    const int oldX = m_movingEvent->x();
    const int oldY = m_movingEvent->y();

    const auto it = std::ranges::find_if(map()->events(), [&](const std::optional<Event>& e) { return e && e->x() == tileCellX() && e->y() == tileCellY() && &e.value() != m_movingEvent; });

    m_movingEvent->setX(m_tileCursor.tileX());
    m_movingEvent->setY(m_tileCursor.tileY());
    if (it != map()->events().end()) {
      m_movingEvent->setX(oldX);
      m_movingEvent->setY(oldY);
    }
  }
}

void MapEditor::handleMouseInput(ImGuiWindow* win) {
  // if (map()) {
  //   map()->data[(tileCellY() * map()->width) + tileCellX()] = 2086;
  //   updateAutotilesAroundPoint(Point{tileCellX(), tileCellY()}, 0);
  // }
  const bool wasCursorVisible = m_tileCursor.visible();

  if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) || ImGui::IsKeyPressed(ImGuiKey_RightArrow) || ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
    m_tileCursor.setKeyboardMode();
  } else if (std::fabs(ImGui::GetIO().MouseDelta.x) > 0.5f || std::fabs(ImGui::GetIO().MouseDelta.y) > 0.5f) {
    m_tileCursor.setMouseMode();
  }

  if (ImGui::IsWindowHovered() && m_parent->editMode() == EditMode::Map) {
    m_tileCursor.update(m_mapScale, map()->width(), map()->height(), Database::instance()->system.tileSize(), win);
  } else if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left) && (m_parent->editMode() == EditMode::Event || m_movingEvent)) {
    m_tileCursor.update(m_mapScale, map()->width(), map()->height(), Database::instance()->system.tileSize(), win);
    handleEventDrag();
  } else if (m_scaleChanged || m_tileCursor.mode() == MapCursorMode::Keyboard) {
    m_tileCursor.update(m_mapScale, map()->width(), map()->height(), Database::instance()->system.tileSize(), win);
    handleEventDrag();
  }

  if (m_scaleChanged && ImGui::IsWindowHovered()) {
    ImGui::SetScrollX((win->ContentRegionRect.Min.x / 2) + ((m_tileCursor.absoluteX() * m_mapScale) - (win->ContentRegionRect.Max.x / 2)));
    ImGui::SetScrollY((win->ContentRegionRect.Min.y / 2) + ((m_tileCursor.absoluteY() * m_mapScale) - (win->ContentRegionRect.Max.y / 2)));
    m_tileCursor.setVisible(wasCursorVisible);
    m_scaleChanged = false;
  }

  if (m_parent->editMode() == EditMode::Event && m_tileCursor.mode() == MapCursorMode::Keyboard && (ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) &&
      (!m_movingEvent || !m_selectedEvent)) {
    m_selectedEvent = map()->eventAt(m_tileCursor.tileX(), m_tileCursor.tileY());
    if (m_selectedEvent && m_selectedEvent->id() == 0) {
      m_selectedEvent = nullptr;
    }
    m_movingEvent = m_selectedEvent;
    if (m_movingEvent) {
      m_movingEventX = m_movingEvent->x();
      m_movingEventY = m_movingEvent->y();
    }
  } else if (m_parent->editMode() == EditMode::Event && m_tileCursor.mode() == MapCursorMode::Keyboard && (ImGui::IsKeyReleased(ImGuiKey_LeftShift) || ImGui::IsKeyReleased(ImGuiKey_RightShift))) {
    if (m_movingEvent) {
      m_movingEvent->setX(m_tileCursor.tileX());
      m_movingEvent->setY(m_tileCursor.tileY());
    }
  }

  if (m_selectedEvent && !m_movingEvent && m_selectedEvent->x() != m_tileCursor.tileX() && m_selectedEvent->y() != m_tileCursor.tileY()) {
    m_selectedEvent = nullptr;
  }

  if ((ImGui::IsMouseClicked(ImGuiMouseButton_Left) && m_parent->editMode() == EditMode::Event && (m_movingEvent || m_selectedEvent))) {
    auto event = map()->eventAt(m_tileCursor.tileX(), m_tileCursor.tileY());
    if (event == nullptr) {
      m_movingEvent = nullptr;
      m_selectedEvent = nullptr;
    } else {
      m_selectedEvent = event;
    }
    m_hasScrolled = true;
  }

  if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::IsWindowHovered() && m_parent->editMode() == EditMode::Event && m_movingEvent == nullptr) {
    m_selectedEvent = map()->eventAt(m_tileCursor.tileX(), m_tileCursor.tileY());
    if (m_selectedEvent && m_selectedEvent->id() == 0) {
      m_selectedEvent = nullptr;
    }
    m_movingEvent = m_selectedEvent;
    if (m_movingEvent) {
      m_movingEventX = m_movingEvent->x();
      m_movingEventY = m_movingEvent->y();
    }
    m_hasScrolled = true;
  }

  if ((ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) && m_parent->editMode() == EditMode::Event &&
      ImGui::IsWindowFocused()) {
    if (m_selectedEvent != nullptr) {
      m_selectedEvent->editor()->open();
    } else if (ImGui::IsWindowFocused()) {
      /* Prevent creating an event on top of the start event */
      if (const auto* tmp = map()->eventAt(tileCellX(), tileCellY()); !tmp || tmp->id() != 0) {
        /* TODO: Use temporary event object */
        const auto ev = map()->createNewEvent();
        ev->setX(tileCellX());
        ev->setY(tileCellY());
        ev->editor()->open();
      }
    }
  }

  if ((ImGui::IsMouseClicked(ImGuiMouseButton_Left) || ImGui::IsMouseDragging(ImGuiMouseButton_Left)) && m_parent->editMode() == EditMode::Map &&
      (ImGui::IsWindowFocused() && ImGui::IsWindowHovered()) && map()) {
    const auto& penData = m_parent->tilesetPicker().penData();
    int layer = -2;
    if (!penData.empty()) {
      if (m_parent->tilesetPicker().isRegionMode()) {
        map()->setTileAt(penData[0][0], m_tileCursor.tileX(), m_tileCursor.tileY(), 5);
      } else {
        switch (m_parent->drawTool()) {
        case DrawTool::Pencil: {
          if (layer == -1) {
            for (int i = 0; i < 4; ++i) {
              if (penData[0][i] == -1) {
                continue;
              }
              map()->setTileAt(penData[0][i], m_tileCursor.tileX(), m_tileCursor.tileY(), i);
              updateAutotilesAroundPoint({m_tileCursor.tileX(), m_tileCursor.tileY()}, i);
            }
          } else {
            const int layerCount = layer == -2 ? 4 : 1;
            layer = layer == -2 ? 0 : layer;
            for (int i = 0; i < penData.size(); ++i) {
              for (int z = layer; z < layerCount; ++z) {
                if (penData[i][z] == -1) {
                  continue;
                }
                map()->setTileAt(penData[i][z], m_tileCursor.tileX(), m_tileCursor.tileY(), z);
                if (TileHelper::isAutoTile(penData[i][z])) {
                  updateAutotilesAroundPoint({m_tileCursor.tileX(), m_tileCursor.tileY()}, z);
                }
              }
            }
          }
        } break;
        case DrawTool::Rectangle:
          break;
        case DrawTool::Ellipse:
          break;
        case DrawTool::Flood_Fill:
          break;
        case DrawTool::Shadow_Pen:
          break;
        case DrawTool::Eraser:
          if (m_parent->tilesetPicker().isRegionMode()) {
            // Clear region tiles
            map()->setTileAt(0, m_tileCursor.tileX(), m_tileCursor.tileY(), 5);
          } else {
            for (int i = 0; i < 5; ++i) {
              map()->setTileAt(0, m_tileCursor.tileX(), m_tileCursor.tileY(), i);
              updateAutotilesAroundPoint({m_tileCursor.tileX(), m_tileCursor.tileY()}, i);
            }
          }
          break;
        }
      }
    }
  }

  if (((m_tileCursor.mode() == MapCursorMode::Keyboard && (ImGui::IsKeyReleased(ImGuiKey_LeftShift)) || ImGui::IsKeyReleased(ImGuiKey_RightShift)) || ImGui::IsMouseReleased(ImGuiMouseButton_Left)) &&
      m_parent->editMode() == EditMode::Event) {
    /* If we have a selected actor, and it's no longer in its original location, push it onto the undo stack
     * as an operation
     */
    if (m_movingEvent != nullptr && (m_movingEvent->x() != m_movingEventX || m_movingEvent->y() != m_movingEventY)) {
      // TODO: Actually implement undo
      // m_parent->addUndo(std::make_shared<EventMoveUndoCommand>(m_movingEvent, m_movingEventX, m_movingEventY));
    }
    m_movingEvent = nullptr;
  }
}

void MapEditor::handleKeyboardShortcuts() {
  /* General */
  if (ImGui::IsKeyDown(ImGuiKey_Delete) || ImGui::IsKeyDown(ImGuiKey_Backspace)) {
    // TODO: Undo command
    if (ImGui::IsWindowFocused() && selectedEvent()) {
      map()->deleteEvent(selectedEvent()->id());
      setSelectedEvent(nullptr);
    }
  }
  if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
    if (ImGui::IsKeyPressed(ImGuiKey_C)) {
      if (OREMapEventFormat == -1) {
        OREMapEventFormat = clip::register_format("application/ore-MapEvent");
      }
      if (ImGui::IsWindowFocused() && selectedEvent()) {
        clip::lock l;
        nlohmann::ordered_json pageJson = *m_selectedEvent;
        auto v = pageJson.dump();
        l.set_data(OREMapEventFormat, v.data(), v.size());
      }
    }
    if (ImGui::IsKeyPressed(ImGuiKey_X)) {
      if (OREMapEventFormat == -1) {
        OREMapEventFormat = clip::register_format("application/ore-MapEvent");
      }
      if (ImGui::IsWindowFocused() && selectedEvent()) {
        clip::lock l;
        nlohmann::ordered_json pageJson = *m_selectedEvent;
        auto v = pageJson.dump();
        l.set_data(OREMapEventFormat, v.data(), v.size());

        map()->deleteEvent(selectedEvent()->id());
        setSelectedEvent(nullptr);
      }
    }
    if (ImGui::IsKeyPressed(ImGuiKey_V)) {
      if (OREMapEventFormat == -1) {
        OREMapEventFormat = clip::register_format("application/ore-MapEvent");
      }
      if (ImGui::IsWindowFocused() && !selectedEvent() && selectedEvent() == nullptr) {
        clip::lock l;
        if (l.is_convertible(OREMapEventFormat)) {
          auto len = l.get_data_length(OREMapEventFormat);
          std::string page;
          page.resize(len);
          if (l.get_data(OREMapEventFormat, page.data(), len)) {
            // Parse from clipboard
            nlohmann::ordered_json pageJson = nlohmann::ordered_json::parse(page);
            Event pastedEvent = pageJson.get<Event>();

            // Set properties
            pastedEvent.setId(map()->findOrMakeFreeId());
            pastedEvent.setX(tileCellX());
            pastedEvent.setY(tileCellY());

            map()->createEventFromTemplate(pastedEvent);
          }
        }
      }
    }
  }
}

void MapEditor::updateAllAutotiles() {
  const auto rect = Rect(0, 0, map()->width(), map()->height());
  updateAutotilesInRect(rect, 0);
  updateAutotilesInRect(rect, 1);
}

void MapEditor::updateAutotilesInRect(const Rect& rect, const int layer) {
  const int minX = rect.x() - 1;
  const int minY = rect.y() - 1;
  const int width = (rect.right() + 1) - minX;
  const int height = (rect.bottom() + 1) - minY;
  for (int y = 0; y < height; ++y) {
    for (int x = 0; x < width; ++x) {
      int flags = 0x10 | 0x08 | 0x01;
      if (x != width) {
        flags = (x == 0 ? 0x40 | 0x04 | 0x02 : 0xFF);
      }

      if (y == height) {
        flags &= 0x20 | 0x02 | 0x01;
      } else if (y == 0) {
        flags &= 0x80 | 0x08 | 0x04;
      }

      Point p{minX + x, minY + y};
      if (!isMapPointValid(p)) {
        break;
      }
      updateAutotile(p, layer, flags);
    }
  }
}

void MapEditor::updateAutotile(const Point& point, const int layer, const int flags) {
  if (isRegionMode() || !isMapPointValid(point)) {
    return;
  }

  auto tileId = readMapData(point, layer);
  if (TileHelper::isFloorTypeAutotile(tileId)) {
    tileId = updateFloorTypeAutotile(point, layer, flags);
  } else if (TileHelper::isWallTypeAutotile(tileId)) {
    tileId = updateWallTypeAutotile(point, layer, flags);
  } else if (TileHelper::isWaterfallTile(tileId)) {
    tileId = updateWaterfallTypeAutotile(point, layer, flags);
  }

  writeMapData(point, layer, tileId);
  /* TODO: Flags */
  if (layer == 0 && (flags & (0x10 | 0x08 | 0x01))) {
    updateShadowData(point);
  }
}

void MapEditor::updateAutotilesAroundPoint(const Point& point, const int layer) {
  updateAutotile(point, layer, 0xFF);
  updateAutotile(Point{point.x(), point.y() - 1}, layer, 0x80 | 0x08 | 0x04);
  updateAutotile(Point{point.x() - 1, point.y()}, layer, 0x40 | 0x04 | 0x02);
  updateAutotile(Point{point.x(), point.y() + 1}, layer, 0x20 | 0x02 | 0x01);
  updateAutotile(Point{point.x() + 1, point.y()}, layer, 0x10 | 0x08 | 0x01);
  updateAutotile(Point{point.x() + 1, point.y() - 1}, layer, 0x08);
  updateAutotile(Point{point.x() - 1, point.y() - 1}, layer, 0x04);
  updateAutotile(Point{point.x() - 1, point.y() + 1}, layer, 0x02);
  updateAutotile(Point{point.x() + 1, point.y() + 1}, layer, 0x01);
}

void MapEditor::updateShadowData(const Point& point) {
  const int tileId = (isGroundTile(point, 0) && isShadowingTile(Point{point.x() - 1, point.y()}, 0) && isShadowingTile(Point{point.x() - 1, point.y() - 1}, 0)) ? 5 : 0;
  writeMapData(point, 4, tileId);
}

int MapEditor::updateFloorTypeAutotile(const Point& point, const int layer, const int flags) const {
  const auto tileId = readMapData(point, layer);
  const auto kind = TileHelper::getAutoTileKind(tileId);
  const auto shape = TileHelper::getAutoTileShape(tileId);
  const auto dir = TileHelper::floorShapeToDir(shape);
  const auto leftTop = makeDirectionBit(Point{point.x() - 1, point.y() - 1}, tileId, layer, flags & 0x01, false);
  const auto rightTop = makeDirectionBit(Point{point.x() + 1, point.y() - 1}, tileId, layer, flags & 0x02, false);
  const auto rightBottom = makeDirectionBit(Point{point.x() + 1, point.y() + 1}, tileId, layer, flags & 0x04, false);
  const auto leftBottom = makeDirectionBit(Point{point.x() - 1, point.y() + 1}, tileId, layer, flags & 0x08, false);
  const auto leftMiddle = makeDirectionBit(Point{point.x() - 1, point.y()}, tileId, layer, flags & 0x10, false);
  const auto topMiddle = makeDirectionBit(Point{point.x(), point.y() - 1}, tileId, layer, flags & 0x20, false);
  const auto rightMiddle = makeDirectionBit(Point{point.x() + 1, point.y()}, tileId, layer, flags & 0x40, false);
  const auto bottomMiddle = makeDirectionBit(Point{point.x(), point.y() + 1}, tileId, layer, flags & 0x80, false);
  const auto newShape = TileHelper::floorDirToShape(rightBottom | rightTop | leftTop | leftBottom | leftMiddle | topMiddle | rightMiddle | bottomMiddle | (dir & ~flags));

  return TileHelper::makeAutoTileId(kind, newShape);
}

int MapEditor::updateWallTypeAutotile(const Point& point, const int layer, const int flags) const {
  const int tileId = readMapData(point, layer);
  const int autotileKind = TileHelper::getAutoTileKind(tileId);
  const int autotileShape = TileHelper::getAutoTileShape(tileId);
  const int direction = TileHelper::wallShapeToDir(autotileShape);

  const int yCoord = point.y();
  const int xCoord = point.x();

  int tmpYStart = yCoord;
  while (isSameKindTile(Point{xCoord, tmpYStart--}, layer, tileId))
    ;

  int tmpYEnd = yCoord;
  while (isSameKindTile(Point{xCoord, tmpYEnd++}, layer, tileId))
    ;

  bool hasSameKindLeft = false;
  bool hasSameKindRight = false;

  if (isSameKindTile(Point{xCoord - 1, yCoord}, layer, tileId)) {
    hasSameKindLeft = !isSameKindTile(Point{xCoord - 1, tmpYStart}, layer, tileId) || !isSameKindTile(Point{xCoord - 1, tmpYEnd}, layer, tileId);
  }

  if (isSameKindTile(Point{xCoord + 1, yCoord}, layer, tileId)) {
    hasSameKindRight = !isSameKindTile(Point{xCoord + 1, tmpYStart}, layer, tileId) || !isSameKindTile(Point{xCoord + 1, tmpYEnd}, layer, tileId);
  }

  if (TileHelper::isWallSideTile(tileId)) {
    if (!hasSameKindLeft) {
      hasSameKindLeft = !isWallOrRoofTile(Point{xCoord - 1, yCoord}, layer) || !isWallOrRoofTile(Point{xCoord - 1, tmpYStart}, layer) || !isWallOrRoofTile(Point{xCoord - 1, tmpYEnd}, layer);
    }

    if (!hasSameKindRight) {
      hasSameKindRight = !isWallOrRoofTile(Point{xCoord + 1, yCoord}, layer) || !isWallOrRoofTile(Point{xCoord + 1, tmpYStart}, layer) || !isWallOrRoofTile(Point{xCoord + 1, tmpYEnd}, layer);
    }
  }

  int updatedDirection = (direction & ~flags) & ~0x10;
  if (!hasSameKindLeft) {
    updatedDirection = (direction & ~flags) | 0x10;
  }
  updatedDirection &= ~0x40;
  if (!hasSameKindRight) {
    updatedDirection |= 0x40;
  }

  const int northDirectionBit = makeDirectionBit(Point{xCoord, yCoord - 1}, tileId, layer, flags & 0x20, true);
  const int southDirectionBit = makeDirectionBit(Point{xCoord, yCoord + 1}, tileId, layer, flags & 0x80, true);

  const int shape = TileHelper::wallDirToShape(southDirectionBit | northDirectionBit | updatedDirection);
  return TileHelper::makeAutoTileId(autotileKind, shape);
}

int MapEditor::updateWaterfallTypeAutotile(const Point& point, const int layer, const int flags) const {
  const auto tileId = readMapData(point, layer);
  const auto kind = TileHelper::getAutoTileKind(tileId);
  const auto shape = TileHelper::getAutoTileShape(tileId);
  const auto dir = TileHelper::waterfallShapeToDir(shape);
  const auto x = point.x();
  const auto y = point.y();
  const auto left = makeDirectionBit(Point{x - 1, y}, tileId, layer, flags & 0x10, false);
  const auto right = makeDirectionBit(Point{x + 1, y}, tileId, layer, flags & 0x40, false);
  return TileHelper::makeAutoTileId(kind, TileHelper::waterfallDirToShape(left | right | (dir & ~flags)));
}

int MapEditor::makeDirectionBit(const Point& nextPos, const int tileId, const int layer, const int flags, const bool skipBorder) const {
  int ret = 0;
  if (flags) {
    const auto nextTileId = readMapData(nextPos, layer);
    if (nextTileId < 0) {
      return 0;
    }
    ret = flags;
    if (!skipBorder) {
      ret = 0;
      if (TileHelper::shouldCreateBorder(tileId, nextTileId)) {
        ret = flags;
      }
    }
  }
  return ret;
}

bool MapEditor::isWallOrRoofTile(const Point& p, const int layer) const { return TileHelper::isWallOrRoofTile(readMapData(p, layer)); }

bool MapEditor::isGroundTile(const Point& p, const int layer) const { return TileHelper::isGroundTile(readMapData(p, layer)); }

bool MapEditor::isShadowingTile(const Point& p, const int layer) const { return TileHelper::isShadowingTile(readMapData(p, layer)); }

bool MapEditor::isSameKindTile(const Point& p, const int layer, const int tileId) const { return TileHelper::isSameKindTile(readMapData(p, layer), tileId); }

void MapEditor::renderLayerRects(ImGuiWindow* win, const MapRenderer::MapLayer& layer) {
  for (const auto& l : layer.tileLayers) {
    for (const auto& tile : l.rects) {
      if (!TileHelper::isAutoTile(tile.tileId)) {
        continue;
      }
      const float x0 = tile.x;
      const float x1 = tile.x + tile.tileWidth;
      const float y0 = tile.y;
      const float y1 = tile.y + tile.tileHeight;
      win->DrawList->AddRect(win->ContentRegionRect.Min + (ImVec2{x0, y0} * m_mapScale), win->ContentRegionRect.Min + (ImVec2{x1, y1} * m_mapScale), 0xFF000000, 0, 0, 2.f);
    }
  }
}

void MapEditor::renderLayerTex(ImGuiWindow* win, const MapRenderer::TileLayer& tLayer) {
  if (!tLayer.tex) {
    return;
  }

  for (const TileRect& tile : tLayer.rects) {
    const float x0 = tile.x;
    const float x1 = tile.x + tile.tileWidth;
    const float y0 = tile.y;
    const float y1 = tile.y + tile.tileHeight;
    const float u0 = tile.u / tLayer.tex.width();
    const float u1 = (tile.u + tile.tileWidth) / tLayer.tex.width();
    const float v0 = tile.v / tLayer.tex.height();
    const float v1 = (tile.v + tile.tileHeight) / tLayer.tex.height();

    const auto minPos = win->WorkRect.Min + (ImVec2{x0, y0} * m_mapScale);
    const auto maxPos = win->WorkRect.Min + (ImVec2{x1, y1} * m_mapScale);
    const float w = tile.tileWidth * m_mapScale;
    const float h = tile.tileHeight * m_mapScale;
    if (minPos.x < win->ClipRect.Min.x - w || maxPos.x > win->ClipRect.Max.x + w) {
      continue;
    }
    if (minPos.y < win->ClipRect.Min.y - h || maxPos.y > win->ClipRect.Max.y + h) {
      continue;
    }
    win->DrawList->AddImage(tLayer.tex, minPos, maxPos, ImVec2{u0, v0}, ImVec2{u1, v1});
  }
}

void MapEditor::renderLayer(ImGuiWindow* win, const MapRenderer::MapLayer& layer) {
  renderLayerTex(win, layer.tileLayers[0]); // A1
  renderLayerTex(win, layer.tileLayers[1]); // A2
  renderLayerTex(win, layer.tileLayers[2]); // A3
  renderLayerTex(win, layer.tileLayers[3]); // A4
  renderLayerTex(win, layer.tileLayers[4]); // A5
  renderLayerTex(win, layer.tileLayers[5]); // B
  renderLayerTex(win, layer.tileLayers[6]); // C
  renderLayerTex(win, layer.tileLayers[7]); // D
  renderLayerTex(win, layer.tileLayers[8]); // E
  // renderLayerRects(win, layer);
}

void MapEditor::draw(const bool closeRequested) {
  if (!m_checkeredBack) {
    m_checkeredBack = CheckerboardTexture(64, 64, CellSizes::_48, 255, 220);
  }
  if (m_mapInfo != nullptr && m_mapInfo->map() != nullptr && m_mapRenderer.map() != m_mapInfo->map()) {
    if (m_mapInfo->id() == Database::instance()->system.startMapId()) {
      const auto& party = Database::instance()->system.partyMembers();
      const auto* actor = Database::instance()->actors.actor(!party.empty() ? party[0] : 1);
      EventImage image;
      if (actor) {
        image.setCharacterName(actor->characterName());
        image.setCharacterIndex(actor->characterIndex());
        image.setDirection(Direction::Down);
      }
      map()->setDummyEvent(image, Database::instance()->system.startX(), Database::instance()->system.startY());
    }

    m_mapRenderer.setMap(m_mapInfo->map(), Database::instance()->tilesets.tileset(m_mapInfo->map()->tilesetId()));
    m_tempMapWidth = m_mapInfo->map()->width();
    m_tempMapHeight = m_mapInfo->map()->height();
    m_initialScrollX = m_mapInfo->scrollX();
    m_initialScrollY = m_mapInfo->scrollY();
    m_initialScrollSet = true;
    const float mapW = (m_tempMapWidth * tileSize());
    const float mapH = (m_tempMapHeight * tileSize());

    if (m_checkeredBack.width() != mapW) {
      m_checkeredBack.setWidth(mapW);
    }

    if (m_checkeredBack.height() != mapH) {
      m_checkeredBack.setHeight(mapH);
    }

    float zoomX = m_mapScale;
    float zoomY = m_mapScale;
    if (m_initialScrollX > mapW) {
      zoomX = m_initialScrollX / mapW;
      // give it a little nudge
      if (zoomX - std::trunc(zoomX) > 0.f) {
        zoomX += 0.2;
      }
    }
    if (m_initialScrollY > mapH) {
      zoomY = m_initialScrollY / mapH;
      // give it a little nudge
      if (zoomY - std::trunc(zoomY) > 0.f) {
        zoomY += 0.2;
      }
    }
    m_mapScale = roundToNearestQuarter(std::max(zoomX, zoomY));
  }

  if (map() && !closeRequested) {
    for (auto& event : map()->events()) {
      if (!event || event->id() == 0) {
        continue;
      }
      event->editor()->draw();
    }
  }

  ImGui::SetNextWindowSizeConstraints(ImGui::GetDPIScaledSize(320, 240 + ImGui::GetMinimumPanelHeight()), {FLT_MAX, FLT_MAX});
  if (ImGui::Begin((tr("Map Editor") + "###mapeditor").c_str(), nullptr, ImGuiWindowFlags_NoScrollbar)) {
    ImGui::BeginChild("##mapcontent_child", {ImGui::GetContentRegionAvail().x, std::max(ImGui::GetMinimumPanelHeight(), ImGui::GetContentRegionAvail().y - ImGui::GetMinimumPanelHeight())}, 0,
                      ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoNav);
    if (ImGui::IsWindowHovered()) {
      // Keep mapScale to a quarter step
      if (ImGui::IsKeyDown(ImGuiKey_MouseWheelY) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
        m_mapScale += ImGui::GetIO().MouseWheel * 0.25f;
        m_scaleChanged = true;
      }

      m_mapScale = roundToNearestQuarter(m_mapScale);
      m_mapScale = std::clamp(m_mapScale, .25f, 4.f);
    }

    {
      // Keep mapScale to a quarter step
      if (m_scaleChanged) {
        m_tileCursor.update(m_mapScale, map()->width(), map()->height(), Database::instance()->system.tileSize(), ImGui::GetCurrentWindow());
      }
      // We don't want to align the scroll position while the cursor is in keyboard mode otherwise the view won't track the tile cursor
      if (map()) {
        const float mapW = (m_tempMapWidth * tileSize()) * m_mapScale;
        const float mapH = (m_tempMapHeight * tileSize()) * m_mapScale;

        if (m_checkeredBack.width() != mapW && m_scaleChanged) {
          m_checkeredBack.setWidth(mapW);
        }

        if (m_checkeredBack.height() != mapH && m_scaleChanged) {
          m_checkeredBack.setHeight(mapH);
        }

        ImGuiWindow* win = ImGui::GetCurrentWindow();
        handleMouseInput(win);
        handleKeyboardShortcuts();
        const auto mapSize = ImVec2{mapW, mapH};
        ImGui::Dummy(mapSize);

        win->DrawList->AddImage(static_cast<ImTextureID>(m_checkeredBack), win->WorkRect.Min, win->WorkRect.Min + mapSize);

        if (ImGui::BeginPopupContextWindow()) {
          if (ImGui::MenuItem(trNOOP("Change event id..."))) {}
          ImGui::SeparatorText("Templates");
          ImGui::BeginDisabled(m_selectedEvent);
          if (ImGui::MenuItem(trNOOP("Insert..."))) {
            m_templateSaving = false;
            m_templatePicker = ObjectPicker(trNOOP("Templates"), Database::instance()->templates.templateList(Template::TemplateType::Event), 0);
            m_templatePicker->setNoSelectionMeansAdd(false);
            m_templatePicker->setOpen(true);
          }
          ImGui::EndDisabled();
          ImGui::BeginDisabled(m_selectedEvent == nullptr);
          if (ImGui::MenuItem(trNOOP("Save as..."))) {
            m_templateSaving = true;
            m_templatePicker = ObjectPicker(trNOOP("Templates"), Database::instance()->templates.templateList(Template::TemplateType::Event), 0);
            m_templatePicker->setNoSelectionMeansAdd(true);
            m_templatePicker->setOpen(true);
          }
          ImGui::EndDisabled();
          ImGui::EndPopup();
        }

        if (m_templatePicker) {
          auto [closed, confirmed] = m_templatePicker->draw();
          if (closed) {
            if (confirmed) {
              if (m_templateSaving) {
                nlohmann::ordered_json eventJson;
                EventParser::serialize(eventJson, *m_selectedEvent);
                if (m_templatePicker.value().selection() == 0) {
                  Database::instance()->templates.addTemplate(Template(Database::instance()->templates.templates.size() + 1,
                                                                       tr("New Event Template") + " " + std::to_string(Database::instance()->templates.templates.size() + 1), "",
                                                                       Template::TemplateType::Event, eventJson.dump(), {}));
                  m_templateNamePicker = TemplateName(&Database::instance()->templates.templates.back(), nullptr);
                } else {
                  Database::instance()->templates.templates.at(m_templatePicker.value().selection() - 1).setCommands(eventJson.dump());
                  m_templateNamePicker = TemplateName(&Database::instance()->templates.templates.at(m_templatePicker.value().selection() - 1), nullptr);
                }
                Database::instance()->templates.templates.back().setCommands(eventJson.dump());
              } else {
                EventParser parser;
                nlohmann::ordered_json eventJson = nlohmann::ordered_json::parse(Database::instance()->templates.templates.at(m_templatePicker->selection() - 1).commands());

                m_templateEvent.emplace(parser.parse(eventJson));
                m_templateEvent.value().setId(map()->events().size() - 1);
                m_eventProperties = TemplatesEvent(&m_templateEvent.value(), map(), nullptr, tileCellX(), tileCellY());
              }
            }
            m_templatePicker.reset();
          }
        }
        if (m_eventProperties) {
          m_eventProperties->draw();
          if (m_eventProperties->hasChanges()) {
            m_eventProperties.reset();
          }
        }
        if (m_templateNamePicker) {
          m_templateNamePicker->draw();
          if (m_templateNamePicker->hasChanges()) {
            if (Database::instance()->templates.serialize(Database::instance()->basePath + "data/Templates.json")) {
              ImGui::InsertNotification(ImGuiToast{ImGuiToastType::Success, trNOOP("Saved event as template successfully!")});
            } else {
              ImGui::InsertNotification(ImGuiToast{ImGuiToastType::Error, trNOOP("Failed to saved event as template!")});
            }
            m_templateNamePicker.reset();
          }
        }

        if (m_initialScrollSet) {
          ImGui::SetScrollX(m_initialScrollX);
          ImGui::SetScrollY(m_initialScrollY);
          m_initialScrollX = m_initialScrollY = 0.0;
          m_initialScrollSet = false;
        }

        m_mapInfo->setScrollX(ImGui::GetScrollX());
        m_mapInfo->setScrollY(ImGui::GetScrollY());

        m_mapRenderer.update();

        drawParallax(win);

        for (int z = 0; z < 4; ++z) {
          renderLayer(win, m_mapRenderer.m_lowerLayers[z]);
          if (m_prisonMode) {
            renderLayer(win, m_mapRenderer.m_upperLayers[z]);
          }
        }

        for (int y = 0; y < map()->height(); ++y) {
          for (int x = 0; x < map()->width(); ++x) {
            auto tile = map()->data()[m_mapRenderer.tileIdFromCoords(x, y, 4)];
            if (!tile) {
              continue;
            }
            const float dx = x * tileSize();
            const float dy = y * tileSize();
            const int bits = *tile;
            if (!(bits & 0x0F)) {
              continue;
            }

            const float w1 = tileSize() / 2;
            const float h1 = tileSize() / 2;
            for (int i = 0; i < 4; ++i) {
              if (!(bits & (1 << i))) {
                continue;
              }
              const float dx1 = dx + ((i % 2) * w1);
              const float dy1 = dy + (std::floor(i / 2) * h1);
              win->DrawList->AddRectFilled(win->ContentRegionRect.Min + (ImVec2{dx1, dy1} * m_mapScale), win->ContentRegionRect.Min + (ImVec2{dx1 + w1, dy1 + h1} * m_mapScale), 0x7F000000);
            }
          }
        }

        auto sortedEvents = prisonMode() ? map()->getSorted() : map()->getRenderSorted();
        for (const auto& event : sortedEvents) {
          if (!event) {
            continue;
          }

          const auto evX = (event->renderer()->x() * tileSize()) * m_mapScale;
          const auto evY = (event->renderer()->y() * tileSize()) * m_mapScale;
          const auto tileSize = m_tileCursor.tileSize() * m_mapScale;
          if (m_selectedEvent == event && !m_hasScrolled) {
            ImGui::SetScrollX((win->ContentRegionRect.Min.x / 2) + (evX - (win->ContentRegionRect.Max.x / 2)));
            ImGui::SetScrollY((win->ContentRegionRect.Min.y / 2) + (evY - (win->ContentRegionRect.Max.y / 2)));
            m_hasScrolled = true;
          }
          const auto min = win->ContentRegionRect.Min + ImVec2{static_cast<float>(evX), static_cast<float>(evY)};
          const auto max = min + ImVec2{tileSize, tileSize};
          const bool isHovered = event->x() == tileCellX() && event->y() == tileCellY();
          const bool updateOnly =
              !((min.x > win->ClipRect.Min.x - tileSize && max.x < win->ClipRect.Max.x + tileSize) || (min.y > win->ClipRect.Min.y - tileSize && max.y < win->ClipRect.Max.y + tileSize));
          if (auto* renderer = static_cast<MapEvent*>(event->renderer())) {
            renderer->setMapEditor(this);
            renderer->draw(m_mapScale, isHovered, m_selectedEvent == event, m_parent->editMode() != EditMode::Event, updateOnly);
          }
        }

        if (!m_prisonMode) {
          for (int z = 0; z < 6; z++) {
            renderLayer(win, m_mapRenderer.m_upperLayers[z]);
          }
        }

        if (m_prisonMode) {
          drawGrid(win);
        }
        if (ImGui::IsWindowHovered() || m_parent->editMode() == EditMode::Event) {
          m_tileCursor.draw(win);
        }

        // if (m_tileCursor.mode() != MapCursorMode::Keyboard && !m_scaleChanged) {
        //   static float tempX = win->Scroll.x;
        //   if (fabs(win->Scroll.x - tempX) > (m_tileCursor.tileSize() * m_mapScale) / 2 || !ImGui::IsKeyDown(ImGuiKey_MouseWheelX)) {
        //     win->Scroll.x = m_tileCursor.alignCoord(win->Scroll.x + (m_tileCursor.tileSize() * m_mapScale) / 2);
        //     tempX = win->Scroll.x;
        //   }
        //   static float tempY = win->Scroll.y;
        //   if (fabs(win->Scroll.y - tempY) > (m_tileCursor.tileSize() * m_mapScale) / 2 || !ImGui::IsKeyDown(ImGuiKey_MouseWheelY)) {
        //     win->Scroll.y = m_tileCursor.alignCoord(win->Scroll.y + (m_tileCursor.tileSize() * m_mapScale) / 2);
        //     tempY = win->Scroll.y;
        //   }
        // }
      }
    }
    ImGui::EndChild();
    ImGui::BeginChild("##map_editor_bottom_panel", ImVec2{}, ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysUseWindowPadding,
                      ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
    {
      // height = ImGui::GetCursorPosY() + ;
      ImGui::BeginHorizontal("##map_editor_bottom_panel_layout", ImGui::GetContentRegionAvail());
      {
        ImGui::AlignTextToFramePadding();
        ImGui::Spring(0.5f);
        ImGui::TextUnformatted(trNOOP("Scale:"));
        if (ImGui::SliderFloat("##map_scale", &m_mapScale, 0.25f, 4.f, "%.02f")) {
          m_mapScale = roundToNearestQuarter(m_mapScale);
        }
        // TL-NOTE: The braces denote the tile ID x and y positions, they get replaced at runtime with those values
        std::string fmt = trFormat("Tile {}, ({}, {})", m_mapRenderer.tileId(m_tileCursor.tileX(), m_tileCursor.tileY(), 0), m_tileCursor.tileX(), m_tileCursor.tileY());
        if (map()) {
          if (const auto ev =
                  std::ranges::find_if(map()->events(), [&](const std::optional<Event>& e) { return e && e->id() != 0 && m_tileCursor.tileX() == e->x() && m_tileCursor.tileY() == e->y(); });
              ev != map()->events().end()) {
            fmt += std::format(" {} ({:03})", (*ev)->name(), (*ev)->id());
          }
        }
        ImGui::TextUnformatted(fmt.c_str());
        ImGui::Spring(0.5f);
      }
      ImGui::EndHorizontal();
    }
    ImGui::EndChild();
  }
  ImGui::End();
}

double MapEditor::adjustX(double x) { return x + ((ImGui::GetCurrentWindow()->ContentRegionRect.Min.x / (tileSize() * m_mapScale))); }

double MapEditor::adjustY(double y) { return y + ((ImGui::GetCurrentWindow()->ContentRegionRect.Min.y / (tileSize() * m_mapScale))); }

double MapEditor::roundX(const double x) { return isLoopHorizontally() ? Math::mod(x, map()->width()) : x; }
double MapEditor::roundY(const double y) { return isLoopVertically() ? Math::mod(y, map()->height()) : y; }

double MapEditor::xWithDirection(const double x, const Direction d) { return x + (d == Direction::Right ? 1 : d == Direction::Left ? -1 : 0); }

double MapEditor::yWithDirection(const double y, const Direction d) { return y + (d == Direction::Down ? 1 : d == Direction::Up ? -1 : 0); }

double MapEditor::roundXWithDirection(const double x, const Direction d) { return roundX(x + (d == Direction::Right ? 1 : d == Direction::Left ? -1 : 0)); }

double MapEditor::roundYWithDirection(const double y, const Direction d) { return roundY(y + (d == Direction::Down ? 1 : d == Direction::Up ? -1 : 0)); }