#include "editor.hpp"

#include <algorithm>
#include <cstdio>
#include <format>
#include <optional>
#include <sstream>
#include <string_view>
#include <vector>

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

#include "../util/imgui.hpp"
#include "../util/imgui/dialogue_text.hpp"
#include "../util/imgui/input_string.hpp"
#include "../util/imgui/path_widget.hpp"
#include "../util/imgui/selectable_renamable.hpp"
#include "../util/imgui/sound_widget.hpp"
#include "../util/imgui/widget.hpp"

namespace game::developer
{
  namespace editor_detail
  {
    using Schema = resource::xml::Schema;
    using Element = Schema::Element;
    using editor_schema::child_insert_after;
    using editor_schema::document_history_context_menu_draw;

#define DIALOGUE_EFFECT_TYPES(X)                                                                                       \
  X(ANIMATION, "Animation", false)                                                                                     \
  X(BIG, "Big", true)                                                                                                  \
  X(COLOR, "Color", true)                                                                                              \
  X(DELAY, "Delay", false)                                                                                             \
  X(OSCILLATION, "Oscillation", true)                                                                                  \
  X(SCREEN_SHAKE, "Screenshake", false)                                                                                \
  X(SHAKE, "Shake", true)                                                                                              \
  X(SMALL, "Small", true)                                                                                              \
  X(SOUND, "Sound", false)

    enum class DialogueEffectType
    {
#define DIALOGUE_EFFECT_TYPE_DECLARE(kind, label, ranged) kind,
      DIALOGUE_EFFECT_TYPES(DIALOGUE_EFFECT_TYPE_DECLARE)
#undef DIALOGUE_EFFECT_TYPE_DECLARE
          UNKNOWN,
    };

    struct DialogueEffectInfo
    {
      DialogueEffectType type{};
      std::string_view name{};
      bool isRanged{};
    };

    constexpr DialogueEffectInfo DIALOGUE_EFFECT_INFO[]{
#define DIALOGUE_EFFECT_INFO_DECLARE(kind, label, ranged)                                                              \
  {.type = DialogueEffectType::kind, .name = label, .isRanged = ranged},
        DIALOGUE_EFFECT_TYPES(DIALOGUE_EFFECT_INFO_DECLARE)
#undef DIALOGUE_EFFECT_INFO_DECLARE
    };

    constexpr auto CENTER_RATIO = 0.5f;
    constexpr auto CHOICE_BUTTON_ACTIVE_MULTIPLIER = 1.3f;
    constexpr auto CHOICE_BUTTON_COLOR_MAX = 0.9f;
    constexpr auto CHOICE_BUTTON_HOVER_MULTIPLIER = 1.6f;
    constexpr auto CHOICE_BUTTON_ROUNDING = 10.0f;
    constexpr auto CHOICE_BUTTON_STYLE_COLOR_COUNT = 3;
    constexpr auto CHOICE_BUTTON_STYLE_VAR_COUNT = 2;
    constexpr auto COLOR_COMPONENT_FULL = 1.0f;
    constexpr auto COLOR_EFFECT_DEFAULT_BLUE = 0.25f;
    constexpr auto COLOR_EFFECT_DEFAULT_GREEN = 0.25f;
    constexpr auto DELAY_MIN = 1;
    constexpr auto DIALOGUE_CHILD_TABLE_COLUMNS = 4;
    constexpr auto DIALOGUE_DELAY_DEFAULT = 4;
    constexpr auto EFFECT_FIELD_PAIR_COUNT = 2.0f;
    constexpr auto EFFECT_INDEX_FAST_STEP = 10;
    constexpr auto EFFECT_INDEX_STEP = 1;
    constexpr auto ENTRY_CONTENT_SPACING_COUNT = 4.0f;
    constexpr auto ENTRY_DIALOGUE_HEIGHT_RATIO = 0.15f;
    constexpr auto ENTRY_ENTITY_HEIGHT_RATIO = 0.47f;
    constexpr auto ENTRY_PLAYER_PADDING_COUNT = 2.0f;
    constexpr auto ENTRY_TEXT_EDITOR_HEIGHT_RATIO = 0.10f;
    constexpr auto LEFT_PANEL_MIN_WIDTH = 240.0f;
    constexpr auto LEFT_PANEL_WIDTH_RATIO = 0.40f;
    constexpr auto NAME_COLOR_DEFAULT = 0.498039f;
    constexpr auto NEXT_INDICATOR_HALF_RATIO = 0.35f;
    constexpr auto NEXT_INDICATOR_OFFSET_RATIO = 0.12f;
    constexpr auto NEXT_INDICATOR_SPEED = 2.0f;
    constexpr auto OSCILLATION_AMPLITUDE_DEFAULT = 2.0f;
    constexpr auto OSCILLATION_FREQUENCY_DEFAULT = 1.0f;
    constexpr auto OSCILLATION_PERIOD_DEFAULT = 1.0f;
    constexpr auto PREVIEW_RATE = 60.0f;
    constexpr auto PROPERTY_ITEM_MIN_WIDTH = 60.0f;
    constexpr auto SCREEN_SHAKE_PREVIEW_MAGNITUDE_DEFAULT = 0.03f;
    constexpr auto SCREEN_SHAKE_TIME_TICKS_DEFAULT = 15;
    constexpr auto SCROLL_ITEM_CENTER_Y = 0.5f;
    constexpr auto SELECTION_OVERLAY_ALPHA = 0.90f;
    constexpr auto SHAKE_AMPLITUDE_DEFAULT = 1.0f;
    constexpr auto SHAKE_FREQUENCY_DEFAULT = 30.0f;
    constexpr auto SHAKE_PERIOD_DEFAULT = 0.08f;
    constexpr auto SOUND_WIDGET_WIDTH = 48.0f;
    constexpr auto TAB_COLOR_DIM_MULTIPLIER = 0.55f;
    constexpr auto TAB_COLOR_HOVER_MULTIPLIER = 0.75f;
    constexpr auto TAB_STYLE_COLOR_COUNT = 3;
    constexpr auto TEXT_SCALE_BIG = 1.25f;
    constexpr auto TEXT_SCALE_SMALL = 0.75f;
    constexpr auto WIDGET_MIN_SIZE = 1.0f;

    const DialogueEffectInfo* dialogue_effect_info_find(std::string_view type)
    {
      for (auto& info : DIALOGUE_EFFECT_INFO)
        if (info.name == type) return &info;
      return nullptr;
    }

    DialogueEffectType dialogue_effect_type_get(std::string_view type)
    {
      auto* info = dialogue_effect_info_find(type);
      return info ? info->type : DialogueEffectType::UNKNOWN;
    }

    void scroll_to_item_if_requested(int& target, int index)
    {
      if (target != index) return;
      ImGui::SetScrollHereY(SCROLL_ITEM_CENTER_Y);
      target = -1;
    }

    bool dialogue_effect_is(const Element& element) { return element.type == Element::EFFECT; }

    bool dialogue_effect_type_is_ranged(std::string_view type)
    {
      auto* info = dialogue_effect_info_find(type);
      return !info || info->isRanged;
    }

    bool dialogue_effect_is_ranged(const Element& effect) { return dialogue_effect_type_is_ranged(effect.typeString); }

    void dialogue_effect_defaults_apply(Element& effect)
    {
      if (effect.typeString.empty()) effect.typeString = "Oscillation";
      switch (dialogue_effect_type_get(effect.typeString))
      {
        case DialogueEffectType::COLOR:
          if (effect.color == glm::vec3{})
            effect.color = glm::vec3{COLOR_COMPONENT_FULL, COLOR_EFFECT_DEFAULT_GREEN, COLOR_EFFECT_DEFAULT_BLUE};
          break;
        case DialogueEffectType::BIG:
        case DialogueEffectType::SMALL:
          break;
        case DialogueEffectType::OSCILLATION:
          break;
        case DialogueEffectType::SHAKE:
          break;
        case DialogueEffectType::SCREEN_SHAKE:
          break;
        case DialogueEffectType::DELAY:
          if (effect.delayTicks <= 0) effect.delayTicks = DIALOGUE_DELAY_DEFAULT;
          break;
        default:
          break;
      }
    }

    struct ReferenceInfo
    {
      const Schema* schema{};
      const std::filesystem::path* path{};
      std::string name{};
      std::string anm2{};
      std::string anm2RootPath{};
      std::string anm2Path{};
      std::string blip{};
      std::string soundRootPath{};
      std::string font{};
      std::string fontRootPath{};
      std::string fontPath{};
      glm::vec3 color{1.0f, 1.0f, 1.0f};
      bool isHasColor{};
      const resource::Audio* blipAudio{};
    };

    std::filesystem::path resource_absolute_path_get(const std::filesystem::path& schemaPath,
                                                     const std::string& rootPath, const std::string& path)
    {
      return util::imgui::path_absolute_get(schemaPath, rootPath, path);
    }

    std::string resource_display_path_get(const std::filesystem::path& schemaPath, const std::string& rootPath,
                                          const std::string& path)
    {
      return util::imgui::path_display_get(schemaPath, rootPath, path);
    }

    std::string resource_store_path_get(const std::filesystem::path& schemaPath, const std::string& rootPath,
                                        const std::filesystem::path& path)
    {
      return util::imgui::path_store_get(schemaPath, rootPath, path);
    }

    void sound_effect_entry_rebuild(const std::filesystem::path& schemaPath, const std::string& rootPath,
                                    Element& effect)
    {
      if (effect.sound.empty())
      {
        effect.soundEntry = {};
        return;
      }
      auto path = resource_absolute_path_get(schemaPath, rootPath, effect.sound);
      effect.soundEntry = resource::xml::Schema::SoundEntry{.path = effect.sound, .sound = resource::Audio(path)};
    }

    std::vector<int> elements_of_type_get(const Schema& schema, Element::Type type)
    {
      std::vector<int> result{};
      auto containerType = type == Element::ENTRY  ? Element::ENTRIES
                           : type == Element::POOL ? Element::POOLS
                                                   : Element::UNKNOWN;
      if (containerType != Element::UNKNOWN)
      {
        auto containerIt = schema.elementsByType.find(containerType);
        if (containerIt != schema.elementsByType.end() && !containerIt->second.empty())
        {
          auto i = containerIt->second.front();
          if (i < 0 || i >= (int)schema.elements.size()) return result;
          for (auto child : schema.elements[i].children)
            if (child >= 0 && child < (int)schema.elements.size() && schema.elements[child].type == type)
              result.emplace_back(child);
          return result;
        }
      }
      if (auto* root = schema.root())
      {
        for (auto child : root->children)
          if (child >= 0 && child < (int)schema.elements.size() && schema.elements[child].type == type)
            result.emplace_back(child);
        if (!result.empty()) return result;
      }
      auto typeIt = schema.elementsByType.find(type);
      if (typeIt != schema.elementsByType.end()) result = typeIt->second;
      return result;
    }

    std::string entry_label_get(const Element& entry)
    {
      if (!entry.idString.empty()) return entry.idString;
      if (!entry.name.empty()) return entry.name;
      return "<entry>";
    }

    std::string pool_label_get(const Element& pool)
    {
      if (!pool.idString.empty()) return pool.idString;
      if (!pool.name.empty()) return pool.name;
      return "<pool>";
    }

    const Editor::LoadedSchema* loaded_schema_get(std::span<const Editor::LoadedSchema> loadedSchemas,
                                                  Element::Type type)
    {
      for (auto& loaded : loadedSchemas)
      {
        if (!loaded.schema) continue;
        auto* root = loaded.schema->root();
        if (root && root->type == type) return &loaded;
      }
      return nullptr;
    }

