#include "dialogue_text.hpp"

#include <algorithm>
#include <cmath>
#include <cstdint>

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

namespace game::util::imgui
{
  namespace
  {
    bool is_word_space(unsigned int codepoint) { return codepoint == ' ' || codepoint == '\t'; }

    float effect_text_scale_get(std::span<const DialogueEffectSpan> effects, int charIndex)
    {
      auto textScale = 1.0f;
      for (auto& effect : effects)
      {
        if (effect.start > charIndex || charIndex >= effect.end) continue;
        if (effect.type == DialogueEffectSpan::TEXT_SCALE) textScale = std::max(0.05f, effect.textScale);
      }
      return textScale;
    }

    float word_width_get(const char* text, const char* limit, int charIndex, std::span<const DialogueEffectSpan> effects)
    {
      auto width = 0.0f;
      for (auto* it = text; it < limit;)
      {
        unsigned int codepoint{};
        auto step = ImTextCharFromUtf8(&codepoint, it, limit);
        if (step <= 0 || codepoint == '\n' || is_word_space(codepoint)) break;
        auto* next = it + step;
        auto textScale = effect_text_scale_get(effects, charIndex);
        width += ImGui::CalcTextSize(it, next).x * textScale;
        it = next;
        ++charIndex;
      }
      return width;
    }

    float hash_to_unit(std::uint32_t value)
    {
      value ^= value >> 16;
      value *= 0x7feb352du;
      value ^= value >> 15;
      value *= 0x846ca68bu;
      value ^= value >> 16;
      return (float)(value & 0xffffu) / 65535.0f;
    }

  }

  int utf8_char_count(std::string_view text)
  {
    return ImTextCountCharsFromUtf8(text.data(), text.data() + text.size());
  }

  int utf8_byte_to_char_index(std::string_view text, int byteIndex)
  {
    auto* textStart = text.data();
    auto* textEnd = textStart + text.size();
    auto* byteEnd = textStart + std::clamp(byteIndex, 0, (int)text.size());
    auto count = 0;
    for (auto* it = textStart; it < textEnd && it < byteEnd;)
    {
      unsigned int codepoint{};
      auto step = ImTextCharFromUtf8(&codepoint, it, textEnd);
      if (step <= 0) break;
      it += step;
      ++count;
    }
    return count;
  }

  const char* utf8_advance_chars(const char* text, const char* end, int count)
  {
    auto* it = text;
    while (it < end && count > 0)
    {
      unsigned int codepoint{};
      auto step = ImTextCharFromUtf8(&codepoint, it, end);
      if (step <= 0) break;
      it += step;
      --count;
    }
    return it;
  }

