#include "item_manager.hpp"

#include <cmath>
#include <string>
#include <utility>

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

#include <imgui.h>

using namespace game::resource;
using namespace game::util;
using namespace glm;

namespace game::state::play
{
  namespace
  {
    int durability_animation_index_get(const resource::xml::Schema& schema, const resource::xml::Anm2& anm2,
                                       int durability, int durabilityMax)
    {
      if (durability >= durabilityMax) return -1;

      auto animationName = schema.root()->animationChew + std::to_string(std::max(0, durability));
      return anm2.animationMap.contains(animationName) ? anm2.animationMap.at(animationName) : -1;
    }

    int durability_animation_index_get(const resource::xml::Schema& schema, const Entity& entity, int durability,
                                       int durabilityMax)
    {
      if (durability >= durabilityMax) return -1;

      auto animationName = schema.root()->animationChew + std::to_string(std::max(0, durability));
      return entity.animationMap.contains(animationName) ? entity.animationMap.at(animationName) : -1;
    }

    bool is_finite(glm::vec4 value)
    {
      return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w) &&
             value.z > 0.0f && value.w > 0.0f;
    }

    glm::vec2 item_spawn_position_get(Entity& character, const glm::vec4& bounds)
    {
      auto rect = character.rect();
      if (!is_finite(rect)) rect = bounds;
      auto center = glm::vec2(rect.x + rect.z * 0.5f, rect.y + rect.w);
      auto radius = rect.w * 0.5f;
      auto angle = glm::pi<float>() + math::random_max(glm::pi<float>());
      auto position = center + glm::vec2(std::cos(angle), std::sin(angle)) * radius;

      return {glm::clamp(position.x, bounds.x, bounds.x + bounds.z),
              glm::clamp(position.y, bounds.y, bounds.y + bounds.w)};
    }
  }

  void ItemManager::update(Entity& character, Entity& cursor, AreaManager& areaManager, Text& text,
                           const glm::vec4& bounds, Canvas& canvas)
  {
    static constexpr float ROTATION_MAX = 90.0f;
    static constexpr float ROTATION_RETURN_SPEED = 120.0f;
    static constexpr float ROTATION_GROUND_DAMPING = 0.85f;
    static constexpr float ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER = 0.1f;
    static constexpr float THROW_THRESHOLD = 10.0f;

    auto& schema = character.data.itemSchema;
    auto& cursorSchema = character.data.cursorSchema;
    auto* cursorRoot = cursorSchema.root();
    auto areaFallback = resource::xml::Schema::AreaEntry{.gravity = 0.95f, .friction = 0.80f, .airResistance = 0.975f};
    auto* areaSelected = areaManager.get(character);
    auto& area = areaSelected ? *areaSelected : areaFallback;
    auto& friction = area.friction;
    auto& airResistance = area.airResistance;
    auto& dialogue = character.data.dialogue;
    auto isOverCapacity = character.is_over_capacity();
    auto dialogue_pool_entry_get = [&](resource::xml::Schema::Element::Type type)
    {
      auto* reference = dialogue.get(type);
      return reference ? dialogue.dialogue_pool_entry_get(*reference) : nullptr;
    };
    auto particles_queue =
        [&](resource::xml::Schema& particleSchema, resource::xml::Schema::Element::Type type, glm::vec2 position)
    {
      auto* root = particleSchema.root();
      auto* actions = root ? particleSchema.child_get(*root, resource::xml::Schema::Element::ACTIONS) : nullptr;
      auto* action = actions ? particleSchema.child_get(*actions, type) : nullptr;
      if (!action) return;
      for (auto* particle : particleSchema.children_get(*action, resource::xml::Schema::Element::PARTICLE))
        if (particle && !particle->typeString.empty())
          particles.push_back({.label = particle->typeString, .position = position});
    };

    auto cursorPosition =
        cursor.entityType == AUTOCURSOR ? cursor.position : canvas.screen_position_convert(cursor.position);
    auto cursorDelta = cursorPosition - cursorPositionPrevious;

    auto isImguiCaptureMouse = ImGui::GetIO().WantCaptureMouse;

    auto isMouseLeftClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
    auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
    auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
    auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
    auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
    auto isAutomation = this->isAutomation;

    auto& io = ImGui::GetIO();

    if (inputOverride)
    {
      isImguiCaptureMouse = inputOverride->isImguiCaptureMouse;
      isMouseLeftClicked = inputOverride->isMouseLeftClicked;
      isMouseLeftDown = inputOverride->isMouseLeftDown;
      isMouseLeftReleased = inputOverride->isMouseLeftReleased;
      isMouseRightClicked = inputOverride->isMouseRightClicked;
      isMouseRightDown = inputOverride->isMouseRightDown;
      isAutomation = isAutomation || inputOverride->isAutomation;
      inputOverride.reset();
    }

    if (isDisabled)
    {
      if (heldItemIndex != -1)
      {
        heldItemIndex = -1;
        isJustItemHeldStopped = true;
      }
      isItemHovered = false;
      isItemHeld = false;
      isJustItemHeld = false;
      isMouseLeftClicked = false;
      isMouseLeftDown = false;
      isMouseLeftReleased = false;
      isMouseRightClicked = false;
      isMouseRightDown = false;
      isImguiCaptureMouse = true;
    }

    if (isJustItemHoveredStopped)
    {
      cursor.play({cursor.defaultAnimation});
      isJustItemHoveredStopped = false;
    }

    if (isJustItemHeldStopped || isJustItemThrown)
    {
      cursor.play({cursor.defaultAnimation});
      if (!isJustItemThrown && !character.is_animation_request())
        character.play({.animation = character.idle_animation_get(), .appendID = character.animation_append_id_get()});
      isJustItemHeldStopped = false;
      isJustItemThrown = false;
    }

    isItemHoveredPrevious = isItemHovered;
    isItemHovered = false;
    particles.clear();
    if (isItemHovered != isItemHoveredPrevious && !isItemHovered) isJustItemHoveredStopped = true;

    for (auto& id : queuedItemIDs)
    {
      auto position = item_spawn_position_get(character, bounds);

      auto& itemSchema = character.data.itemSchema;
      auto& item = itemSchema.items.at(id);
      auto& anm2 = itemSchema.anm2s.at(id);
      auto durabilityMax = item.isDurability ? item.durability : itemSchema.durability;
      auto animationIndex = durability_animation_index_get(itemSchema, anm2, 0, durabilityMax);
      items.emplace_back(anm2, position, id, 0, animationIndex);
      particles_queue(itemSchema, resource::xml::Schema::Element::SUMMON, position);
    }
    queuedItemIDs.clear();

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

    if (auto heldItem = vector::find(items, heldItemIndex))
    {
      auto& item = schema.items[heldItem->schemaID];
      auto& rotationOverride = heldItem->overrides[heldItem->rotationOverrideID];
      auto& rotation = *rotationOverride.frame.rotation;

      auto delta = cursorPositionPrevious - cursorPosition;
      heldItem->position = cursorPosition;
      heldItem->velocity = vec2();

      heldItem->angularVelocity -= delta.x * ROTATION_HOLD_DELTA_ANGULAR_VELOCITY_MULTIPLIER;
      heldItem->angularVelocity *= friction;
      rotation *= friction;
      rotation = glm::clamp(-ROTATION_MAX, rotation, ROTATION_MAX);

      if (schema.categories[item.categoryID].isEdible)
      {
        auto durabilityMax = item.isDurability ? item.durability : schema.durability;
        auto caloriesPerBite = item.isCalories && durabilityMax > 0 ? item.calories / durabilityMax : 0;
        auto isCanEat = character.calories + caloriesPerBite <= character.max_capacity();

        if (isJustItemHeld)
        {
          if (!isAutomation)
          {
            if (isCanEat)
              text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::FEED_FULL
                                                              : resource::xml::Schema::Element::FEED),
                       character);
            else if (caloriesPerBite > character.capacity)
              text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::LOW_CAPACITY), character);
            else
              text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::FULL), character);
          }
          isJustItemHeld = false;
        }

        for (auto* eatArea : character.data.eat_areas_get())
        {
          heldItem = vector::find(items, heldItemIndex);
          if (!heldItem) break;

          auto nullID = character.data.null_id_get(eatArea->null);
          auto rect = character.null_frame_rect(nullID);
          if (!is_finite(rect))
          {
            auto animationIndex =
                character.animation_index_get(eatArea->animation + character.animation_append_id_get());
            rect = character.null_frame_rect(nullID, animationIndex, 0.0f);
          }

          if (isCanEat && math::is_point_in_rectf(rect, heldItem->position))
          {
            character.play({.animation = eatArea->animation,
                            .appendID = character.animation_append_id_get(),
                            .speedMultiplier = character.eatSpeed});

            if (character.playedEventID == character.data.event_id_get(eatArea->event))
            {
              heldItem->durability++;
              character.consume_played_event();

              character.calories += caloriesPerBite;
              character.totalCaloriesConsumed += caloriesPerBite;

              if (item.isCapacityBonus)
              {
                character.capacity += item.capacityBonus / durabilityMax;
                character.capacity = glm::clamp(character.capacity, (float)character.data.root()->capacityMinCalories,
                                                (float)character.data.root()->capacityMaxCalories);
              }
              if (item.isEatSpeedBonus)
              {
                character.eatSpeed += item.eatSpeedBonus / durabilityMax;
                character.eatSpeed = glm::clamp(character.eatSpeed, (float)character.data.root()->eatSpeedMinMultiplier,
                                                (float)character.data.root()->eatSpeedMaxMultiplier);
              }
              if (item.isDigestionBonus)
              {
                character.digestionRate += item.digestionBonus / durabilityMax;
                character.digestionRate =
                    glm::clamp(character.digestionRate, (float)character.data.root()->digestionRateMin,
                               (float)character.data.root()->digestionRateMax);
              }

              if (heldItem->durability >= durabilityMax)
              {
                isQueueFinishFood = true;
                character.totalFoodItemsEaten++;
                queuedRemoveItemIndex = heldItemIndex;
                heldItemIndex = -1;
              }
              else
              {
                auto animationIndex =
                    durability_animation_index_get(schema, *heldItem, heldItem->durability, durabilityMax);
                heldItem->play({.index = animationIndex, .mode = Entity::SET});
              }
            }
          }

          if (isMouseLeftReleased)
          {
            if (fabs(delta.x) >= THROW_THRESHOLD || fabs(delta.y) >= THROW_THRESHOLD)
            {
              if (cursorRoot) cursorRoot->soundThrow.play();
              if (!isAutomation) text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::THROW), character);
              isJustItemThrown = true;
            }
            else if (cursorRoot)
              cursorRoot->soundRelease.play();

            heldItem->velocity -= delta;
            heldItemIndex = -1;
            isJustItemHeldStopped = true;
          }

          // Food stolen
          if (auto animation = character.animation_get(eatArea->animation + character.animation_append_id_get());
              animation && character.is_playing(animation->name))
          {
            if (!math::is_point_in_rectf(rect, heldItem->position))
              if (!isAutomation)
                text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::FOOD_TAKEN_FULL
                                                                : resource::xml::Schema::Element::FOOD_TAKEN),
                         character);
          }
        }
      }
    }

    if (auto animation = character.animation_get(); character.time >= animation->frameNum && isQueueFinishFood)
    {
      if (!isAutomation)
        text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::EAT_FULL
                                                        : resource::xml::Schema::Element::EAT),
                 character);
      isQueueFinishFood = false;
    }

    if (queuedRemoveItemIndex > -1)
    {
      items.erase(items.begin() + queuedRemoveItemIndex);
      queuedRemoveItemIndex = -1;
    }

    int heldItemMoveIndex = -1;
    for (int i = 0; i < (int)items.size(); i++)
    {
      auto& item = items[i];
      auto& schemaItem = schema.items[item.schemaID];
      auto& rotationOverride = item.overrides[item.rotationOverrideID];
      auto& rotation = *rotationOverride.frame.rotation;
      auto& gravity = schemaItem.isGravity ? schemaItem.gravity : area.gravity;

      item.update();

      if (math::is_point_in_rectf(item.rect(), cursorPosition) && !isImguiCaptureMouse)
      {
        isItemHovered = true;
        if (cursorRoot)
          if (auto animation = cursorRoot->animationHover.get())
            cursor.play({.animation = *animation, .transition = Entity::Transition::IF_IDLE});
        cursor.cursorState = Entity::HOVER;

        if (isMouseLeftClicked)
        {
          if (cursorRoot) cursorRoot->soundGrab.play();
          particles_queue(character.data.cursorSchema, resource::xml::Schema::Element::GRAB, cursorPosition);
          isJustItemHeld = true;
        }

        if (isMouseLeftDown)
        {
          isItemHeld = true;
          if (cursorRoot)
            if (auto animation = cursorRoot->animationGrab.get()) cursor.play({*animation});
          cursor.cursorState = Entity::ACTION;
          heldItemIndex = i;
          heldItemMoveIndex = i;
        }

        if (isMouseRightClicked)
        {
          if (item.durability > 0)
          {
            schema.root()->soundDispose.play();
            particles_queue(schema, resource::xml::Schema::Element::DISPOSE, item.position);
          }
          else
          {
            schema.root()->soundReturn.play();
            particles_queue(schema, resource::xml::Schema::Element::RETURN_, item.position);
            returnItemIDs.emplace_back(item.schemaID);
          }

          if (heldItemIndex == i) heldItemIndex = -1;
          if (heldItemMoveIndex == i) heldItemMoveIndex = -1;
          if (heldItemIndex > i) heldItemIndex--;
          if (heldItemMoveIndex > i) heldItemMoveIndex--;
          items.erase(items.begin() + i);
          continue;
        }
      }

      if (i != heldItemIndex)
      {

        if (item.position.x <= bounds.x || item.position.x >= bounds.z)
        {
          if (item.position.x <= bounds.x) item.position.x = bounds.x + 1.0f;
          if (item.position.x >= bounds.z) item.position.x = bounds.z - 1.0f;

          item.velocity.x *= friction;
          item.velocity.x = -item.velocity.x;
          item.angularVelocity *= friction;
          item.angularVelocity = -item.angularVelocity;
          rotation = -rotation;
          schema.root()->soundBounce.play();
        }

        if (item.position.y <= bounds.y || item.position.y >= bounds.w)
        {

          if (item.position.y >= bounds.w && item.velocity.y <= gravity)
          {
            item.position.y = bounds.w;
            item.velocity.y = 0;
            item.velocity.x *= friction;

            rotation = std::fmod(rotation, 360.0f);
            if (rotation < 0.0f) rotation += 360.0f;
            if (rotation > 180.0f) rotation -= 360.0f;

            auto returnStep = ROTATION_RETURN_SPEED * io.DeltaTime;
            if (std::abs(rotation) <= returnStep)
              rotation = 0.0f;
            else
              rotation += (rotation > 0.0f) ? -returnStep : returnStep;

            item.angularVelocity *= ROTATION_GROUND_DAMPING;
          }
          else
          {
            item.velocity.y = -item.velocity.y;
            item.angularVelocity *= friction;
            schema.root()->soundBounce.play();
          }

          item.velocity.y *= friction;
        }

        item.velocity.x *= airResistance;
        item.velocity.y += gravity;
      }

      item.position.x = glm::clamp(bounds.x, item.position.x, bounds.z);
      item.position.y = glm::clamp(bounds.y, item.position.y, bounds.w);
    }

    if (heldItemMoveIndex != -1 && heldItemMoveIndex < (int)items.size() - 1)
    {
      auto heldItem = std::move(items[heldItemMoveIndex]);
      items.erase(items.begin() + heldItemMoveIndex);
      items.push_back(std::move(heldItem));
      heldItemIndex = (int)items.size() - 1;
    }

    cursorPositionPrevious = cursorPosition;
    cursorDeltaPrevious = cursorDelta;
  }
}