    std::optional<ReferenceInfo> reference_info_get(const Editor::LoadedSchema* loaded)
    {
      if (!loaded || !loaded->schema) return std::nullopt;

      ReferenceInfo info{};
      info.schema = loaded->schema;
      info.path = loaded->path;
      for (auto& element : loaded->schema->elements)
      {
        if (info.anm2RootPath.empty())
        {
          if (!element.anm2RootPath.empty())
            info.anm2RootPath = element.anm2RootPath;
          else if (!element.textureRootPath.empty())
            info.anm2RootPath = element.textureRootPath;
        }
        if (info.soundRootPath.empty() && !element.soundRootPath.empty()) info.soundRootPath = element.soundRootPath;
        if (info.fontRootPath.empty() && !element.fontRootPath.empty()) info.fontRootPath = element.fontRootPath;
        if (info.font.empty() && !element.font.empty()) info.font = element.font;
        if (info.name.empty() && !element.name.empty()) info.name = element.name;
        if (!info.isHasColor && element.color != glm::vec3{})
        {
          info.color = element.color;
          info.isHasColor = true;
        }
        if (info.anm2.empty())
        {
          if (!element.anm2.empty())
            info.anm2 = element.anm2;
          else if (!element.baseAnm2.empty())
            info.anm2 = element.baseAnm2;
          if (!info.anm2.empty() && element.anm2Entry.is_valid()) info.anm2Path = element.anm2Entry.path;
        }
        if (info.blip.empty() && element.type == Element::BLIP)
        {
          if (!element.sound.empty())
            info.blip = element.sound;
          else if (!element.path.empty())
            info.blip = element.path;
          else if (element.soundEntry.is_valid())
            info.blip = element.soundEntry.path;
          if (element.soundEntry.is_valid()) info.blipAudio = &element.soundEntry.sound;
        }
      }

      if (auto* root = loaded->schema->root())
      {
        if (info.anm2RootPath.empty())
        {
          if (!root->anm2RootPath.empty())
            info.anm2RootPath = root->anm2RootPath;
          else if (!root->textureRootPath.empty())
            info.anm2RootPath = root->textureRootPath;
        }
        if (info.soundRootPath.empty()) info.soundRootPath = root->soundRootPath;
        if (info.fontRootPath.empty()) info.fontRootPath = root->fontRootPath;
        if (info.font.empty()) info.font = root->font;
        if (info.name.empty()) info.name = root->name;
        if (info.anm2.empty() && !root->anm2.empty())
        {
          info.anm2 = root->anm2;
          if (root->anm2Entry.is_valid()) info.anm2Path = root->anm2Entry.path;
        }
        if (info.blip.empty())
        {
          auto* textPlayback = loaded->schema->child_get(*root, Element::TEXT_PLAYBACK);
          auto* blipElement = textPlayback ? loaded->schema->child_get(*textPlayback, Element::BLIP) : nullptr;
          auto* sound = blipElement ? loaded->schema->child_get(*blipElement, Element::SOUND) : nullptr;
          if (sound && !sound->path.empty()) info.blip = sound->path;
        }
        if (info.blip.empty())
        {
          if (auto* blip = root->soundBlip.get())
          {
            info.blip = "<cached blip>";
            info.blipAudio = blip;
          }
        }
      }

      if (info.anm2Path.empty() && loaded->path && !info.anm2.empty())
        info.anm2Path = resource_absolute_path_get(*loaded->path, info.anm2RootPath, info.anm2).string();
      if (info.fontPath.empty() && loaded->path && !info.font.empty())
        info.fontPath = resource_absolute_path_get(*loaded->path, info.fontRootPath, info.font).string();
      return info;
    }

    int container_index_get(const Schema& schema, Element::Type type)
    {
      auto it = schema.elementsByType.find(type);
      if (it != schema.elementsByType.end() && !it->second.empty()) return it->second.front();
      return schema.root() ? 0 : -1;
    }

    int unique_element_id_get(const Schema& schema, std::string_view prefix)
    {
      auto id = 1;
      auto exists = [&](const std::string& candidate)
      { return std::ranges::any_of(schema.elements, [&](auto& element) { return element.idString == candidate; }); };
      while (exists(std::format("{}{}", prefix, id)))
        ++id;
      return id;
    }

    editor_schema::ElementActionOptions element_action_options_get(int parent, int target, Element::Type type,
                                                                 bool isInsertVisible = true,
                                                                 bool isClearVisible = false,
                                                                 bool isClearEnabled = false)
    {
      return {.parent = parent,
              .target = target,
              .after = target,
              .pasteParent = parent,
              .pasteAfter = target,
              .type = type,
              .isInsertVisible = isInsertVisible,
              .isInsertEnabled = isInsertVisible,
              .isClearVisible = isClearVisible,
              .isClearEnabled = isClearEnabled};
    }

    bool element_selected_has(int selected, const std::vector<int>& selectedElements, int index)
    {
      return selected == index || std::ranges::find(selectedElements, index) != selectedElements.end();
    }

    void element_selection_set(int& selected, std::vector<int>& selectedElements, int index)
    {
      selected = index;
      selectedElements = index >= 0 ? std::vector<int>{index} : std::vector<int>{};
    }

    void element_selection_toggle(int& selected, std::vector<int>& selectedElements, int index)
    {
      auto it = std::ranges::find(selectedElements, index);
      if (it == selectedElements.end() && selected == index)
        selected = selectedElements.empty() ? -1 : selectedElements.back();
      else if (it == selectedElements.end())
        selectedElements.emplace_back(index);
      else
        selectedElements.erase(it);
      selected = selectedElements.empty() ? -1 : selectedElements.back();
    }

    void element_selection_range_set(Schema& schema, int& selected, std::vector<int>& selectedElements, int index)
    {
      if (selected < 0 || selected >= (int)schema.elements.size() ||
          schema.elements[selected].parent != schema.elements[index].parent)
      {
        element_selection_set(selected, selectedElements, index);
        return;
      }

      auto& siblings = schema.elements[schema.elements[index].parent].children;
      auto begin = std::ranges::find(siblings, selected);
      auto end = std::ranges::find(siblings, index);
      if (begin == siblings.end() || end == siblings.end())
      {
        element_selection_set(selected, selectedElements, index);
        return;
      }

      selectedElements.clear();
      if (end < begin) std::swap(begin, end);
      for (auto it = begin; it <= end; ++it)
        selectedElements.emplace_back(*it);
      selected = index;
    }

    std::vector<int> selected_elements_get(Schema& schema, int selected, const std::vector<int>& selectedElements,
                                           Element::Type type)
    {
      std::vector<int> result{};
      auto add = [&](int index)
      {
        if (index <= 0 || index >= (int)schema.elements.size()) return;
        if (schema.elements[index].type != type) return;
        if (std::ranges::find(result, index) == result.end()) result.emplace_back(index);
      };
      for (auto index : selectedElements)
        add(index);
      add(selected);
      if (result.empty()) return result;

      auto parent = schema.elements[result.front()].parent;
      if (parent >= 0 && parent < (int)schema.elements.size())
      {
        std::vector<int> sorted{};
        for (auto child : schema.elements[parent].children)
          if (std::ranges::find(result, child) != result.end()) sorted.emplace_back(child);
        return sorted;
      }

      std::ranges::sort(result);
      return result;
    }

    std::string xml_element_text_get(tinyxml2::XMLElement* element)
    {
      tinyxml2::XMLPrinter printer{};
      element->Accept(&printer);
      return printer.CStr();
    }

    std::string selected_elements_serialize(Schema& schema, int selected, const std::vector<int>& selectedElements,
                                            Element::Type type)
    {
      auto elements = selected_elements_get(schema, selected, selectedElements, type);
      if (elements.empty()) return {};
      if (elements.size() == 1) return schema.element_serialize(elements.front());

      std::ostringstream stream{};
      stream << "<EditorElements>";
      for (auto index : elements)
        stream << schema.element_serialize(index);
      stream << "</EditorElements>";
      return stream.str();
    }

    bool selected_elements_erase(Schema& schema, int& selected, std::vector<int>& selectedElements, Element::Type type)
    {
      auto elements = selected_elements_get(schema, selected, selectedElements, type);
      if (elements.empty()) return false;

      std::ranges::sort(elements, std::greater{});
      auto isDirty = false;
      for (auto index : elements)
        if (schema.element_erase(index)) isDirty = true;
      selected = -1;
      selectedElements.clear();
      return isDirty;
    }

    bool selected_elements_cut(util::Clipboard& clipboard, Schema& schema, int& selected,
                               std::vector<int>& selectedElements, Element::Type type)
    {
      auto text = selected_elements_serialize(schema, selected, selectedElements, type);
      if (text.empty()) return false;
      clipboard.cut(text);
      return selected_elements_erase(schema, selected, selectedElements, type);
    }

    bool selected_elements_copy(util::Clipboard& clipboard, Schema& schema, int selected,
                                const std::vector<int>& selectedElements, Element::Type type)
    {
      auto text = selected_elements_serialize(schema, selected, selectedElements, type);
      if (text.empty()) return false;
      clipboard.copy(text);
      return true;
    }

    int elements_paste(util::Clipboard& clipboard, Schema& schema, int parent, int beforeSibling)
    {
      auto* text = clipboard.get();
      if (!text || !*text) return -1;

      tinyxml2::XMLDocument document{};
      if (document.Parse(text) == tinyxml2::XML_SUCCESS && document.RootElement() &&
          std::string_view{document.RootElement()->Name()} == "EditorElements")
      {
        auto selected = -1;
        for (auto* child = document.RootElement()->FirstChildElement(); child; child = child->NextSiblingElement())
        {
          auto inserted = schema.element_insert_xml(xml_element_text_get(child), parent, beforeSibling);
          if (inserted >= 0) selected = inserted;
        }
        return selected;
      }

      return editor_schema::element_paste(clipboard, schema, parent, beforeSibling);
    }

    bool selected_elements_duplicate(Schema& schema, int& selected, std::vector<int>& selectedElements,
                                     Element::Type type)
    {
      auto elements = selected_elements_get(schema, selected, selectedElements, type);
      if (elements.empty()) return false;

      auto last = -1;
      auto after = elements.back();
      selectedElements.clear();
      for (auto index : elements)
      {
        auto duplicated = editor_schema::element_duplicate(schema, schema.elements[index].parent, index, after, type);
        if (duplicated < 0) continue;
        selectedElements.emplace_back(duplicated);
        selected = duplicated;
        after = duplicated;
        last = duplicated;
      }
      return last >= 0;
    }

    bool elements_move(Schema& schema, const std::vector<int>& selectedElements, int parent, int beforeSibling)
    {
      if (selectedElements.empty()) return false;
      auto isDirty = false;
      for (auto index : selectedElements)
      {
        if (index == beforeSibling) continue;
        if (schema.element_move(index, parent, beforeSibling)) isDirty = true;
      }
      return isDirty;
    }

    int sibling_after_get(Schema& schema, int parent, int sibling)
    {
      if (parent < 0 || parent >= (int)schema.elements.size()) return -1;
      auto& siblings = schema.elements[parent].children;
      auto it = std::ranges::find(siblings, sibling);
      if (it == siblings.end() || it + 1 == siblings.end()) return -1;
      return *(it + 1);
    }

    bool sibling_drag_drop_draw(Schema& schema, int index, const char* payloadName, const char* label,
                                const std::vector<int>* selectedElements = nullptr)
    {
      auto isDirty = false;
      if (ImGui::BeginDragDropSource())
      {
        ImGui::SetDragDropPayload(payloadName, &index, sizeof(index));
        ImGui::TextUnformatted(label);
        ImGui::EndDragDropSource();
      }
      if (ImGui::BeginDragDropTarget())
      {
        if (auto* payload = ImGui::AcceptDragDropPayload(payloadName))
        {
          auto source = payload && payload->Data ? *(const int*)payload->Data : -1;
          if (source >= 0 && source < (int)schema.elements.size() && source != index)
          {
            auto parent = schema.elements[index].parent;
            auto beforeSibling = index;
            if (parent >= 0 && parent < (int)schema.elements.size() && schema.elements[source].parent == parent)
            {
              auto& siblings = schema.elements[parent].children;
              auto sourceIt = std::ranges::find(siblings, source);
              auto targetIt = std::ranges::find(siblings, index);
              if (sourceIt != siblings.end() && targetIt != siblings.end() && sourceIt < targetIt)
              {
                auto afterTarget = targetIt + 1;
                beforeSibling = afterTarget == siblings.end() ? -1 : *afterTarget;
              }
            }
            if (selectedElements && std::ranges::find(*selectedElements, source) != selectedElements->end())
              isDirty = elements_move(schema, *selectedElements, parent, beforeSibling);
            else if (schema.element_move(source, parent, beforeSibling))
              isDirty = true;
          }
        }
        ImGui::EndDragDropTarget();
      }
      return isDirty;
    }

