#include "world.hpp"

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

#include <algorithm>
#include <cmath>

using namespace game::util;

namespace game::state::play
{
  constexpr auto ZOOM_LADDER_EPSILON = 0.01f;
  constexpr auto ZOOM_WHEEL_ZERO = 0.0f;

  float zoom_ladder_level_get(int index)
  {
    auto clampedIndex = std::clamp(index, 0, World::ZOOM_LADDER_LEVEL_COUNT - 1);
    auto exponent = (float)clampedIndex / (float)(World::ZOOM_LADDER_LEVEL_COUNT - 1);
    return World::ZOOM_MIN * (float)std::pow(World::ZOOM_MAX / World::ZOOM_MIN, exponent);
  }

  int zoom_ladder_next_index_get(float zoom, int direction)
  {
    if (direction > 0)
    {
      for (int index = 0; index < World::ZOOM_LADDER_LEVEL_COUNT; ++index)
        if (zoom < zoom_ladder_level_get(index) - ZOOM_LADDER_EPSILON) return index;
      return World::ZOOM_LADDER_LEVEL_COUNT - 1;
    }

    for (int index = World::ZOOM_LADDER_LEVEL_COUNT - 1; index >= 0; --index)
      if (zoom > zoom_ladder_level_get(index) + ZOOM_LADDER_EPSILON) return index;
    return 0;
  }

  float zoom_ladder_zoom_get(float zoom, float wheel)
  {
    auto direction = wheel > ZOOM_WHEEL_ZERO ? 1 : -1;
    auto stepCount = std::max(1, (int)std::ceil(std::abs(wheel)));
    auto index = zoom_ladder_next_index_get(zoom, direction);
    index = std::clamp(index + ((stepCount - 1) * direction), 0, World::ZOOM_LADDER_LEVEL_COUNT - 1);
    return zoom_ladder_level_get(index);
  }

  void World::set(Entity& character, Canvas& canvas, Focus focus)
  {
    character.stage = character.stage_get();
    character_focus(character, canvas, focus);
  }

  void World::update(Entity& character, Entity& cursor, Canvas& canvas, Focus focus)
  {
    auto& cursorSchema = character.data.cursorSchema;
    auto* cursorRoot = cursorSchema.root();
    auto& pan = canvas.pan;
    auto& zoom = canvas.zoom;
    auto& io = ImGui::GetIO();
    auto isMouseMiddleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
    auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
    auto isCtrlDown = ImGui::IsKeyDown(ImGuiMod_Ctrl);
    auto panMultiplier = ZOOM_BASE / zoom;

    if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
    {
      if ((isMouseMiddleDown) || (isMouseLeftDown && isCtrlDown))
      {
        if (cursorRoot)
          if (auto animation = cursorRoot->animationPan.get()) cursor.play({*animation});
        pan -= imgui::to_vec2(io.MouseDelta) * panMultiplier;
      }

      if (io.MouseWheel != 0)
      {
        auto viewport = ImGui::GetMainViewport();
        auto mousePos = io.MousePos;
        auto cursorPos = imgui::to_vec2(ImVec2(mousePos.x - viewport->Pos.x, mousePos.y - viewport->Pos.y));
        auto zoomBefore = zoom;
        auto zoomFactorBefore = math::to_unit(zoomBefore);
        auto cursorWorld = pan + (cursorPos / zoomFactorBefore);

        if (cursorRoot)
          if (auto animation = cursorRoot->animationZoom.get()) cursor.play({*animation});

        zoom = zoom_ladder_zoom_get(zoom, io.MouseWheel);

        auto zoomFactorAfter = math::to_unit(zoom);
        pan = cursorWorld - (cursorPos / zoomFactorAfter);
      }
    }

    zoom = glm::clamp(ZOOM_MIN, zoom, ZOOM_MAX);

    if (ImGui::IsKeyPressed(ImGuiKey_Home)) character_focus(character, canvas, focus);
  }

  void World::character_focus(Entity& character, Canvas& canvas, Focus focus)
  {
    static constexpr float MENU_WIDTH_MULTIPLIER = 0.30f;
    static constexpr float TOOLS_WIDTH_MULTIPLIER = 0.10f;
    static constexpr float INFO_HEIGHT_MULTIPLIER = 4.0f;
    static constexpr float PADDING = 100.0f;

    auto rect = character.rect();

    if (!std::isfinite(rect.x) || !std::isfinite(rect.y) || !std::isfinite(rect.z) || !std::isfinite(rect.w) ||
        rect.z <= 0.0f || rect.w <= 0.0f)
      return;

    rect = {rect.x - PADDING * 0.5f, rect.y - PADDING * 0.5f, rect.z + PADDING, rect.w + PADDING};

    auto infoHeightPixels =
        ImGui::GetTextLineHeightWithSpacing() * INFO_HEIGHT_MULTIPLIER + ImGui::GetStyle().WindowPadding.y * 2.0f;
    auto usableHeightPixels = std::max(1.0f, (float)canvas.size.y - infoHeightPixels);

    auto zoomFactor = std::min((float)canvas.size.x / rect.z, usableHeightPixels / rect.w);
    canvas.zoom = glm::clamp(ZOOM_MIN, math::to_percent(zoomFactor), ZOOM_MAX);
    zoomFactor = math::to_unit(canvas.zoom);

    auto rectCenter = glm::vec2(rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f);
    auto viewSizeWorld = glm::vec2(canvas.size) / zoomFactor;
    auto infoHeightWorld = infoHeightPixels / zoomFactor;
    canvas.pan = rectCenter - vec2(viewSizeWorld.x * 0.5f, (viewSizeWorld.y + infoHeightWorld) * 0.5f);
    auto menuWidthWorld = (canvas.size.x * MENU_WIDTH_MULTIPLIER) / zoomFactor;
    auto toolsWidthWorld = (canvas.size.x * TOOLS_WIDTH_MULTIPLIER) / zoomFactor;

    if (focus == Focus::MENU || focus == Focus::MENU_TOOLS) canvas.pan.x += menuWidthWorld * 0.5f;
    if (focus == Focus::TOOLS || focus == Focus::MENU_TOOLS) canvas.pan.x -= toolsWidthWorld * 0.5f;

    auto panMin = glm::vec2(0.0f, 0.0f);
    auto panMax = glm::max(glm::vec2(0.0f), SIZE - viewSizeWorld);
    canvas.pan = glm::clamp(panMin, canvas.pan, panMax);
  }
}
