#include "window.hpp"

#include <algorithm>
#include <format>
#include <fstream>
#include <vector>

#include <imgui.h>
#include <imgui_internal.h>

#include "../log.hpp"
#include "../util/physfs.hpp"
#include "../util/preferences.hpp"

namespace game::developer
{
  bool gIsDocumentUndoMenuRequested{};
  bool gIsDocumentRedoMenuRequested{};

  namespace window_detail
  {
    constexpr auto RECENT_FILES_MAX = 10;
    constexpr auto CENTER_RATIO = 0.5f;
    constexpr auto DEVELOPER_WINDOW_SIZE_RATIO = 0.75f;
    constexpr auto INFO_SCHEMA_TABLE_COLUMNS = 4;
    constexpr auto DEVELOPER_WINDOW_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings;
    constexpr auto EDITOR_CHILD_FLAGS = ImGuiWindowFlags_MenuBar;
    constexpr auto DIALOGUE_SCHEMA_FILENAME = "dialogue.xml";

    using Schema = resource::xml::Schema;
    using Element = Schema::Element;

#define GAME_DEVELOPER_SCHEMA_FILES(X)                                                                                \
  X(CHARACTER, "character.xml", "Character", Element::CHARACTER)                                                      \
  X(DIALOGUE, "dialogue.xml", "Dialogue", Element::DIALOGUE)                                                         \
  X(ITEMS, "items.xml", "Items", Element::ITEM_SCHEMA)                                                               \
  X(AREAS, "areas.xml", "Areas", Element::AREAS)                                                                     \
  X(MENU, "menu.xml", "Menu", Element::MENU)                                                                         \
  X(CURSOR, "cursor.xml", "Cursor", Element::CURSOR)                                                                 \
  X(PARTICLES, "particles.xml", "Particles", Element::PARTICLES)                                                     \
  X(STRINGS, "strings.xml", "Strings", Element::STRINGS)

    struct SchemaInfo
    {
      std::string_view filename{};
      std::string_view label{};
      Element::Type type{};
    };

    constexpr SchemaInfo SCHEMA_INFOS[]{
#define GAME_DEVELOPER_SCHEMA_INFO(SYMBOL, FILENAME, LABEL, TYPE) {.filename = FILENAME, .label = LABEL, .type = TYPE},
        GAME_DEVELOPER_SCHEMA_FILES(GAME_DEVELOPER_SCHEMA_INFO)
#undef GAME_DEVELOPER_SCHEMA_INFO
    };

    std::filesystem::path path_normalized_get(const std::filesystem::path& path)
    {
      std::error_code error{};
      auto normalized = std::filesystem::weakly_canonical(path, error);
      if (!error) return normalized;
      return std::filesystem::absolute(path, error).lexically_normal();
    }

    std::filesystem::path recent_file_path_get() { return util::preferences::path() / "recent.txt"; }

    std::string document_path_label_get(const std::filesystem::path& path)
    {
      auto filename = path.filename();
      if (filename.empty()) return path.string();
      return filename.string();
    }

    bool shortcut_is_pressed(ImGuiKeyChord chord)
    {
      auto& io = ImGui::GetIO();
      return !io.WantTextInput && !ImGui::IsAnyItemActive() &&
             ImGui::Shortcut(chord, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused);
    }

#undef GAME_DEVELOPER_SCHEMA_FILES
  }

  using namespace window_detail;

  void Window::recent_files_load()
  {
    if (isRecentFilesLoaded) return;
    isRecentFilesLoaded = true;
    recentFiles.clear();

    std::ifstream file(recent_file_path_get());
    if (!file.is_open()) return;

    std::string line{};
    while (std::getline(file, line))
    {
      if (line.empty()) continue;
      recentFiles.push_back(path_normalized_get(line));
      if ((int)recentFiles.size() >= RECENT_FILES_MAX) break;
    }
  }

  void Window::recent_files_save()
  {
    auto recentPath = recent_file_path_get();
    if (recentPath.empty()) return;
    std::error_code error{};
    if (!recentPath.parent_path().empty()) std::filesystem::create_directories(recentPath.parent_path(), error);

    std::ofstream file(recentPath, std::ios::trunc);
    if (!file.is_open())
    {
      logger.warning(std::format("Failed to write {}", recentPath.string()));
      return;
    }

    for (auto& path : recentFiles)
      file << path.string() << '\n';
  }

  void Window::recent_file_push(const std::filesystem::path& path)
  {
    recent_files_load();
    auto normalized = path_normalized_get(path);
    if (normalized.empty()) return;

    std::erase_if(recentFiles, [&](const auto& recent) { return recent == normalized; });
    recentFiles.insert(recentFiles.begin(), normalized);
    if ((int)recentFiles.size() > RECENT_FILES_MAX) recentFiles.resize(RECENT_FILES_MAX);
    recent_files_save();
  }

