#include "text.hpp"

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

#include <algorithm>
#include <cmath>
#include <string_view>
#include <vector>

#include "../../util/imgui.hpp"
#include "../../util/imgui/dialogue_text.hpp"
#include "../../util/imgui/widget.hpp"
#include "../../util/math.hpp"

using namespace game::util;

namespace game::state::play
{
  namespace
  {
    bool dialogue_effect_is(const resource::xml::Schema::Element& element)
    {
      return element.type == resource::xml::Schema::Element::EFFECT;
    }

    constexpr auto OSCILLATION_AMPLITUDE_DEFAULT = 2.0f;
    constexpr auto OSCILLATION_FREQUENCY_DEFAULT = 1.0f;
    constexpr auto SCREEN_SHAKE_MAGNITUDE_DEFAULT = 0.03f;
    constexpr auto SCREEN_SHAKE_TIME_TICKS_DEFAULT = 15;
    constexpr auto SHAKE_AMPLITUDE_DEFAULT = 1.0f;
    constexpr auto SHAKE_FREQUENCY_DEFAULT = 30.0f;
    constexpr auto TEXT_SCALE_BIG = 1.25f;
    constexpr auto TEXT_SCALE_SMALL = 0.75f;

    std::string_view dialogue_char_get(std::string_view text, int index)
    {
      auto* start = text.data();
      auto* end = start + text.size();
      auto* charStart = imgui::utf8_advance_chars(start, end, index);
      auto* charEnd = 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 resource::xml::Schema& dialogue, std::string_view text,
                                                        int index, int fallback)
    {
      auto value = dialogue_char_get(text, index);
      if (value.empty()) return {.delay = fallback};

      auto* root = dialogue.root();
      if (!root) return {.delay = fallback};
      auto* textPlayback = dialogue.child_get(*root, resource::xml::Schema::Element::TEXT_PLAYBACK);
      auto* delayCharacters =
          textPlayback ? dialogue.child_get(*textPlayback, resource::xml::Schema::Element::DELAY_CHARACTERS) : nullptr;
      if (!delayCharacters) return {.delay = fallback};

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

    void dialogue_effect_one_shot_apply(const resource::xml::Schema::Element& effect, Canvas& canvas)
    {
      if (effect.typeString == "Screenshake")
      {
        canvas.shake(SCREEN_SHAKE_MAGNITUDE_DEFAULT, SCREEN_SHAKE_TIME_TICKS_DEFAULT);
      }
      else if (effect.typeString == "Sound" && effect.soundEntry.is_valid())
      {
        effect.soundEntry.sound.play();
      }
    }

    bool dialogue_entry_terminal_get(const resource::xml::Schema& dialogue,
                                     const resource::xml::Schema::Element& entry)
    {
      if (!entry.next.empty()) return false;
      for (auto child : entry.children)
      {
        if (child < 0 || child >= (int)dialogue.elements.size()) continue;
        if (dialogue.elements[child].type == resource::xml::Schema::Element::CHOICE) return false;
      }
      return true;
    }
  }

  void Text::set(resource::xml::Schema::Element* dialogueEntry, Entity& character, bool isInterruptible)
  {
    if (!dialogueEntry) return;
    this->entry = dialogueEntry;

    isFinished = false;
    isTerminal = dialogue_entry_terminal_get(character.data.dialogue, *dialogueEntry);
    index = 0;
    updateDelay = 0;
    baseDelay = 0;
    currentDelay = 0;
    isStartEffectsApplied = false;
    isBlipQueued = false;
    time = 0.0f;
    isEnabled = true;
    if (!dialogueEntry->animation.empty())
      character.play({.animation = dialogueEntry->animation,
                      .appendID = character.animation_append_id_get(),
                      .interrupt = isInterruptible ? Entity::Interrupt::IF_ALLOWED
                                                   : Entity::Interrupt::NEVER});
    auto* dialogueRoot = character.data.dialogue.root();
    baseDelay = dialogueRoot && dialogueRoot->delayTicks > 0 ? dialogueRoot->delayTicks : 2;
    currentDelay = baseDelay;
    for (auto child : dialogueEntry->children)
    {
      if (child < 0 || child >= (int)character.data.dialogue.elements.size()) continue;
      auto& effect = character.data.dialogue.elements[child];
      if (!dialogue_effect_is(effect) || effect.start != 0) continue;
      if (effect.typeString == "Delay")
      {
        baseDelay = std::max(1, effect.delayTicks);
        currentDelay = baseDelay;
      }
      else if (effect.typeString == "Animation" && !effect.animation.empty())
      {
        character.play({.animation = effect.animation,
                        .appendID = character.animation_append_id_get(),
                        .mode = Entity::PLAY_FORCE});
      }
    }
    if (dialogueEntry->text.empty())
      isEnabled = false;
    else
      character.isTalking = true;
  }