  ImVec2 dialogue_text_draw(std::string_view text, int count, std::span<const DialogueEffectSpan> effects,
                            glm::vec3 defaultColor, float time)
  {
    static constexpr auto TAU = 6.28318530717958647692f;
    auto blockStart = ImGui::GetCursorScreenPos();
    auto blockMax = blockStart;
    auto* textStart = text.data();
    auto* textEnd = textStart + text.size();
    auto* textLimit = utf8_advance_chars(textStart, textEnd, count);
    auto lineStartX = ImGui::GetCursorScreenPos().x;
    auto wrapX = lineStartX + ImGui::GetContentRegionAvail().x;
    auto charIndex = 0;
    auto isLineStart = true;

    for (auto* it = textStart; it < textLimit;)
    {
      unsigned int codepoint{};
      auto step = ImTextCharFromUtf8(&codepoint, it, textLimit);
      if (step <= 0) break;

      auto* next = it + step;
      if (codepoint == '\n')
      {
        ImGui::NewLine();
        lineStartX = ImGui::GetCursorScreenPos().x;
        wrapX = lineStartX + ImGui::GetContentRegionAvail().x;
        blockMax.y = std::max(blockMax.y, ImGui::GetCursorScreenPos().y);
        it = next;
        ++charIndex;
        isLineStart = true;
        continue;
      }

      auto isSpace = is_word_space(codepoint);
      auto color = defaultColor;
      auto textScale = 1.0f;
      auto xOffset = 0.0f;
      auto yOffset = 0.0f;
      for (auto& effect : effects)
      {
        if (effect.start > charIndex || charIndex >= effect.end) continue;
        if (effect.type == DialogueEffectSpan::COLOR)
          color = effect.color;
        else if (effect.type == DialogueEffectSpan::TEXT_SCALE)
          textScale = std::max(0.05f, effect.textScale);
        else if (effect.type == DialogueEffectSpan::OSCILLATE)
        {
          auto frequency = effect.frequency > 0.0f ? effect.frequency : 1.0f;
          auto amplitude = effect.amplitude > 0.0f ? effect.amplitude : 2.0f;
          yOffset += std::sin((time * frequency + charIndex * 0.16f) * TAU) * amplitude;
        }
        else if (effect.type == DialogueEffectSpan::SHAKE)
        {
          auto frequency = effect.frequency > 0.0f ? effect.frequency : 12.0f;
          auto amplitude = effect.amplitude > 0.0f ? effect.amplitude : 1.0f;
          auto tick = (std::uint32_t)std::floor(time * frequency);
          auto seed = tick * 747796405u + (std::uint32_t)charIndex * 2891336453u;
          xOffset += (hash_to_unit(seed) * 2.0f - 1.0f) * amplitude;
          yOffset += (hash_to_unit(seed ^ 0xa511e9b3u) * 2.0f - 1.0f) * amplitude;
        }
      }

      auto* font = ImGui::GetFont();
      auto fontSize = ImGui::GetFontSize() * textScale;
      auto size = ImGui::CalcTextSize(it, next);
      size.x *= textScale;
      size.y *= textScale;
      auto cursorX = ImGui::GetCursorScreenPos().x;
      if (!isSpace && isLineStart == false &&
          (it == textStart || is_word_space((unsigned char)*(it - 1)) || *(it - 1) == '\n'))
      {
        auto wordWidth = word_width_get(it, textLimit, charIndex, effects);
        if (wordWidth > 0.0f && cursorX + wordWidth > wrapX && lineStartX + wordWidth <= wrapX)
        {
          ImGui::NewLine();
          lineStartX = ImGui::GetCursorScreenPos().x;
          wrapX = lineStartX + ImGui::GetContentRegionAvail().x;
          cursorX = ImGui::GetCursorScreenPos().x;
          isLineStart = true;
        }
      }
      if (cursorX + size.x > wrapX)
      {
        ImGui::NewLine();
        lineStartX = ImGui::GetCursorScreenPos().x;
        wrapX = lineStartX + ImGui::GetContentRegionAvail().x;
        cursorX = ImGui::GetCursorScreenPos().x;
        if (isSpace)
        {
          it = next;
          ++charIndex;
          isLineStart = true;
          continue;
        }
      }

      auto cursor = ImGui::GetCursorScreenPos();
      auto drawCursor = ImVec2(cursor.x + xOffset, cursor.y + yOffset);
      auto textColor = ImGui::GetColorU32(ImVec4(color.r, color.g, color.b, 1.0f));
      ImGui::GetWindowDrawList()->AddText(font, fontSize, drawCursor, textColor, it, next);
      ImGui::Dummy(size);
      blockMax.x = std::max(blockMax.x, drawCursor.x + size.x);
      blockMax.y = std::max(blockMax.y, drawCursor.y + size.y);
      ImGui::SetCursorScreenPos(ImVec2(cursor.x + size.x, cursor.y));

      it = next;
      ++charIndex;
      if (!isSpace) isLineStart = false;
      if (it < textLimit) ImGui::SameLine(0.0f, 0.0f);
    }

    auto result = ImVec2(blockMax.x - blockStart.x, blockMax.y - blockStart.y);
    auto reservedHeight = std::max(result.y, ImGui::GetTextLineHeight());
    ImGui::SetCursorScreenPos(ImVec2(blockStart.x, blockStart.y + reservedHeight));
    ImGui::Dummy(ImVec2(1.0f, 1.0f));
    return result;
  }
}