    int entry_insert(Schema& schema, int after = -1)
    {
      auto parent = container_index_get(schema, Element::ENTRIES);
      auto& entry = schema.element_add(Element::ENTRY, parent);
      entry.idString = std::format("New{}", unique_element_id_get(schema, "New"));
      auto index = (int)schema.elements.size() - 1;
      child_insert_after(schema, parent, index, after);
      schema.indices_refresh();
      return index;
    }

    std::vector<int> dialogue_effect_indices_get(const Schema& schema, const Element& entry)
    {
      std::vector<int> result{};
      for (auto child : entry.children)
      {
        if (child < 0 || child >= (int)schema.elements.size()) continue;
        if (dialogue_effect_is(schema.elements[child])) result.emplace_back(child);
      }
      return result;
    }

    std::vector<int> dialogue_choice_indices_get(const Schema& schema, const Element& entry)
    {
      std::vector<int> result{};
      for (auto child : entry.children)
      {
        if (child < 0 || child >= (int)schema.elements.size()) continue;
        if (schema.elements[child].type == Element::CHOICE) result.emplace_back(child);
      }
      return result;
    }

    std::vector<int> dialogue_child_indices_get(const Schema& schema, const Element& entry)
    {
      std::vector<int> result{};
      for (auto child : entry.children)
      {
        if (child < 0 || child >= (int)schema.elements.size()) continue;
        if (schema.elements[child].type == Element::CHOICE || dialogue_effect_is(schema.elements[child]))
          result.emplace_back(child);
      }
      return result;
    }

    Element::Type dialogue_child_type_get(const Schema& schema, int index)
    {
      if (index < 0 || index >= (int)schema.elements.size()) return Element::UNKNOWN;
      if (schema.elements[index].type == Element::CHOICE) return Element::CHOICE;
      if (dialogue_effect_is(schema.elements[index])) return Element::EFFECT;
      return Element::UNKNOWN;
    }

    int dialogue_child_selected_get(const Editor& editor)
    {
      if (editor.selectedChoice >= 0) return editor.selectedChoice;
      if (editor.selectedEffect >= 0) return editor.selectedEffect;
      return -1;
    }

    void dialogue_child_select(Editor& editor, const Schema& schema, int index)
    {
      auto type = dialogue_child_type_get(schema, index);
      editor.selectedChoice = type == Element::CHOICE ? index : -1;
      editor.selectedEffect = type == Element::EFFECT ? index : -1;
      if (type == Element::CHOICE) editor.scrollToChoice = index;
      if (type == Element::EFFECT) editor.scrollToEffect = index;
    }

    void dialogue_child_clear(Editor& editor)
    {
      editor.selectedChoice = -1;
      editor.selectedEffect = -1;
    }

    bool dialogue_child_type_set(Editor& editor, Schema& schema, int entryIndex, int index,
                                 std::string_view type)
    {
      if (index < 0 || index >= (int)schema.elements.size()) return false;
      auto& child = schema.elements[index];
      if (type == "Choice")
      {
        if (child.type == Element::CHOICE) return false;
        child.type = Element::CHOICE;
        if (child.text.empty()) child.text = "Choice";
        schema.indices_refresh();
        dialogue_child_select(editor, schema, index);
        return true;
      }

      auto isNewEffect = child.type != Element::EFFECT;
      if (!isNewEffect && child.typeString == type) return false;
      child.type = Element::EFFECT;
      child.typeString = type;
      if (isNewEffect && entryIndex >= 0 && entryIndex < (int)schema.elements.size())
      {
        auto textLength = util::imgui::utf8_char_count(schema.elements[entryIndex].text);
        child.start = std::clamp(editor.textSelectionStart, 0, textLength);
        child.end = std::clamp(editor.textSelectionEnd, 0, textLength);
        if (child.end < child.start) std::swap(child.start, child.end);
        if (dialogue_effect_type_is_ranged(type) && child.start == child.end)
          child.end = std::min(textLength, child.start + 1);
        if (!dialogue_effect_type_is_ranged(type)) child.end = child.start;
      }
      dialogue_effect_defaults_apply(child);
      schema.indices_refresh();
      dialogue_child_select(editor, schema, index);
      return true;
    }

    std::vector<util::imgui::DialogueEffectSpan> dialogue_effect_spans_get(const Schema& schema, const Element& entry)
    {
      std::vector<util::imgui::DialogueEffectSpan> result{};
      for (auto index : dialogue_effect_indices_get(schema, entry))
      {
        auto& effect = schema.elements[index];
        switch (dialogue_effect_type_get(effect.typeString))
        {
          case DialogueEffectType::COLOR:
            result.push_back({.type = util::imgui::DialogueEffectSpan::COLOR,
                              .start = effect.start,
                              .end = effect.end,
                              .color = effect.color});
            break;
          case DialogueEffectType::BIG:
            result.push_back({.type = util::imgui::DialogueEffectSpan::TEXT_SCALE,
                              .start = effect.start,
                              .end = effect.end,
                              .textScale = TEXT_SCALE_BIG});
            break;
          case DialogueEffectType::SMALL:
            result.push_back({.type = util::imgui::DialogueEffectSpan::TEXT_SCALE,
                              .start = effect.start,
                              .end = effect.end,
                              .textScale = TEXT_SCALE_SMALL});
            break;
          case DialogueEffectType::OSCILLATION:
            result.push_back(
                {.type = util::imgui::DialogueEffectSpan::OSCILLATE,
                 .start = effect.start,
                 .end = effect.end,
                 .frequency = OSCILLATION_FREQUENCY_DEFAULT,
                 .amplitude = OSCILLATION_AMPLITUDE_DEFAULT});
            break;
          case DialogueEffectType::SHAKE:
            result.push_back({.type = util::imgui::DialogueEffectSpan::SHAKE,
                              .start = effect.start,
                              .end = effect.end,
                              .frequency = SHAKE_FREQUENCY_DEFAULT,
                              .amplitude = SHAKE_AMPLITUDE_DEFAULT});
            break;
          default:
            break;
        }
      }
      return result;
    }

    int dialogue_effect_insert(Schema& schema, int entryIndex, int after, std::string_view type, int start, int end)
    {
      if (entryIndex < 0 || entryIndex >= (int)schema.elements.size()) return -1;
      auto textLength = util::imgui::utf8_char_count(schema.elements[entryIndex].text);
      start = std::clamp(start, 0, textLength);
      end = std::clamp(end, 0, textLength);
      if (end < start) std::swap(start, end);
      auto isRanged = dialogue_effect_type_is_ranged(type);
      if (isRanged && start == end) end = std::min(textLength, start + 1);
      if (!isRanged) end = start;

      auto& effect = schema.element_add(Element::EFFECT, entryIndex);
      effect.typeString = type;
      effect.start = start;
      effect.end = end;
      dialogue_effect_defaults_apply(effect);
      auto index = (int)schema.elements.size() - 1;
      child_insert_after(schema, entryIndex, index, after);
      schema.indices_refresh();
      return index;
    }

    void dialogue_preview_start_effects_apply(Editor& editor, const Schema& schema, const Element& entry,
                                              int index)
    {
      for (auto effectIndex : dialogue_effect_indices_get(schema, entry))
      {
        auto& effect = schema.elements[effectIndex];
        if (effect.start != index) continue;
        switch (dialogue_effect_type_get(effect.typeString))
        {
          case DialogueEffectType::DELAY:
            editor.previewBaseDelay = std::max(DELAY_MIN, effect.delayTicks);
            editor.previewDelay = editor.previewBaseDelay;
            break;
          case DialogueEffectType::ANIMATION:
            editor.previewAnimationRequest = effect.animation;
            editor.previewNeedsAnimationPlay = true;
            break;
          case DialogueEffectType::SCREEN_SHAKE:
          {
            editor.previewCanvas.canvas.shake(SCREEN_SHAKE_PREVIEW_MAGNITUDE_DEFAULT, SCREEN_SHAKE_TIME_TICKS_DEFAULT);
            break;
          }
          case DialogueEffectType::SOUND:
            if (effect.soundEntry.is_valid()) effect.soundEntry.sound.play();
            break;
          default:
            break;
        }
      }
    }

    std::string_view dialogue_char_get(std::string_view text, int index)
    {
      auto* start = text.data();
      auto* end = start + text.size();
      auto* charStart = util::imgui::utf8_advance_chars(start, end, index);
      auto* charEnd = util::imgui::utf8_advance_chars(charStart, end, 1);
      return charEnd > charStart ? std::string_view(charStart, charEnd - charStart) : std::string_view{};
    }

    struct DialogueDelayCharacter
    {
      int delay{};
      bool isDisableTalk{};
    };

    DialogueDelayCharacter dialogue_delay_character_get(const Schema& schema, std::string_view text, int index,
                                                        int fallback)
    {
      auto value = dialogue_char_get(text, index);
      auto* root = schema.root();
      if (value.empty() || !root) return {.delay = fallback};
      auto* textPlayback = schema.child_get(*root, Element::TEXT_PLAYBACK);
      auto* delayCharacters = textPlayback ? schema.child_get(*textPlayback, Element::DELAY_CHARACTERS) : nullptr;
      if (!delayCharacters) return {.delay = fallback};

      for (auto* delayCharacter : schema.children_get(*delayCharacters, Element::DELAY_CHARACTER))
      {
        if (delayCharacter->character == value)
          return {.delay = fallback + std::max(0, delayCharacter->delayTicks),
                  .isDisableTalk = delayCharacter->isDisableTalk};
      }
      return {.delay = fallback};
    }

    void dialogue_preview_entry_set(Editor& editor, Schema& schema, int entryIndex, bool isPlay)
    {
      if (entryIndex < 0 || entryIndex >= (int)schema.elements.size()) return;
      auto* entry = &schema.elements[entryIndex];
      if (entry->type != Element::ENTRY) return;
      editor.mode = Editor::ENTRY;
      editor.selectedEntry = entryIndex;
      editor.selectedPoolEntry = -1;
      editor.selectedChoice = -1;
      editor.selectedEffect = -1;
      editor.textSelectionStart = 0;
      editor.textSelectionEnd = 0;
      editor.textSelectionStartByte = 0;
      editor.textSelectionEndByte = 0;
      editor.previewIndex = 0;
      editor.previewTimer = 0.0f;
      editor.previewBaseDelay = std::max(DELAY_MIN, editor.delay);
      editor.previewDelay = editor.previewBaseDelay;
      editor.isPreviewBlipQueued = false;
      editor.isPlaying = isPlay && util::imgui::utf8_char_count(entry->text) > 0;
      editor.previewNeedsAnimationPlay = true;
      if (editor.isPlaying) dialogue_preview_start_effects_apply(editor, schema, *entry, editor.previewIndex);
    }

    void dialogue_preview_skipped_effects_apply(Editor& editor, const Schema& schema, const Element& entry,
                                                int fromExclusive, int toInclusive)
    {
      if (toInclusive <= fromExclusive) return;
      for (int index = fromExclusive + 1; index <= toInclusive; ++index)
        dialogue_preview_start_effects_apply(editor, schema, entry, index);
    }

    int dialogue_choice_insert(Schema& schema, int entryIndex, int after = -1)
    {
      if (entryIndex < 0 || entryIndex >= (int)schema.elements.size()) return -1;
      auto& choice = schema.element_add(Element::CHOICE, entryIndex);
      choice.text = "Choice";
      auto index = (int)schema.elements.size() - 1;
      child_insert_after(schema, entryIndex, index, after);
      schema.indices_refresh();
      return index;
    }