  void Text::update(Entity& character, Canvas& canvas)
  {
    static constexpr auto WIDTH_MULTIPLIER = 0.30f;
    static constexpr auto HEIGHT_MULTIPLIER = 6.0f;

    if (!entry) return;
    if (!isFinished)
    {
      if (!isStartEffectsApplied)
      {
        isStartEffectsApplied = true;
        for (auto child : entry->children)
        {
          if (child < 0 || child >= (int)character.data.dialogue.elements.size()) continue;
          auto& effect = character.data.dialogue.elements[child];
          if (!dialogue_effect_is(effect) || effect.start != 0) continue;
          dialogue_effect_one_shot_apply(effect, canvas);
        }
      }
      auto* dialogueRoot = character.data.dialogue.root();
      auto rootDelay = dialogueRoot && dialogueRoot->delayTicks > 0 ? dialogueRoot->delayTicks : 2;
      if (baseDelay <= 0) baseDelay = rootDelay;
      if (currentDelay <= 0) currentDelay = baseDelay;
      if (++updateDelay >= currentDelay)
      {
        updateDelay = 0;
        index++;
        currentDelay = baseDelay;
        for (auto child : entry->children)
        {
          if (child < 0 || child >= (int)character.data.dialogue.elements.size()) continue;
          auto& effect = character.data.dialogue.elements[child];
          if (!dialogue_effect_is(effect) || effect.start != index) continue;
          if (effect.typeString == "Delay")
          {
            baseDelay = std::max(1, effect.delayTicks);
            currentDelay = baseDelay;
          }
          else if (effect.typeString == "Animation" && !effect.animation.empty())
          {
            character.play({.animation = effect.animation,
                            .appendID = character.animation_append_id_get(),
                            .mode = Entity::PLAY_FORCE});
          }
          else
          {
            dialogue_effect_one_shot_apply(effect, canvas);
          }
        }
        auto delayCharacter = dialogue_delay_character_get(character.data.dialogue, entry->text, index - 1, currentDelay);
        currentDelay = delayCharacter.delay;
        character.isTalking = !delayCharacter.isDisableTalk;
        auto blipDelay = dialogueRoot ? dialogueRoot->blipDelayTicks : 0;
        auto isBlipStep = blipDelay > 0 && index % blipDelay == 0;
        if (delayCharacter.isDisableTalk)
        {
          if (isBlipStep) isBlipQueued = true;
        }
        else if (isBlipStep || isBlipQueued)
        {
          if (dialogueRoot) dialogueRoot->soundBlip.play();
          isBlipQueued = false;
        }

        if (index >= imgui::utf8_char_count(entry->text))
        {
          isFinished = true;
          character.isTalking = false;
          isBlipQueued = false;
        }
      }
    }

    auto& dialogue = character.data.dialogue;
    auto& menuSchema = character.data.menuSchema;

    auto& style = ImGui::GetStyle();
    auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);

    auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
                       ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER);
    auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), windowSize.y - size.y - style.WindowPadding.y);

    ImGui::SetNextWindowSize(size);
    ImGui::SetNextWindowPos(pos);

    if (!entry) return;

    if (ImGui::Begin("##Text", nullptr,
                     ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
                         ImGuiWindowFlags_NoMove))
    {
      auto isHovered = ImGui::IsWindowHovered();
      auto isMouse = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
      auto isSpace = ImGui::IsKeyReleased(ImGuiKey_Space);
      auto isAdvance = (isHovered && isMouse) || isSpace;

      ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x, 0));

      if (ImGui::BeginTabBar("##Name"))
      {
        if (ImGui::BeginTabItem(character.data.root()->name.c_str())) ImGui::EndTabItem();
        ImGui::EndTabBar();
      }

      auto font = ImGui::GetFont();
      auto fontSize = resource::Font::NORMAL;

      ImGui::PushFont(font, (float)fontSize);

      auto text = [&]()
      {
        auto text = entry ? std::string_view(entry->text) : "null";
        auto length = std::clamp(index, 0, imgui::utf8_char_count(text));

        if (length <= 0)
        {
          ImGui::Dummy(ImVec2(1.0f, ImGui::GetTextLineHeight()));
          return;
        }

        std::vector<imgui::DialogueEffectSpan> effects{};
        effects.reserve(entry->children.size());
        for (auto child : entry->children)
        {
          if (child < 0 || child >= (int)dialogue.elements.size()) continue;
          auto& effect = dialogue.elements[child];
          if (!dialogue_effect_is(effect)) continue;
          if (effect.typeString == "Color")
            effects.push_back({.type = imgui::DialogueEffectSpan::COLOR,
                               .start = effect.start,
                               .end = effect.end,
                               .color = effect.color});
          else if (effect.typeString == "Big")
            effects.push_back({.type = imgui::DialogueEffectSpan::TEXT_SCALE,
                               .start = effect.start,
                               .end = effect.end,
                               .textScale = TEXT_SCALE_BIG});
          else if (effect.typeString == "Small")
            effects.push_back({.type = imgui::DialogueEffectSpan::TEXT_SCALE,
                               .start = effect.start,
                               .end = effect.end,
                               .textScale = TEXT_SCALE_SMALL});
          else if (effect.typeString == "Oscillation")
            effects.push_back({.type = imgui::DialogueEffectSpan::OSCILLATE,
                               .start = effect.start,
                               .end = effect.end,
                               .frequency = OSCILLATION_FREQUENCY_DEFAULT,
                               .amplitude = OSCILLATION_AMPLITUDE_DEFAULT});
          else if (effect.typeString == "Shake")
            effects.push_back({.type = imgui::DialogueEffectSpan::SHAKE,
                               .start = effect.start,
                               .end = effect.end,
                               .frequency = SHAKE_FREQUENCY_DEFAULT,
                               .amplitude = SHAKE_AMPLITUDE_DEFAULT});
        }
        imgui::dialogue_text_draw(text, length, effects, glm::vec3{1.0f}, (float)ImGui::GetTime());
      };

      text();

      if (entry)
      {
        if (isFinished)
        {
          auto choices = dialogue.children_get(*entry, resource::xml::Schema::Element::CHOICE);
          if (!choices.empty())
          {
            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 - style.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 = imgui::row_widget_size_get((int)choices.size());

            for (auto index = 0; index < (int)choices.size(); ++index)
            {
              auto* branch = choices[index];
              auto label = branch->text.empty() ? "<choice>" : branch->text.c_str();
              ImGui::PushID(index);
              if (WIDGET_FX(ImGui::Button(label, buttonSize)))
                set(dialogue.dialogue_entry_get(*branch), character);

              ImGui::SetItemTooltip("%s", label);
              ImGui::PopID();
              if (index + 1 < (int)choices.size()) ImGui::SameLine();
            }

            if (isHovered && isSpace)
            {
              auto* next = dialogue.dialogue_entry_get(*choices.front());
              if (next) menuSchema.root()->soundSelect.play();
              set(next, character);
            }
          }
          else
          {
            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() * 2.0f) * (indicatorSize.x * 0.12f);
              auto center =
                  ImVec2(cursorPos.x + (indicatorSize.x * 0.5f) + offset, cursorPos.y + (indicatorSize.y * 0.5f));
              auto half = std::min(indicatorSize.x, indicatorSize.y) * 0.35f;
              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);

              if (isAdvance)
              {
                auto* next = dialogue.dialogue_entry_get(*entry);
                if (next) menuSchema.root()->soundSelect.play();
                set(next, character);
              }
            }
            else if (isAdvance)
            {
              isEnabled = false;
              entry = nullptr;
            }
          }
        }
        else
        {
          if (isAdvance)
          {
            auto target = imgui::utf8_char_count(entry->text);
            for (auto skippedIndex = index + 1; skippedIndex <= target; ++skippedIndex)
            {
              for (auto child : entry->children)
              {
                if (child < 0 || child >= (int)character.data.dialogue.elements.size()) continue;
                auto& effect = character.data.dialogue.elements[child];
                if (!dialogue_effect_is(effect) || effect.start != skippedIndex) continue;
                if (effect.typeString == "Delay")
                {
                  baseDelay = std::max(1, effect.delayTicks);
                  currentDelay = baseDelay;
                }
                else if (effect.typeString == "Animation" && !effect.animation.empty())
                {
                  character.play({.animation = effect.animation,
                                  .appendID = character.animation_append_id_get(),
                                  .mode = Entity::PLAY_FORCE});
                }
                else
                {
                  dialogue_effect_one_shot_apply(effect, canvas);
                }
              }
            }
            index = target;
            isFinished = true;
            character.isTalking = false;
          }
        }
      }

      ImGui::PopFont();
      ImGui::PopStyleVar();
    };
    ImGui::End();

    if (isEnabled && isFinished && entry && isTerminal)
    {
      if (time += ImGui::GetIO().DeltaTime; time > LIFETIME)
      {
        isEnabled = false;
        entry = nullptr;
      }
    }
  }

  bool Text::is_interruptible() const { return !entry || isTerminal; }
  bool Text::is_finished() const { return isFinished; }
  bool Text::is_terminal() const { return isTerminal; }
}