  Document Window::document_load(const std::filesystem::path& path)
  {
    Document document{};
    document.path = path_normalized_get(path);

    for (auto& info : SCHEMA_INFOS)
    {
      Document::SchemaFile schemaFile{};
      schemaFile.filename = info.filename;
      schemaFile.label = info.label;
      schemaFile.type = info.type;
      schemaFile.path = document.path / schemaFile.filename;
      schemaFile.isExists = std::filesystem::is_regular_file(schemaFile.path);
      if (schemaFile.isExists)
      {
        schemaFile.isValid = schemaFile.schema.load(schemaFile.path);
        auto* root = schemaFile.schema.root();
        if (!root || root->type != schemaFile.type) schemaFile.isValid = false;
      }
      document.schemas.emplace_back(std::move(schemaFile));
    }

    return document;
  }

  void Window::folder_open(const std::filesystem::path& path)
  {
    auto normalizedPath = path_normalized_get(path);
    std::error_code error{};
    if (!std::filesystem::is_directory(normalizedPath, error))
    {
      logger.warning(std::format("Ignored {}", normalizedPath.string()));
      return;
    }

    auto it = std::ranges::find_if(documents, [&](const auto& document) { return document.path == normalizedPath; });
    if (it != documents.end())
    {
      *it = document_load(normalizedPath);
      it->history_clear();
      it->isDirty = false;
      selectedDocument = (int)std::distance(documents.begin(), it);
      logger.info(std::format("Opened {}", normalizedPath.string()));
      toasts.push(std::format("Opened {}", normalizedPath.string()));
    }
    else
    {
      documents.push_back(document_load(normalizedPath));
      selectedDocument = (int)documents.size() - 1;
      logger.info(std::format("Opened {}", normalizedPath.string()));
      toasts.push(std::format("Opened {}", normalizedPath.string()));
    }
    recent_file_push(normalizedPath);
  }

  void Window::save(int index)
  {
    if (index < 0 || index >= (int)documents.size()) return;
    auto& document = documents[index];
    auto* dialogue = document.dialogue_schema_get();
    if (!dialogue || !dialogue->isExists)
    {
      toasts.push("No dialogue.xml to save");
      return;
    }

    logger.info(std::format("Wrote to {}", dialogue->path.string()));
    dialogue->schema.serialize(dialogue->path);
    documents[index].isDirty = false;
    toasts.push(std::format("Wrote to {}", dialogue->path.string()));
    recent_file_push(documents[index].path);
  }

  void Window::export_to(int index, const std::filesystem::path& path)
  {
    if (index < 0 || index >= (int)documents.size()) return;
    if (documents[index].isDirty) save(index);
    auto destination = path_normalized_get(path);
    if (util::physfs::archive_write(documents[index].path, destination))
      toasts.push(std::format("Exported {}", destination.string()));
    else
      toasts.push(std::format("Failed to export {}", destination.string()));
  }

  void Window::close(int index)
  {
    if (index < 0 || index >= (int)documents.size()) return;
    auto label = document_path_label_get(documents[index].path);
    documents.erase(documents.begin() + index);
    selectedDocument = documents.empty() ? 0 : std::clamp(index, 0, (int)documents.size() - 1);
    toasts.push(std::format("Closed {}", label));
  }

  void Window::undo(int index)
  {
    if (index < 0 || index >= (int)documents.size()) return;
    auto& document = documents[index];
    if (!document.can_undo()) return;

    auto message = document.undo_apply();
    if (!message.empty()) toasts.push(std::format("Undo: {}", message));
  }

  void Window::redo(int index)
  {
    if (index < 0 || index >= (int)documents.size()) return;
    auto& document = documents[index];
    if (!document.can_redo()) return;

    auto message = document.redo_apply();
    if (!message.empty()) toasts.push(std::format("Redo: {}", message));
  }

  void Window::dropped_file_open(const char* path)
  {
    if (!path) return;
    folder_open(path);
  }