    bool dialogue_child_insert_menu_draw(Editor& editor, Schema& schema, int entryIndex, int after)
    {
      if (!ImGui::BeginMenu("Insert")) return false;

      auto isDirty = false;
      if (ImGui::MenuItem("Choice"))
      {
        editor.selectedChoice = dialogue_choice_insert(schema, entryIndex, after);
        if (editor.selectedChoice >= 0)
        {
          editor.selectedEffect = -1;
          editor.scrollToChoice = editor.selectedChoice;
          isDirty = true;
        }
      }
#define DIALOGUE_EFFECT_MENU_DRAW(kind, label, ranged)                                                                 \
  if (ImGui::MenuItem(label))                                                                                          \
  {                                                                                                                    \
    auto end = ranged ? editor.textSelectionEnd : editor.textSelectionStart;                                           \
    editor.selectedEffect = dialogue_effect_insert(schema, entryIndex, after, label, editor.textSelectionStart, end);  \
    if (editor.selectedEffect >= 0) editor.selectedChoice = -1;                                                        \
    if (editor.selectedEffect >= 0) editor.scrollToEffect = editor.selectedEffect;                                     \
    isDirty = editor.selectedEffect >= 0;                                                                              \
  }
      DIALOGUE_EFFECT_TYPES(DIALOGUE_EFFECT_MENU_DRAW)
#undef DIALOGUE_EFFECT_MENU_DRAW

      ImGui::EndMenu();
      return isDirty;
    }

    bool dialogue_child_type_combo_draw(Editor& editor, Schema& schema, int entryIndex, int index)
    {
      if (index < 0 || index >= (int)schema.elements.size()) return false;
      auto& child = schema.elements[index];
      auto preview = child.type == Element::CHOICE ? "Choice" : child.typeString.c_str();
      auto isDirty = false;
      ImGui::SetNextItemWidth(-FLT_MIN);
      if (ImGui::BeginCombo("##Type", preview))
      {
        if (ImGui::Selectable("Choice", child.type == Element::CHOICE))
          isDirty = dialogue_child_type_set(editor, schema, entryIndex, index, "Choice") || isDirty;
#define DIALOGUE_EFFECT_TYPE_SELECT_DRAW(kind, label, ranged)                                                          \
  if (ImGui::Selectable(label, child.type == Element::EFFECT && child.typeString == label))                            \
    isDirty = dialogue_child_type_set(editor, schema, entryIndex, index, label) || isDirty;
        DIALOGUE_EFFECT_TYPES(DIALOGUE_EFFECT_TYPE_SELECT_DRAW)
#undef DIALOGUE_EFFECT_TYPE_SELECT_DRAW
        ImGui::EndCombo();
      }
      return isDirty;
    }

    void text_selection_overlay_draw(std::string_view text, int selectionStart, int selectionEnd, const ImVec2& rectMin,
                                     const ImVec2& rectMax, const ImVec2& scroll)
    {
      if (selectionStart == selectionEnd) return;
      if (selectionEnd < selectionStart) std::swap(selectionStart, selectionEnd);
      selectionStart = std::clamp(selectionStart, 0, (int)text.size());
      selectionEnd = std::clamp(selectionEnd, 0, (int)text.size());
      if (selectionStart == selectionEnd) return;

      auto* drawList = ImGui::GetWindowDrawList();
      auto& style = ImGui::GetStyle();
      auto lineHeight = ImGui::GetTextLineHeight();
      auto origin = ImVec2(rectMin.x + style.FramePadding.x - scroll.x, rectMin.y + style.FramePadding.y - scroll.y);
      auto color = ImGui::GetColorU32(ImGuiCol_TextSelectedBg, SELECTION_OVERLAY_ALPHA);

      drawList->PushClipRect(rectMin, rectMax, true);
      auto lineStartByte = 0;
      auto lineIndex = 0;
      while (lineStartByte <= (int)text.size())
      {
        auto lineEndByte = lineStartByte;
        while (lineEndByte < (int)text.size() && text[lineEndByte] != '\n')
          ++lineEndByte;

        auto selectedStart = std::max(selectionStart, lineStartByte);
        auto selectedEnd = std::min(selectionEnd, lineEndByte);
        if (selectedStart < selectedEnd)
        {
          auto* lineStart = text.data() + lineStartByte;
          auto* selectedStartPtr = text.data() + selectedStart;
          auto* selectedEndPtr = text.data() + selectedEnd;
          auto x0 = origin.x + ImGui::CalcTextSize(lineStart, selectedStartPtr).x;
          auto x1 = origin.x + ImGui::CalcTextSize(lineStart, selectedEndPtr).x;
          auto y0 = origin.y + lineHeight * lineIndex;
          drawList->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y0 + lineHeight), color);
        }

        if (lineEndByte >= (int)text.size()) break;
        lineStartByte = lineEndByte + 1;
        ++lineIndex;
      }
      drawList->PopClipRect();
    }

    int pool_insert(Schema& schema, int after = -1)
    {
      auto parent = container_index_get(schema, Element::POOLS);
      auto& pool = schema.element_add(Element::POOL, parent);
      pool.idString = std::format("NewPool{}", unique_element_id_get(schema, "NewPool"));
      auto index = (int)schema.elements.size() - 1;
      child_insert_after(schema, parent, index, after);
      schema.indices_refresh();
      return index;
    }

    void entry_tooltip_draw(const Schema& schema, const Element& entry)
    {
      if (!ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal)) return;
      auto choiceCount = (int)dialogue_choice_indices_get(schema, entry).size();
      auto effectCount = (int)dialogue_effect_indices_get(schema, entry).size();
      auto isAnimationVisible = !entry.animation.empty();
      auto isCountVisible = choiceCount > 0 || effectCount > 0;
      auto isSummaryVisible = isAnimationVisible || isCountVisible;
      ImGui::BeginTooltip();
      ImGui::TextUnformatted(entry_label_get(entry).c_str());
      if (isSummaryVisible)
      {
        ImGui::Separator();
        if (isAnimationVisible) ImGui::Text("Animation: %s", entry.animation.c_str());
        if (choiceCount > 0 && effectCount > 0)
          ImGui::Text("%d %s, %d %s", choiceCount, choiceCount == 1 ? "choice" : "choices", effectCount,
                      effectCount == 1 ? "effect" : "effects");
        else if (choiceCount > 0)
          ImGui::Text("%d %s", choiceCount, choiceCount == 1 ? "choice" : "choices");
        else if (effectCount > 0)
          ImGui::Text("%d %s", effectCount, effectCount == 1 ? "effect" : "effects");
      }
      if (!entry.text.empty())
      {
        if (isSummaryVisible) ImGui::Separator();
        ImGui::TextUnformatted(entry.text.c_str());
      }
      ImGui::EndTooltip();
    }

    Element* selected_get(Schema& schema, int index)
    {
      return index >= 0 && index < (int)schema.elements.size() ? &schema.elements[index] : nullptr;
    }

    int element_index_get(const Schema& schema, const Element* element)
    {
      if (!element) return -1;
      for (int i = 0; i < (int)schema.elements.size(); ++i)
        if (&schema.elements[i] == element) return i;
      return -1;
    }

    bool element_is_type(const Schema& schema, int index, Element::Type type)
    {
      return index >= 0 && index < (int)schema.elements.size() && schema.elements[index].type == type;
    }

    void indices_refresh(Schema& schema) { schema.indices_refresh(); }

    bool entry_combo_draw(const char* label, Schema& schema, std::string& value)
    {
      auto isDirty = false;
      auto entries = elements_of_type_get(schema, Element::ENTRY);
      auto preview = value.empty() ? "<none>" : value.c_str();
      if (ImGui::BeginCombo(label, preview))
      {
        if (ImGui::Selectable("<none>", value.empty()))
        {
          value.clear();
          isDirty = true;
        }
        for (auto index : entries)
        {
          auto& entry = schema.elements[index];
          auto entryLabel = entry_label_get(entry);
          auto isSelected = value == entry.idString;
          if (ImGui::Selectable(entryLabel.c_str(), isSelected))
          {
            value = entry.idString;
            isDirty = true;
          }
          entry_tooltip_draw(schema, entry);
        }
        ImGui::EndCombo();
      }
      return isDirty;
    }

    bool pool_entry_delete(Schema& schema, int poolIndex, int poolEntryIndex)
    {
      if (poolIndex < 0 || poolIndex >= (int)schema.elements.size()) return false;
      auto& children = schema.elements[poolIndex].children;
      auto it = std::ranges::find(children, poolEntryIndex);
      if (it == children.end()) return false;
      children.erase(it);
      return schema.element_erase(poolEntryIndex);
    }

    int pool_entry_insert(Schema& schema, int poolIndex, const std::string& entryID, int after = -1)
    {
      if (poolIndex < 0 || poolIndex >= (int)schema.elements.size()) return -1;
      auto& poolEntry = schema.element_add(Element::POOL_ENTRY, poolIndex);
      poolEntry.idString = entryID;
      auto index = (int)schema.elements.size() - 1;
      child_insert_after(schema, poolIndex, index, after);
      schema.indices_refresh();
      return index;
    }

    bool pool_entry_insert_menu_draw(Schema& schema, int poolIndex, int after, const std::vector<int>& entries,
                                     int& selectedPoolEntry, int& scrollToPoolEntry)
    {
      if (!ImGui::BeginMenu("Insert")) return false;

      auto isDirty = false;
      for (auto entryIndex : entries)
      {
        auto& entry = schema.elements[entryIndex];
        auto label = entry_label_get(entry);
        if (ImGui::MenuItem(label.c_str()))
        {
          selectedPoolEntry = pool_entry_insert(schema, poolIndex, entry.idString, after);
          scrollToPoolEntry = selectedPoolEntry;
          isDirty = selectedPoolEntry >= 0;
        }
        entry_tooltip_draw(schema, entry);
      }
      ImGui::EndMenu();
      return isDirty;
    }

    void entity_preview_update(Editor& editor, Resources& resources, const Schema& schema,
                               const ReferenceInfo* reference, Element& entry, ImVec2 size)
    {
      auto anm2Path = reference ? reference->anm2Path : std::string{};
      if (editor.previewAnm2Path != anm2Path)
      {
        editor.previewAnm2Path = anm2Path;
        if (reference && reference->schema)
        {
          editor.previewCharacter = {};
          editor.previewCharacter.schema = *reference->schema;
          editor.previewEntity = Entity(editor.previewCharacter, {});
        }
        else
        {
          editor.previewCharacter = {};
          editor.previewEntity = {};
        }
        editor.previewCanvas.clear();
        editor.previewAnimation.clear();
        editor.previewAnimationRequest.clear();
        editor.previewNeedsAnimationPlay = editor.isPlaying;
      }

      if (!editor.previewEntity.isValid) return;

      auto animation = editor.previewAnimationRequest.empty() ? entry.animation : editor.previewAnimationRequest;
      if (editor.previewNeedsAnimationPlay)
      {
        editor.previewAnimation = animation;
        editor.previewAnimationRequest.clear();
        editor.previewCanvas.clear();
        if (!animation.empty())
        {
          editor.previewEntity.play(
              {.animation = animation, .appendID = editor.animationAppendID, .mode = Entity::PLAY_FORCE});
        }
        editor.previewNeedsAnimationPlay = false;
      }
      auto delayCharacter =
          dialogue_delay_character_get(schema, entry.text, editor.previewIndex - 1, editor.previewDelay);
      editor.previewEntity.isTalking = editor.isPlaying &&
                                       editor.previewIndex < util::imgui::utf8_char_count(entry.text) &&
                                       !delayCharacter.isDisableTalk;
      editor.previewEntity.update();

      auto& canvas = editor.previewCanvas.render(resources, editor.previewEntity, size);
      canvas.update();
      auto imageSize = editor.previewCanvas.imageSize;
      auto cursor = ImGui::GetCursorPos();
      ImGui::SetCursorPos(ImVec2(cursor.x + (size.x * CENTER_RATIO) - (imageSize.x * CENTER_RATIO),
                                 cursor.y + (size.y * CENTER_RATIO) - (imageSize.y * CENTER_RATIO)));
      auto imageCursor = ImGui::GetCursorPos();
      auto shakeOffset = editor.previewCanvas.shake_offset_get();
      ImGui::SetCursorPos(ImVec2(imageCursor.x + shakeOffset.x, imageCursor.y + shakeOffset.y));
      ImGui::Image(canvas.texture, imageSize);
    }
#undef DIALOGUE_EFFECT_TYPES
  }

  using namespace editor_detail;

  bool Editor::update(Resources& resources, Schema& schema, const std::filesystem::path& schemaPath,
                              util::Dialog& dialog, std::span<const LoadedSchema> loadedSchemas)
  {
    auto isDirty = false;
    auto* root = schema.root();
    if (!root)
    {
      ImGui::TextUnformatted("No schema root.");
      return false;
    }

    if (pendingSetting == PENDING_SOUND && dialog.is_selected(util::Dialog::SCHEMA_SOUND_OPEN))
    {
      if (pendingSoundEffect >= 0 && pendingSoundEffect < (int)schema.elements.size())
      {
        auto& effect = schema.elements[pendingSoundEffect];
        effect.sound = resource_store_path_get(schemaPath, root->soundRootPath, dialog.path);
        sound_effect_entry_rebuild(schemaPath, root->soundRootPath, effect);
        isDirty = true;
      }
      pendingSoundEffect = -1;
      pendingSetting = PENDING_NONE;
      dialog.reset();
    }

    auto referenceInfo = reference_info_get(loaded_schema_get(loadedSchemas, Element::CHARACTER));
    auto* reference = referenceInfo ? &*referenceInfo : nullptr;
    auto menuInfo = reference_info_get(loaded_schema_get(loadedSchemas, Element::MENU));
    previewBlip = root->soundBlip.get();
    if (!previewBlip && reference && reference->blipAudio) previewBlip = reference->blipAudio;
    nameColor = reference && reference->isHasColor ? reference->color : glm::vec3{NAME_COLOR_DEFAULT};

    if (menuInfo && menuInfo->path && !menuInfo->font.empty() &&
        (fontPath != menuInfo->font || fontRootPath != menuInfo->fontRootPath || fontSchemaPath != *menuInfo->path))
    {
      fontPath = menuInfo->font;
      fontRootPath = menuInfo->fontRootPath;
      fontSchemaPath = *menuInfo->path;
      previewFont = resource::Font(resource_absolute_path_get(fontSchemaPath, fontRootPath, fontPath));
    }
    else if ((!menuInfo || !menuInfo->path || menuInfo->font.empty()) &&
             (!fontPath.empty() || !fontRootPath.empty() || !fontSchemaPath.empty()))
    {
      fontPath.clear();
      fontRootPath.clear();
      fontSchemaPath.clear();
      previewFont = {};
    }

    auto entries = elements_of_type_get(schema, Element::ENTRY);
    auto pools = elements_of_type_get(schema, Element::POOL);
    if (!element_is_type(schema, selectedEntry, Element::ENTRY)) selectedEntry = -1;
    if (!element_is_type(schema, selectedPool, Element::POOL)) selectedPool = -1;
    selectedEntries.erase(std::remove_if(selectedEntries.begin(), selectedEntries.end(),
                                         [&](int index) { return !element_is_type(schema, index, Element::ENTRY); }),
                          selectedEntries.end());
    selectedPools.erase(std::remove_if(selectedPools.begin(), selectedPools.end(),
                                       [&](int index) { return !element_is_type(schema, index, Element::POOL); }),
                        selectedPools.end());
    if (!element_is_type(schema, selectedPoolEntry, Element::POOL_ENTRY)) selectedPoolEntry = -1;
    if (!element_is_type(schema, selectedChoice, Element::CHOICE)) selectedChoice = -1;
    if (selectedEffect < 0 || selectedEffect >= (int)schema.elements.size() ||
        !dialogue_effect_is(schema.elements[selectedEffect]))
      selectedEffect = -1;
    if (selectedEntry < 0 && !entries.empty()) selectedEntry = entries.front();
    if (selectedPool < 0 && !pools.empty()) selectedPool = pools.front();
    delay = root->delayTicks > 0 ? std::max(DELAY_MIN, root->delayTicks) : std::max(DELAY_MIN, delay);
    if (root->delayTicks <= 0) root->delayTicks = delay;
    blipDelay = root->blipDelayTicks > 0 ? std::max(DELAY_MIN, root->blipDelayTicks) : std::max(DELAY_MIN, blipDelay);
    if (root->blipDelayTicks <= 0) root->blipDelayTicks = blipDelay;

    auto available = ImGui::GetContentRegionAvail();
    auto leftWidth = std::max(LEFT_PANEL_MIN_WIDTH, available.x * LEFT_PANEL_WIDTH_RATIO);
    auto rightWidth = std::max(WIDGET_MIN_SIZE, available.x - leftWidth - ImGui::GetStyle().ItemSpacing.x);

    ImGui::BeginChild("dialogue-left", ImVec2(leftWidth, 0.0f));
    auto settingsHeight = ImGui::GetFrameHeightWithSpacing();

    if (ImGui::BeginChild("dialogue-settings", ImVec2(0.0f, settingsHeight), ImGuiChildFlags_None,
                          ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
    {
      util::imgui::input_string("AppendStringForAnimations", animationAppendID);
    }
    ImGui::EndChild();

    if (ImGui::BeginChild("dialogue-lists", ImVec2(0.0f, 0.0f)))
    {
      if (ImGui::BeginTabBar("dialogue-lists-tabs"))
      {
        if (ImGui::BeginTabItem("Entries"))
        {
          if (ImGui::BeginChild("dialogue-entries-list", ImVec2(0.0f, 0.0f)))
          {
            auto entriesListChanged = false;
            auto hoveredEntry = -1;
            auto entriesShortcutScope = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
                                        ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
            auto entriesParent = container_index_get(schema, Element::ENTRIES);
            auto entryInsert = [&](int after) { return entry_insert(schema, after); };
            auto entrySelect = [&](int)
            {
              mode = ENTRY;
              scrollToEntry = selectedEntry;
            };
            auto entryMenuDraw = [&](int target)
            {
              auto actionDirty = false;
              auto targetValid = element_is_type(schema, target, Element::ENTRY);

              if (ImGui::MenuItem("Insert"))
              {
                selectedEntry = entryInsert(target);
                element_selection_set(selectedEntry, selectedEntries, selectedEntry);
                entrySelect(selectedEntry);
                actionDirty = selectedEntry >= 0;
              }
              if (ImGui::MenuItem("Duplicate", nullptr, false, targetValid))
                actionDirty =
                    selected_elements_duplicate(schema, selectedEntry, selectedEntries, Element::ENTRY) || actionDirty;
              if (ImGui::MenuItem("Delete", "Del", false, targetValid))
                actionDirty =
                    selected_elements_erase(schema, selectedEntry, selectedEntries, Element::ENTRY) || actionDirty;
              ImGui::Separator();
              if (ImGui::MenuItem("Cut", "Ctrl+X", false, targetValid))
                actionDirty =
                    selected_elements_cut(clipboard, schema, selectedEntry, selectedEntries, Element::ENTRY) ||
                    actionDirty;
              if (ImGui::MenuItem("Copy", "Ctrl+C", false, targetValid))
                selected_elements_copy(clipboard, schema, target, selectedEntries, Element::ENTRY);
              if (ImGui::MenuItem("Paste", "Ctrl+V", false, entriesParent >= 0))
              {
                auto pasted =
                    elements_paste(clipboard, schema, entriesParent, sibling_after_get(schema, entriesParent, target));
                if (pasted >= 0)
                {
                  element_selection_set(selectedEntry, selectedEntries, pasted);
                  entrySelect(pasted);
                  actionDirty = true;
                }
              }
              return actionDirty;
            };
            if (ImGui::BeginPopupContextWindow("entries-context",
                                               ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
            {
              if (entryMenuDraw(selectedEntry))
              {
                isDirty = true;
                entriesListChanged = true;
              }
              document_history_context_menu_draw();
              ImGui::EndPopup();
            }
            for (auto index : entries)
            {
              if (entriesListChanged) break;
              auto& entry = schema.elements[index];
              auto isSelected = mode == ENTRY && element_selected_has(selectedEntry, selectedEntries, index);
              auto renameResult =
                  util::imgui::selectable_renamable(std::format("entry-{}", index).c_str(), entry.idString, "<entry>",
                                                    isSelected, ImGuiSelectableFlags_SelectOnRelease);
              if (renameResult.selected && !ImGui::IsDragDropActive() &&
                  !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left))
              {
                mode = ENTRY;
                auto& io = ImGui::GetIO();
                if (io.KeyCtrl)
                  element_selection_toggle(selectedEntry, selectedEntries, index);
                else if (io.KeyShift)
                  element_selection_range_set(schema, selectedEntry, selectedEntries, index);
                else
                  element_selection_set(selectedEntry, selectedEntries, index);
                selectedPoolEntry = -1;
                selectedChoice = -1;
                selectedEffect = -1;
                textSelectionStart = 0;
                textSelectionEnd = 0;
                textSelectionStartByte = 0;
                textSelectionEndByte = 0;
                previewIndex = 0;
                previewTimer = 0.0f;
                previewBaseDelay = std::max(DELAY_MIN, delay);
                previewDelay = previewBaseDelay;
                isPreviewBlipQueued = false;
                isPlaying = util::imgui::utf8_char_count(entry.text) > 0;
                previewNeedsAnimationPlay = true;
                if (isPlaying) dialogue_preview_start_effects_apply(*this, schema, entry, previewIndex);
              }
              if (renameResult.renamed)
              {
                indices_refresh(schema);
                isDirty = true;
              }
              scroll_to_item_if_requested(scrollToEntry, index);
              if (ImGui::IsItemHovered()) hoveredEntry = index;
              if (sibling_drag_drop_draw(schema, index, "DIALOGUE_ENTRY", entry_label_get(entry).c_str(),
                                         &selectedEntries))
              {
                isDirty = true;
                entriesListChanged = true;
              }
              if (ImGui::BeginPopupContextItem())
              {
                if (!element_selected_has(selectedEntry, selectedEntries, index))
                  element_selection_set(selectedEntry, selectedEntries, index);
                if (entryMenuDraw(index))
                {
                  isDirty = true;
                  entriesListChanged = true;
                }
                document_history_context_menu_draw();
                ImGui::EndPopup();
              }
              if (entriesListChanged) break;
              entry_tooltip_draw(schema, entry);
            }
            if (!entriesListChanged && entriesShortcutScope)
            {
              auto target = hoveredEntry >= 0 ? hoveredEntry : selectedEntry;
              if (element_is_type(schema, target, Element::ENTRY))
              {
                if (editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
                {
                  if (!element_selected_has(selectedEntry, selectedEntries, target))
                    element_selection_set(selectedEntry, selectedEntries, target);
                  entriesListChanged = selected_elements_erase(schema, selectedEntry, selectedEntries, Element::ENTRY);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_X))
                {
                  if (!element_selected_has(selectedEntry, selectedEntries, target))
                    element_selection_set(selectedEntry, selectedEntries, target);
                  entriesListChanged =
                      selected_elements_cut(clipboard, schema, selectedEntry, selectedEntries, Element::ENTRY);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_C))
                {
                  if (!element_selected_has(selectedEntry, selectedEntries, target))
                    element_selection_set(selectedEntry, selectedEntries, target);
                  selected_elements_copy(clipboard, schema, target, selectedEntries, Element::ENTRY);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_V))
                {
                  auto pasted = elements_paste(clipboard, schema, entriesParent,
                                               sibling_after_get(schema, entriesParent, target));
                  if (pasted >= 0)
                  {
                    element_selection_set(selectedEntry, selectedEntries, pasted);
                    entrySelect(pasted);
                    entriesListChanged = true;
                  }
                }
                if (entriesListChanged) isDirty = true;
              }
            }
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }

        if (ImGui::BeginTabItem("Pools"))
        {
          if (ImGui::BeginChild("dialogue-pools-list", ImVec2(0.0f, 0.0f)))
          {
            auto poolsListChanged = false;
            auto hoveredPool = -1;
            auto poolsShortcutScope = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
                                      ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
            auto poolsParent = container_index_get(schema, Element::POOLS);
            auto poolInsert = [&](int after) { return pool_insert(schema, after); };
            auto poolSelect = [&](int)
            {
              mode = POOL;
              selectedPoolEntry = -1;
              scrollToPool = selectedPool;
            };
            auto poolMenuDraw = [&](int target)
            {
              auto actionDirty = false;
              auto targetValid = element_is_type(schema, target, Element::POOL);

              if (ImGui::MenuItem("Insert"))
              {
                selectedPool = poolInsert(target);
                element_selection_set(selectedPool, selectedPools, selectedPool);
                poolSelect(selectedPool);
                actionDirty = selectedPool >= 0;
              }
              if (ImGui::MenuItem("Duplicate", nullptr, false, targetValid))
                actionDirty =
                    selected_elements_duplicate(schema, selectedPool, selectedPools, Element::POOL) || actionDirty;
              if (ImGui::MenuItem("Delete", "Del", false, targetValid))
                actionDirty =
                    selected_elements_erase(schema, selectedPool, selectedPools, Element::POOL) || actionDirty;
              ImGui::Separator();
              if (ImGui::MenuItem("Cut", "Ctrl+X", false, targetValid))
                actionDirty =
                    selected_elements_cut(clipboard, schema, selectedPool, selectedPools, Element::POOL) || actionDirty;
              if (ImGui::MenuItem("Copy", "Ctrl+C", false, targetValid))
                selected_elements_copy(clipboard, schema, target, selectedPools, Element::POOL);
              if (ImGui::MenuItem("Paste", "Ctrl+V", false, poolsParent >= 0))
              {
                auto pasted =
                    elements_paste(clipboard, schema, poolsParent, sibling_after_get(schema, poolsParent, target));
                if (pasted >= 0)
                {
                  element_selection_set(selectedPool, selectedPools, pasted);
                  poolSelect(pasted);
                  actionDirty = true;
                }
              }
              return actionDirty;
            };
            if (ImGui::BeginPopupContextWindow("pools-context",
                                               ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
            {
              if (poolMenuDraw(selectedPool))
              {
                isDirty = true;
                poolsListChanged = true;
              }
              document_history_context_menu_draw();
              ImGui::EndPopup();
            }
            for (auto index : pools)
            {
              if (poolsListChanged) break;
              auto& pool = schema.elements[index];
              auto isSelected = mode == POOL && element_selected_has(selectedPool, selectedPools, index);
              auto renameResult =
                  util::imgui::selectable_renamable(std::format("pool-{}", index).c_str(), pool.idString, "<pool>",
                                                    isSelected, ImGuiSelectableFlags_SelectOnRelease);
              if (renameResult.selected && !ImGui::IsDragDropActive() &&
                  !ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left))
              {
                mode = POOL;
                auto& io = ImGui::GetIO();
                if (io.KeyCtrl)
                  element_selection_toggle(selectedPool, selectedPools, index);
                else if (io.KeyShift)
                  element_selection_range_set(schema, selectedPool, selectedPools, index);
                else
                  element_selection_set(selectedPool, selectedPools, index);
                selectedPoolEntry = -1;
              }
              if (renameResult.renamed)
              {
                indices_refresh(schema);
                isDirty = true;
              }
              scroll_to_item_if_requested(scrollToPool, index);
              if (ImGui::IsItemHovered()) hoveredPool = index;
              if (sibling_drag_drop_draw(schema, index, "DIALOGUE_POOL", pool_label_get(pool).c_str(), &selectedPools))
              {
                isDirty = true;
                poolsListChanged = true;
              }
              if (ImGui::BeginPopupContextItem())
              {
                if (!element_selected_has(selectedPool, selectedPools, index))
                  element_selection_set(selectedPool, selectedPools, index);
                if (poolMenuDraw(index))
                {
                  isDirty = true;
                  poolsListChanged = true;
                }
                document_history_context_menu_draw();
                ImGui::EndPopup();
              }
              if (poolsListChanged) break;
            }
            if (!poolsListChanged && poolsShortcutScope)
            {
              auto target = hoveredPool >= 0 ? hoveredPool : selectedPool;
              if (element_is_type(schema, target, Element::POOL))
              {
                if (editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
                {
                  if (!element_selected_has(selectedPool, selectedPools, target))
                    element_selection_set(selectedPool, selectedPools, target);
                  poolsListChanged = selected_elements_erase(schema, selectedPool, selectedPools, Element::POOL);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_X))
                {
                  if (!element_selected_has(selectedPool, selectedPools, target))
                    element_selection_set(selectedPool, selectedPools, target);
                  poolsListChanged =
                      selected_elements_cut(clipboard, schema, selectedPool, selectedPools, Element::POOL);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_C))
                {
                  if (!element_selected_has(selectedPool, selectedPools, target))
                    element_selection_set(selectedPool, selectedPools, target);
                  selected_elements_copy(clipboard, schema, target, selectedPools, Element::POOL);
                }
                else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_V))
                {
                  auto pasted =
                      elements_paste(clipboard, schema, poolsParent, sibling_after_get(schema, poolsParent, target));
                  if (pasted >= 0)
                  {
                    element_selection_set(selectedPool, selectedPools, pasted);
                    poolSelect(pasted);
                    poolsListChanged = true;
                  }
                }
                if (poolsListChanged) isDirty = true;
              }
            }
          }
          ImGui::EndChild();
          ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
      }
    }
    ImGui::EndChild();
    ImGui::EndChild();

    ImGui::SameLine();

    ImGui::PushID("dialogue-right");
    ImGui::BeginGroup();
    if (mode == ENTRY)
    {
      auto* entry = selected_get(schema, selectedEntry);
      if (!entry)
      {
        ImGui::TextUnformatted("No dialogue entry selected.");
      }
      else
      {
        auto rightAvailable = ImVec2(rightWidth, ImGui::GetContentRegionAvail().y);
        auto spacing = ImGui::GetStyle().ItemSpacing.y;
        auto playerHeight = ImGui::GetFrameHeight() + ImGui::GetStyle().WindowPadding.y * ENTRY_PLAYER_PADDING_COUNT;
        auto contentHeight =
            std::max(WIDGET_MIN_SIZE, rightAvailable.y - playerHeight - spacing * ENTRY_CONTENT_SPACING_COUNT);
        auto entityHeight = contentHeight * ENTRY_ENTITY_HEIGHT_RATIO;
        auto dialogueHeight = contentHeight * ENTRY_DIALOGUE_HEIGHT_RATIO;
        auto editorHeight = contentHeight * ENTRY_TEXT_EDITOR_HEIGHT_RATIO;
        auto propertiesHeight = std::max(WIDGET_MIN_SIZE, contentHeight - entityHeight - dialogueHeight - editorHeight);

        if (ImGui::BeginChild("dialogue-entity", ImVec2(rightWidth, entityHeight), ImGuiChildFlags_Borders))
        {
          entity_preview_update(*this, resources, schema, reference, *entry, ImGui::GetContentRegionAvail());
        }
        ImGui::EndChild();

        ImGui::PushFont(resources.font.get(), resource::Font::SMALL);
        if (ImGui::BeginChild("dialogue-display", ImVec2(rightWidth, dialogueHeight), ImGuiChildFlags_Borders))
        {
          auto maxIndex = util::imgui::utf8_char_count(entry->text);
          auto shownCount = std::min(previewIndex, maxIndex);
          auto tabName = reference && !reference->name.empty() ? reference->name : std::string{"Dialogue"};
          auto* dialogueFont = previewFont.is_valid() ? previewFont.get() : ImGui::GetFont();
          ImGui::PushStyleColor(ImGuiCol_Tab,
                                ImVec4(nameColor.r * TAB_COLOR_DIM_MULTIPLIER, nameColor.g * TAB_COLOR_DIM_MULTIPLIER,
                                       nameColor.b * TAB_COLOR_DIM_MULTIPLIER, 1.0f));
          ImGui::PushStyleColor(ImGuiCol_TabHovered, ImVec4(nameColor.r * TAB_COLOR_HOVER_MULTIPLIER,
                                                            nameColor.g * TAB_COLOR_HOVER_MULTIPLIER,
                                                            nameColor.b * TAB_COLOR_HOVER_MULTIPLIER, 1.0f));
          ImGui::PushStyleColor(ImGuiCol_TabSelected, ImVec4(nameColor.r, nameColor.g, nameColor.b, 1.0f));
          ImGui::PushFont(dialogueFont, resource::Font::NORMAL);
          if (ImGui::BeginTabBar("dialogue-name-tab", ImGuiTabBarFlags_NoCloseWithMiddleMouseButton))
          {
            if (ImGui::BeginTabItem(tabName.c_str())) ImGui::EndTabItem();
            ImGui::EndTabBar();
          }
          ImGui::PopFont();
          ImGui::PopStyleColor(TAB_STYLE_COLOR_COUNT);
          ImGui::PushFont(dialogueFont, resource::Font::NORMAL);
          auto effects = dialogue_effect_spans_get(schema, *entry);
          util::imgui::dialogue_text_draw(entry->text, shownCount, effects, textColor, (float)ImGui::GetTime());
          ImGui::PopFont();

          auto displayAdvance =
              ImGui::IsWindowHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left) && !ImGui::IsAnyItemHovered();
          if (shownCount >= maxIndex)
          {
            auto choices = schema.children_get(*entry, Element::CHOICE);
            if (!choices.empty())
            {
              ImGui::PushFont(previewFont.is_valid() ? previewFont.get() : ImGui::GetFont(), resource::Font::NORMAL);
              auto contentMin = ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMin().x,
                                       ImGui::GetWindowPos().y + ImGui::GetWindowContentRegionMin().y);
              auto contentMax = ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x,
                                       ImGui::GetWindowPos().y + ImGui::GetWindowContentRegionMax().y);
              auto buttonY = contentMax.y - ImGui::GetFrameHeight();
              auto separatorY = buttonY - ImGui::GetStyle().ItemSpacing.y;
              ImGui::GetWindowDrawList()->AddLine(ImVec2(contentMin.x, separatorY), ImVec2(contentMax.x, separatorY),
                                                  ImGui::GetColorU32(ImGuiCol_Separator));
              ImGui::SetCursorScreenPos(ImVec2(contentMin.x, buttonY));
              auto buttonSize = util::imgui::row_widget_size_get((int)choices.size());
              ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, CHOICE_BUTTON_ROUNDING);
              ImGui::PushStyleVar(ImGuiStyleVar_GrabRounding, CHOICE_BUTTON_ROUNDING);
              ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(nameColor.r, nameColor.g, nameColor.b, 1.0f));
              ImGui::PushStyleColor(
                  ImGuiCol_ButtonHovered,
                  ImVec4(std::min(nameColor.r * CHOICE_BUTTON_HOVER_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX),
                         std::min(nameColor.g * CHOICE_BUTTON_HOVER_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX),
                         std::min(nameColor.b * CHOICE_BUTTON_HOVER_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX), 1.0f));
              ImGui::PushStyleColor(
                  ImGuiCol_ButtonActive,
                  ImVec4(std::min(nameColor.r * CHOICE_BUTTON_ACTIVE_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX),
                         std::min(nameColor.g * CHOICE_BUTTON_ACTIVE_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX),
                         std::min(nameColor.b * CHOICE_BUTTON_ACTIVE_MULTIPLIER, CHOICE_BUTTON_COLOR_MAX), 1.0f));
              for (auto index = 0; index < (int)choices.size(); ++index)
              {
                auto* choice = choices[index];
                auto label = choice->text.empty() ? "<choice>" : choice->text.c_str();
                ImGui::PushID(index);
                if (WIDGET_FX(ImGui::Button(label, buttonSize)))
                {
                  if (auto* next = schema.dialogue_entry_get(*choice))
                    dialogue_preview_entry_set(*this, schema, element_index_get(schema, next), true);
                }
                ImGui::SetItemTooltip("%s", label);
                ImGui::PopID();
                if (index + 1 < (int)choices.size()) ImGui::SameLine();
              }
              ImGui::PopStyleColor(CHOICE_BUTTON_STYLE_COLOR_COUNT);
              ImGui::PopStyleVar(CHOICE_BUTTON_STYLE_VAR_COUNT);
              ImGui::PopFont();
            }
            else if (displayAdvance && !entry->next.empty())
            {
              if (auto* next = schema.dialogue_entry_get(*entry))
              {
                dialogue_preview_entry_set(*this, schema, element_index_get(schema, next), true);
              }
            }
            if (!entry->next.empty())
            {
              auto indicatorSize = ImVec2(ImGui::GetTextLineHeightWithSpacing(), ImGui::GetTextLineHeightWithSpacing());
              auto contentMax = ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x,
                                       ImGui::GetWindowPos().y + ImGui::GetWindowContentRegionMax().y);
              ImGui::SetCursorScreenPos(ImVec2(contentMax.x - indicatorSize.x, contentMax.y - indicatorSize.y));
              auto cursorPos = ImGui::GetCursorScreenPos();
              auto offset = std::sin((float)ImGui::GetTime() * NEXT_INDICATOR_SPEED) *
                            (indicatorSize.x * NEXT_INDICATOR_OFFSET_RATIO);
              auto center = ImVec2(cursorPos.x + (indicatorSize.x * CENTER_RATIO) + offset,
                                   cursorPos.y + (indicatorSize.y * CENTER_RATIO));
              auto half = std::min(indicatorSize.x, indicatorSize.y) * NEXT_INDICATOR_HALF_RATIO;
              auto tip = ImVec2(center.x + half, center.y);
              auto baseA = ImVec2(center.x - half, center.y - half);
              auto baseB = ImVec2(center.x - half, center.y + half);
              auto color = ImGui::GetColorU32(ImGuiCol_Text);
              ImGui::GetWindowDrawList()->AddTriangleFilled(tip, baseA, baseB, color);
              ImGui::InvisibleButton("##next-indicator", indicatorSize);
            }
          }
          else if (displayAdvance)
          {
            dialogue_preview_skipped_effects_apply(*this, schema, *entry, previewIndex, maxIndex);
            previewIndex = maxIndex;
            previewTimer = 0.0f;
            isPreviewBlipQueued = false;
            isPlaying = false;
          }
        }
        ImGui::EndChild();
        ImGui::PopFont();
        entry = selected_get(schema, selectedEntry);
        if (!entry)
        {
          ImGui::EndGroup();
          ImGui::PopID();
          return isDirty;
        }

        if (ImGui::BeginChild("dialogue-text-editor", ImVec2(rightWidth, editorHeight), ImGuiChildFlags_Borders))
        {
          auto textResult =
              util::imgui::input_string_multiline_result("##entry-text", entry->text, ImVec2(-FLT_MIN, -FLT_MIN));
          auto textEditorRectMin = ImGui::GetItemRectMin();
          auto textEditorRectMax = ImGui::GetItemRectMax();

          auto textEditorRightClicked = ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
          auto textEditorScroll = ImVec2{};
          if (auto* inputState = ImGui::GetInputTextState(ImGui::GetItemID()))
          {
            textEditorScroll = inputState->Scroll;
            auto selectionStart = inputState->GetSelectionStart();
            auto selectionEnd = inputState->GetSelectionEnd();
            if (selectionEnd < selectionStart) std::swap(selectionStart, selectionEnd);
            if (selectionStart != selectionEnd)
            {
              textSelectionStartByte = selectionStart;
              textSelectionEndByte = selectionEnd;
              textSelectionStart = util::imgui::utf8_byte_to_char_index(entry->text, selectionStart);
              textSelectionEnd = util::imgui::utf8_byte_to_char_index(entry->text, selectionEnd);
            }
            else if (textEditorRightClicked && textSelectionStartByte != textSelectionEndByte)
            {
              inputState->WantReloadUserBuf = true;
              inputState->ReloadSelectionStart = textSelectionStartByte;
              inputState->ReloadSelectionEnd = textSelectionEndByte;
            }
            else if (!textEditorRightClicked)
            {
              textSelectionStartByte = selectionStart;
              textSelectionEndByte = selectionEnd;
              textSelectionStart = util::imgui::utf8_byte_to_char_index(entry->text, selectionStart);
              textSelectionEnd = util::imgui::utf8_byte_to_char_index(entry->text, selectionEnd);
            }
          }
          if ((textEditorRightClicked || ImGui::IsPopupOpen("text-editor-context") ||
               ImGui::IsPopupOpen("text-editor-item-context")) &&
              textSelectionStartByte != textSelectionEndByte)
            text_selection_overlay_draw(entry->text, textSelectionStartByte, textSelectionEndByte, textEditorRectMin,
                                        textEditorRectMax, textEditorScroll);

          if (ImGui::BeginPopupContextItem("text-editor-item-context"))
          {
            if (dialogue_child_insert_menu_draw(*this, schema, selectedEntry, dialogue_child_selected_get(*this)))
              isDirty = true;
            if (selectedEffect >= 0) scrollToEffect = selectedEffect;
            document_history_context_menu_draw();
            ImGui::EndPopup();
          }

          if (ImGui::BeginPopupContextWindow("text-editor-context", ImGuiPopupFlags_MouseButtonRight))
          {
            if (dialogue_child_insert_menu_draw(*this, schema, selectedEntry, dialogue_child_selected_get(*this)))
              isDirty = true;
            if (selectedEffect >= 0) scrollToEffect = selectedEffect;
            document_history_context_menu_draw();
            ImGui::EndPopup();
          }

          entry = selected_get(schema, selectedEntry);
          if (!entry)
          {
            ImGui::EndChild();
            ImGui::EndGroup();
            ImGui::PopID();
            return isDirty;
          }

          if (textResult.changed || textResult.committed)
          {
            auto textLength = util::imgui::utf8_char_count(entry->text);
            if (isPlaying)
              previewIndex = std::min(previewIndex, textLength);
            else
              previewIndex = textLength;
            if (textResult.committed) isDirty = true;
          }
        }
        ImGui::EndChild();

        if (ImGui::BeginChild("dialogue-properties", ImVec2(rightWidth, propertiesHeight), ImGuiChildFlags_Borders))
        {
          auto propertiesShortcutScope = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
                                         ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
          auto availableWidth = ImGui::GetContentRegionAvail().x;
          auto spacingX = ImGui::GetStyle().ItemSpacing.x;
          auto innerSpacingX = ImGui::GetStyle().ItemInnerSpacing.x;
          auto idLabelWidth = ImGui::CalcTextSize("Label").x + innerSpacingX;
          auto nextLabelWidth = ImGui::CalcTextSize("Next").x + innerSpacingX;
          auto itemWidth = std::max(PROPERTY_ITEM_MIN_WIDTH,
                                    (availableWidth - spacingX - idLabelWidth - nextLabelWidth) * CENTER_RATIO);
          auto choices = dialogue_choice_indices_get(schema, *entry);
          ImGui::SetNextItemWidth(itemWidth);
          if (util::imgui::input_string("Label", entry->idString))
          {
            indices_refresh(schema);
            isDirty = true;
          }
          ImGui::SameLine();
          ImGui::BeginDisabled(!choices.empty());
          ImGui::SetNextItemWidth(itemWidth);
          if (entry_combo_draw("Next", schema, entry->next)) isDirty = true;
          ImGui::EndDisabled();
          ImGui::Separator();
          if (util::imgui::input_string("Animation", entry->animation))
          {
            isDirty = true;
          }
          if (ImGui::BeginChild("dialogue-children-child", ImVec2(0.0f, 0.0f)))
          {
            auto isChildrenStructureChanged = false;
            auto isEffectsChanged = false;
            auto hoveredChild = -1;
            auto children = dialogue_child_indices_get(schema, *entry);
            auto childrenRectMin = ImGui::GetCursorScreenPos();
            auto childInsert = [&](int) { return -1; };
            auto childSelect = [&](int index) { dialogue_child_select(*this, schema, index); };
            auto childClear = [&]() { dialogue_child_clear(*this); };
            auto childActionDraw = [&](int target)
            {
              auto type = dialogue_child_type_get(schema, target);
              auto selectedChild = dialogue_child_selected_get(*this);
              auto options =
                  element_action_options_get(selectedEntry, target, type, false, true, type != Element::UNKNOWN);
              options.isPasteEnabled = type != Element::UNKNOWN;
              return editor_schema::element_context_menu_draw(clipboard, schema, options, selectedChild, childInsert,
                                                            childSelect, childClear);
            };

            if (ImGui::BeginTable("dialogue-children", DIALOGUE_CHILD_TABLE_COLUMNS,
                                  ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable |
                                      ImGuiTableFlags_SizingStretchSame))
            {
              ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthStretch, 1.0f);
              ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch, 1.0f);
              ImGui::TableSetupColumn("Start / Next", ImGuiTableColumnFlags_WidthStretch, 1.0f);
              ImGui::TableSetupColumn("End", ImGuiTableColumnFlags_WidthStretch, 1.0f);
              for (auto childIndex : children)
              {
                if (childIndex < 0 || childIndex >= (int)schema.elements.size()) continue;
                auto& child = schema.elements[childIndex];
                ImGui::PushID(childIndex);
                ImGui::TableNextRow();

                ImGui::TableNextColumn();
                auto previousType = child.type;
                auto childLabel = child.type == Element::CHOICE ? "Choice" : child.typeString.c_str();
                auto isChildHovered = false;
                auto isChildClicked = false;
                auto isChildContextRequested = false;
                if (dialogue_child_type_combo_draw(*this, schema, selectedEntry, childIndex))
                {
                  isDirty = true;
                  if (child.type == Element::EFFECT) isEffectsChanged = true;
                  if (child.type != previousType) isChildrenStructureChanged = true;
                }
                scroll_to_item_if_requested(scrollToChoice, childIndex);
                scroll_to_item_if_requested(scrollToEffect, childIndex);
                isChildHovered = ImGui::IsItemHovered();
                isChildClicked = ImGui::IsItemClicked();
                isChildContextRequested = isChildHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
                if (sibling_drag_drop_draw(schema, childIndex, "DIALOGUE_CHILD", childLabel))
                {
                  isDirty = true;
                  isChildrenStructureChanged = true;
                }

                auto contextSpaceWidth = ImGui::GetContentRegionAvail().x;
                if (contextSpaceWidth > WIDGET_MIN_SIZE)
                {
                  ImGui::SameLine(0.0f, 0.0f);
                  ImGui::InvisibleButton("##Child Context Space", ImVec2(contextSpaceWidth, ImGui::GetFrameHeight()));
                  isChildHovered = ImGui::IsItemHovered() || isChildHovered;
                  isChildClicked = ImGui::IsItemClicked() || isChildClicked;
                  isChildContextRequested = (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) ||
                                            isChildContextRequested;
                  if (sibling_drag_drop_draw(schema, childIndex, "DIALOGUE_CHILD", childLabel))
                  {
                    isDirty = true;
                    isChildrenStructureChanged = true;
                  }
                }

                if (isChildHovered) hoveredChild = childIndex;
                if (isChildClicked) dialogue_child_select(*this, schema, childIndex);
                if (isChildContextRequested) ImGui::OpenPopup("child-context");
                if (ImGui::BeginPopup("child-context"))
                {
                  dialogue_child_select(*this, schema, childIndex);
                  auto isRowStructureChanged = false;
                  if (dialogue_child_insert_menu_draw(*this, schema, selectedEntry, childIndex))
                  {
                    isDirty = true;
                    isChildrenStructureChanged = true;
                    isRowStructureChanged = true;
                  }
                  auto action = childActionDraw(childIndex);
                  if (action.isDirty) isDirty = true;
                  if (action.isStructureChanged) isChildrenStructureChanged = true;
                  if (action.isStructureChanged) isRowStructureChanged = true;
                  document_history_context_menu_draw();
                  ImGui::EndPopup();
                  if (isRowStructureChanged)
                  {
                    ImGui::PopID();
                    break;
                  }
                }

                ImGui::TableNextColumn();
                if (child.type == Element::CHOICE)
                {
                  ImGui::SetNextItemWidth(-FLT_MIN);
                  if (util::imgui::input_string("##Choice Text", child.text)) isDirty = true;
                }
                else
                {
                  switch (dialogue_effect_type_get(child.typeString))
                  {
                    case DialogueEffectType::ANIMATION:
                      ImGui::SetNextItemWidth(-FLT_MIN);
                      if (util::imgui::input_string("##Value", child.animation)) isEffectsChanged = true;
                      break;
                    case DialogueEffectType::DELAY:
                      ImGui::SetNextItemWidth(-FLT_MIN);
                      if (ImGui::InputInt("##Value", &child.delayTicks)) isEffectsChanged = true;
                      break;
                    case DialogueEffectType::OSCILLATION:
                    case DialogueEffectType::SHAKE:
                    case DialogueEffectType::BIG:
                    case DialogueEffectType::SMALL:
                      break;
                    case DialogueEffectType::SCREEN_SHAKE:
                      break;
                    case DialogueEffectType::SOUND:
                    {
                      auto* root = schema.root();
                      auto rootPath = root ? root->soundRootPath : std::string{};
                      if (!child.sound.empty() && child.soundEntry.path != child.sound)
                        sound_effect_entry_rebuild(schemaPath, rootPath, child);
                      auto displayPath = resource_display_path_get(schemaPath, rootPath, child.sound);
                      auto suffixWidth = SOUND_WIDGET_WIDTH;
                      auto pathWidth = std::max(WIDGET_MIN_SIZE, ImGui::GetContentRegionAvail().x - suffixWidth -
                                                                     ImGui::GetStyle().ItemSpacing.x);
                      if (util::imgui::path_widget("##Sound Value", child.sound, displayPath, ImVec2(pathWidth, 0.0f)))
                      {
                        pendingSetting = PENDING_SOUND;
                        pendingSoundEffect = childIndex;
                        dialog.file_open(util::Dialog::SCHEMA_SOUND_OPEN);
                      }
                      ImGui::SameLine();
                      auto isMissing = !child.sound.empty() && !child.soundEntry.sound.is_valid();
                      if (util::imgui::sound_widget("##Sound Play", isMissing))
                      {
                        if (!child.soundEntry.sound.is_valid()) sound_effect_entry_rebuild(schemaPath, rootPath, child);
                        if (child.soundEntry.sound.is_valid()) child.soundEntry.sound.play();
                      }
                      break;
                    }
                    default:
                      ImGui::SetNextItemWidth(-FLT_MIN);
                      if (ImGui::ColorEdit3("##Value", &child.color.x)) isEffectsChanged = true;
                      break;
                  }
                }

                ImGui::TableNextColumn();
                if (child.type == Element::CHOICE)
                {
                  ImGui::SetNextItemWidth(-FLT_MIN);
                  if (entry_combo_draw("##Choice Next", schema, child.next)) isDirty = true;
                }
                else
                {
                  ImGui::TextUnformatted("Start");
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(-FLT_MIN);
                  if (ImGui::InputInt("##Start", &child.start, EFFECT_INDEX_STEP, EFFECT_INDEX_FAST_STEP))
                    isEffectsChanged = true;
                }

                ImGui::TableNextColumn();
                if (child.type == Element::CHOICE)
                {
                  ImGui::TextUnformatted("-");
                }
                else if (dialogue_effect_is_ranged(child))
                {
                  ImGui::TextUnformatted("End");
                  ImGui::SameLine();
                  ImGui::SetNextItemWidth(-FLT_MIN);
                  if (ImGui::InputInt("##End", &child.end, EFFECT_INDEX_STEP, EFFECT_INDEX_FAST_STEP))
                    isEffectsChanged = true;
                }

                ImGui::PopID();
                if (isChildrenStructureChanged) break;
              }
              ImGui::EndTable();
            }
            auto childrenRectMax = ImGui::GetCursorScreenPos();
            auto isChildrenShortcutScope =
                propertiesShortcutScope && ImGui::IsMouseHoveringRect(childrenRectMin, childrenRectMax);
            if (ImGui::IsMouseHoveringRect(childrenRectMin, childrenRectMax) &&
                ImGui::BeginPopupContextWindow("children-context",
                                               ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
            {
              auto target = dialogue_child_selected_get(*this);
              if (dialogue_child_insert_menu_draw(*this, schema, selectedEntry, target))
              {
                isDirty = true;
                isChildrenStructureChanged = true;
              }
              auto action = childActionDraw(target);
              if (action.isDirty) isDirty = true;
              if (action.isStructureChanged) isChildrenStructureChanged = true;
              document_history_context_menu_draw();
              ImGui::EndPopup();
            }
            if (isChildrenShortcutScope)
            {
              auto target = hoveredChild >= 0 ? hoveredChild : dialogue_child_selected_get(*this);
              auto type = dialogue_child_type_get(schema, target);
              auto selectedChild = dialogue_child_selected_get(*this);
              auto options = element_action_options_get(selectedEntry, target, type, false);
              options.isPasteEnabled = type != Element::UNKNOWN;
              auto action = editor_schema::element_shortcuts_apply(clipboard, schema, options, selectedChild, childSelect,
                                                                 childClear);
              if (action.isDirty) isDirty = true;
              if (action.isStructureChanged) isChildrenStructureChanged = true;
            }
            if (isChildrenStructureChanged)
            {
              entry = selected_get(schema, selectedEntry);
              if (!entry)
              {
                ImGui::EndChild();
                ImGui::EndChild();
                ImGui::EndGroup();
                ImGui::PopID();
                return isDirty;
              }
            }

            if (isEffectsChanged)
            {
              entry = selected_get(schema, selectedEntry);
              if (!entry)
              {
                ImGui::EndChild();
                ImGui::EndChild();
                ImGui::EndGroup();
                ImGui::PopID();
                return isDirty;
              }
              for (auto effectIndex : dialogue_effect_indices_get(schema, *entry))
              {
                auto& effect = schema.elements[effectIndex];
                dialogue_effect_defaults_apply(effect);
                switch (dialogue_effect_type_get(effect.typeString))
                {
                  case DialogueEffectType::DELAY:
                    effect.delayTicks = std::max(DELAY_MIN, effect.delayTicks);
                    break;
                  case DialogueEffectType::OSCILLATION:
                  case DialogueEffectType::SHAKE:
                  case DialogueEffectType::SCREEN_SHAKE:
                  case DialogueEffectType::BIG:
                  case DialogueEffectType::SMALL:
                    break;
                  default:
                    break;
                }
              }
              isDirty = true;
            }
          }
          ImGui::EndChild();
        }
        ImGui::EndChild();

        if (ImGui::BeginChild("dialogue-player", ImVec2(rightWidth, playerHeight), ImGuiChildFlags_Borders))
        {
          auto playPauseWidth =
              ImGui::CalcTextSize("Pause").x + ImGui::GetStyle().FramePadding.x * ENTRY_PLAYER_PADDING_COUNT;
          if (ImGui::Button(isPlaying ? "Pause" : "Play", ImVec2(playPauseWidth, 0.0f)))
          {
            if (isPlaying)
              isPlaying = false;
            else
            {
              if (previewIndex >= util::imgui::utf8_char_count(entry->text)) previewIndex = 0;
              previewTimer = 0.0f;
              previewBaseDelay = std::max(DELAY_MIN, delay);
              previewDelay = previewBaseDelay;
              isPreviewBlipQueued = false;
              isPlaying = true;
              previewNeedsAnimationPlay = true;
              dialogue_preview_start_effects_apply(*this, schema, *entry, previewIndex);
            }
          }
          ImGui::SameLine();
          auto maxIndex = util::imgui::utf8_char_count(entry->text);
          if (isPlaying && previewIndex < maxIndex)
          {
            previewTimer += ImGui::GetIO().DeltaTime;
            auto secondsPerIndex = std::max(DELAY_MIN, previewDelay) / PREVIEW_RATE;
            while (previewTimer >= secondsPerIndex && previewIndex < maxIndex)
            {
              previewTimer -= secondsPerIndex;
              ++previewIndex;
              previewDelay = previewBaseDelay;
              dialogue_preview_start_effects_apply(*this, schema, *entry, previewIndex);
              auto delayCharacter = dialogue_delay_character_get(schema, entry->text, previewIndex - 1, previewDelay);
              previewDelay = delayCharacter.delay;
              auto isBlipStep = blipDelay > 0 && previewIndex % blipDelay == 0;
              if (delayCharacter.isDisableTalk)
              {
                if (isBlipStep) isPreviewBlipQueued = true;
              }
              else if (isBlipStep || isPreviewBlipQueued)
              {
                if (previewBlip && previewBlip->is_valid()) previewBlip->play();
                isPreviewBlipQueued = false;
              }
              secondsPerIndex = std::max(DELAY_MIN, previewDelay) / PREVIEW_RATE;
            }
          }
          if (previewIndex >= maxIndex)
          {
            isPlaying = false;
            isPreviewBlipQueued = false;
          }
          ImGui::SetNextItemWidth(-FLT_MIN);
          if (ImGui::SliderInt("##dialogue-index", &previewIndex, 0, maxIndex))
          {
            previewTimer = 0.0f;
            isPlaying = false;
            previewBaseDelay = std::max(DELAY_MIN, delay);
            previewDelay = previewBaseDelay;
            isPreviewBlipQueued = false;
          }
        }
        ImGui::EndChild();
      }
    }
    else
    {
      auto* pool = selected_get(schema, selectedPool);
      if (!pool)
      {
        ImGui::TextUnformatted("No dialogue pool selected.");
      }
      else if (ImGui::BeginChild("pool-entries", ImVec2(rightWidth, 0.0f), ImGuiChildFlags_Borders))
      {
        auto poolEntriesShortcutScope = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
                                        ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
        auto hoveredPoolEntry = -1;
        if (util::imgui::input_string("Name", pool->idString))
        {
          indices_refresh(schema);
          isDirty = true;
        }
        ImGui::Separator();
        if (ImGui::BeginPopupContextWindow("pool-context",
                                           ImGuiPopupFlags_MouseButtonRight | ImGuiPopupFlags_NoOpenOverItems))
        {
          if (pool_entry_insert_menu_draw(schema, selectedPool, selectedPoolEntry, entries, selectedPoolEntry,
                                          scrollToPoolEntry))
            isDirty = true;
          if (ImGui::MenuItem("Delete", "Del", false, selectedPoolEntry >= 0))
          {
            if (pool_entry_delete(schema, selectedPool, selectedPoolEntry))
            {
              selectedPoolEntry = -1;
              isDirty = true;
            }
          }
          document_history_context_menu_draw();
          ImGui::EndPopup();
        }
        auto children = pool->children;
        for (auto childIndex : children)
        {
          if (childIndex < 0 || childIndex >= (int)schema.elements.size()) continue;
          auto& poolEntry = schema.elements[childIndex];
          if (poolEntry.type != Element::POOL_ENTRY) continue;
          auto label = poolEntry.idString.empty() ? "<entry>" : poolEntry.idString.c_str();
          if (ImGui::Selectable(label, selectedPoolEntry == childIndex)) selectedPoolEntry = childIndex;
          scroll_to_item_if_requested(scrollToPoolEntry, childIndex);
          if (ImGui::IsItemHovered()) hoveredPoolEntry = childIndex;
          if (sibling_drag_drop_draw(schema, childIndex, "DIALOGUE_POOL_ENTRY", label))
          {
            selectedPoolEntry = childIndex;
            isDirty = true;
          }
          if (ImGui::BeginPopupContextItem())
          {
            if (pool_entry_insert_menu_draw(schema, selectedPool, childIndex, entries, selectedPoolEntry,
                                            scrollToPoolEntry))
              isDirty = true;
            if (ImGui::MenuItem("Delete", "Del"))
            {
              if (pool_entry_delete(schema, selectedPool, childIndex))
              {
                selectedPoolEntry = -1;
                isDirty = true;
              }
            }
            document_history_context_menu_draw();
            ImGui::EndPopup();
          }
          if (auto* entry = schema.dialogue_entry_get(poolEntry.idString)) entry_tooltip_draw(schema, *entry);
        }
        if (poolEntriesShortcutScope && editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
        {
          auto target = hoveredPoolEntry >= 0 ? hoveredPoolEntry : selectedPoolEntry;
          if (target >= 0 && pool_entry_delete(schema, selectedPool, target))
          {
            selectedPoolEntry = -1;
            isDirty = true;
          }
        }
      }
      ImGui::EndChild();
    }
    ImGui::EndGroup();
    ImGui::PopID();

    return isDirty;
  }
}