  void Window::editor_shortcuts_update()
  {
    if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_O)) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
    if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z))
      redo(selectedDocument);
    else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_Z))
      undo(selectedDocument);
    if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_S))
      dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
    else if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_S))
      save(selectedDocument);
    if (shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_W)) close(selectedDocument);
  }

  void Window::dialog_selection_update()
  {
    if (dialog.is_selected(util::Dialog::SCHEMA_FOLDER_OPEN))
    {
      folder_open(dialog.path);
      dialog.reset();
    }
    else if (dialog.is_selected(util::Dialog::DOCUMENT_EXPORT))
    {
      export_to(selectedDocument, dialog.path);
      dialog.reset();
    }
  }

  void Window::editor_menu_update()
  {
    if (!ImGui::BeginMenuBar()) return;

    if (ImGui::BeginMenu("File"))
    {
      if (ImGui::MenuItem("Open Folder", "Ctrl+O")) dialog.folder_open(util::Dialog::SCHEMA_FOLDER_OPEN);
      if (ImGui::BeginMenu("Open Recent", !recentFiles.empty()))
      {
        for (int recentIndex = 0; recentIndex < (int)recentFiles.size(); ++recentIndex)
        {
          ImGui::PushID(recentIndex);
          auto label = recentFiles[recentIndex].string();
          if (ImGui::MenuItem(label.c_str())) folder_open(recentFiles[recentIndex]);
          ImGui::PopID();
        }
        ImGui::EndMenu();
      }
      if (ImGui::MenuItem("Save", "Ctrl+S", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
        save(selectedDocument);
      if (ImGui::MenuItem("Export", "Ctrl+Shift+S", false,
                          selectedDocument >= 0 && selectedDocument < (int)documents.size()))
        dialog.file_save(util::Dialog::DOCUMENT_EXPORT);
      if (ImGui::MenuItem("Close", "Ctrl+W", false, selectedDocument >= 0 && selectedDocument < (int)documents.size()))
        close(selectedDocument);
      ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Edit"))
    {
      auto isCanUndo =
          selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
      auto isCanRedo =
          selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
      if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) undo(selectedDocument);
      if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) redo(selectedDocument);
      ImGui::EndMenu();
    }
    ImGui::EndMenuBar();
  }

  void Window::editor_documents_update(Resources& resources)
  {
    if (documents.empty() || !ImGui::BeginTabBar("schemas", ImGuiTabBarFlags_Reorderable)) return;

    auto closeRequest = -1;
    for (int i = 0; i < (int)documents.size(); ++i)
    {
      ImGui::PushID(i);
      auto label = document_path_label_get(documents[i].path) + (documents[i].isDirty ? " *" : "");
      label += std::format("###{}", documents[i].path.string());

      auto isOpen = true;
      auto isTabOpen = ImGui::BeginTabItem(label.c_str(), &isOpen);
      if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal))
        ImGui::SetTooltip("%s", documents[i].path.string().c_str());

      if (isTabOpen)
      {
        selectedDocument = i;
        if (ImGui::BeginTabBar("##Document Tabs"))
        {
          if (ImGui::BeginTabItem("Info"))
          {
            document_info_update(documents[i]);
            ImGui::EndTabItem();
          }
          if (ImGui::BeginTabItem("Dialogue"))
          {
            document_dialogue_update(resources, documents[i]);
            ImGui::EndTabItem();
          }
          ImGui::EndTabBar();
        }
        ImGui::EndTabItem();
      }
      if (!isOpen) closeRequest = i;
      ImGui::PopID();
    }
    ImGui::EndTabBar();
    if (closeRequest >= 0) close(closeRequest);
  }

  void Window::document_info_update(const Document& document)
  {
    if (!ImGui::BeginTable("##Schema Info", INFO_SCHEMA_TABLE_COLUMNS, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
      return;

    ImGui::TableSetupColumn("Schema");
    ImGui::TableSetupColumn("File");
    ImGui::TableSetupColumn("Exists");
    ImGui::TableSetupColumn("Valid");
    ImGui::TableHeadersRow();

    for (auto& schema : document.schemas)
    {
      ImGui::TableNextRow();
      ImGui::TableNextColumn();
      ImGui::TextUnformatted(schema.label.c_str());
      ImGui::TableNextColumn();
      ImGui::TextUnformatted(schema.filename.c_str());
      ImGui::TableNextColumn();
      ImGui::TextUnformatted(schema.isExists ? "Yes" : "No");
      ImGui::TableNextColumn();
      ImGui::TextUnformatted(schema.isValid ? "Yes" : "No");
    }

    ImGui::EndTable();
  }

  void Window::document_dialogue_update(Resources& resources, Document& document)
  {
    auto* dialogue = document.dialogue_schema_get();
    if (!dialogue || !dialogue->isExists)
    {
      ImGui::TextUnformatted("No dialogue.xml found.");
      return;
    }
    if (!dialogue->isValid)
    {
      ImGui::TextUnformatted("dialogue.xml is not valid.");
      return;
    }

    if (ImGui::IsAnyItemActive()) document.edit_snapshot_begin();
    std::vector<Editor::LoadedSchema> loadedSchemas{};
    for (auto& schemaFile : document.schemas)
    {
      if (!schemaFile.isValid) continue;
      loadedSchemas.push_back({.path = &schemaFile.path, .schema = &schemaFile.schema});
    }

    auto before = dialogue->schema;
    if (document.editor.update(resources, dialogue->schema, dialogue->path, dialog, loadedSchemas))
    {
      before = document.edit_snapshot_take_or(std::move(before));
      if (before.element_serialize(0) != dialogue->schema.element_serialize(0))
      {
        auto message = Document::change_message_get(before, dialogue->schema);
        document.snapshot_push(std::move(before), message);
      }
      else
        document.isDirty = true;
    }
    else if (!ImGui::IsAnyItemActive())
      document.edit_snapshot_clear();
  }

  void Window::editor_update(Resources& resources)
  {
    if (ImGui::BeginChild("##Editor", ImVec2(), ImGuiChildFlags_None, EDITOR_CHILD_FLAGS))
    {
      editor_menu_update();

      if (ImGui::BeginPopupContextWindow("developer-context",
                                         ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
      {
        auto isCanUndo =
            selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_undo();
        auto isCanRedo =
            selectedDocument >= 0 && selectedDocument < (int)documents.size() && documents[selectedDocument].can_redo();
        if (ImGui::MenuItem("Undo", "Ctrl+Z", false, isCanUndo)) isContextUndoRequested = true;
        if (ImGui::MenuItem("Redo", "Ctrl+Shift+Z", false, isCanRedo)) isContextRedoRequested = true;
        ImGui::EndPopup();
      }

      editor_documents_update(resources);
    }
    ImGui::EndChild();
  }

  void Window::update(Resources& resources, SDL_Window* window, state::Play* play)
  {
    dialog.window_set(window);

    if (!resources.isDeveloper && shortcut_is_pressed(ImGuiKey_GraveAccent))
    {
      resources.isDeveloper = true;
      if (selectedTab == Tab::CONSOLE) console.focus_request();
    }

    if (!resources.isDeveloper)
    {
      isDeveloperOpen = false;
      return;
    }

    recent_files_load();

    auto* viewport = ImGui::GetMainViewport();
    auto windowSize =
        ImVec2(viewport->WorkSize.x * DEVELOPER_WINDOW_SIZE_RATIO, viewport->WorkSize.y * DEVELOPER_WINDOW_SIZE_RATIO);
    auto windowPosition = ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x * CENTER_RATIO),
                                 viewport->WorkPos.y + (viewport->WorkSize.y * CENTER_RATIO));
    ImGui::SetNextWindowPos(windowPosition, ImGuiCond_Appearing, ImVec2(CENTER_RATIO, CENTER_RATIO));
    ImGui::SetNextWindowSize(windowSize, ImGuiCond_Appearing);

    if (!isDeveloperOpen)
    {
      ImGui::SetNextWindowFocus();
      if (selectedTab == Tab::CONSOLE) console.focus_request();
    }
    isDeveloperOpen = true;

    isContextUndoRequested = false;
    isContextRedoRequested = false;
    action = {};
    console.action = {};
    gIsDocumentUndoMenuRequested = false;
    gIsDocumentRedoMenuRequested = false;

    if (selectedTab == Tab::EDITOR) editor_shortcuts_update();
    dialog_selection_update();

    auto gameStyle = ImGui::GetStyle();
    ImGui::GetStyle() = ImGuiStyle{};
    ImGui::StyleColorsDark();
    ImGui::PushFont(resources.font.get(), resource::Font::SMALL);

    if (ImGui::Begin("Developer", &resources.isDeveloper, DEVELOPER_WINDOW_FLAGS))
    {
      auto isFocused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
      if (isFocused && (shortcut_is_pressed(ImGuiKey_GraveAccent) || shortcut_is_pressed(ImGuiKey_Escape)))
        resources.isDeveloper = false;

      if (ImGui::BeginTabBar("##Developer Tabs"))
      {
        auto isWasConsole = selectedTab == Tab::CONSOLE;
        if (ImGui::BeginTabItem("Console"))
        {
          selectedTab = Tab::CONSOLE;
          if (!isWasConsole) console.focus_request();
          console.update(resources, play);
          ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Editor"))
        {
          selectedTab = Tab::EDITOR;
          editor_update(resources);
          ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
      }
    }
    ImGui::End();

    if (gIsDocumentUndoMenuRequested) isContextUndoRequested = true;
    if (gIsDocumentRedoMenuRequested) isContextRedoRequested = true;
    action = console.action;
    if (isContextUndoRequested) undo(selectedDocument);
    if (isContextRedoRequested) redo(selectedDocument);

    toasts.update();

    ImGui::PopFont();
    ImGui::GetStyle() = gameStyle;
  }
}
