#include "autofeed.hpp"

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

#include <cmath>
#include <imgui.h>

using namespace game::resource::xml;
using namespace game::util;

namespace game::state::play
{
  namespace
  {
    constexpr float CURSOR_SPEED = 9.0f;
    constexpr float DIGEST_CURSOR_SPEED = 3.0f;
    constexpr float CURSOR_SPEED_REFERENCE_AREA = 512.0f * 512.0f;
    constexpr float CURSOR_SPEED_SCALE_MIN = 0.5f;
    constexpr float CURSOR_SPEED_SCALE_MAX = 2.0f;
    constexpr float CALORIE_EPSILON = 0.001f;

    glm::vec2 rect_center(glm::vec4 rect) { return {rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f}; }

    bool is_finite(glm::vec2 value) { return std::isfinite(value.x) && std::isfinite(value.y); }

    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;
    }

    bool move_toward(Entity& cursor, glm::vec2 target, float speed)
    {
      if (!is_finite(cursor.position)) cursor.position = {};
      if (!is_finite(target)) return false;

      auto delta = target - cursor.position;
      auto distance = glm::length(delta);
      if (!std::isfinite(distance)) return false;
      if (distance <= speed || distance <= 0.001f)
      {
        cursor.position = target;
        return true;
      }

      cursor.position += delta / distance * speed;
      return false;
    }

    float cursor_speed_scale_get(Entity& character)
    {
      auto rect = character.rect();
      if (!is_finite(rect)) return 1.0f;

      auto area = rect.z * rect.w;
      if (!std::isfinite(area) || area <= 0.0f) return 1.0f;

      return glm::clamp(std::sqrt(area / CURSOR_SPEED_REFERENCE_AREA), CURSOR_SPEED_SCALE_MIN, CURSOR_SPEED_SCALE_MAX);
    }

    bool is_food(const Schema& schema, const Schema::ItemEntry& item)
    {
      return item.categoryID >= 0 && item.categoryID < (int)schema.categories.size() &&
             schema.categories[item.categoryID].isEdible;
    }

    bool is_negative_effect(const Schema::ItemEntry& item)
    {
      return (item.isCapacityBonus && item.capacityBonus < 0.0f) ||
             (item.isEatSpeedBonus && item.eatSpeedBonus < 0.0f) ||
             (item.isDigestionBonus && item.digestionBonus < 0.0f);
    }

    bool is_autofeed_food(const Schema& schema, const Schema::ItemEntry& item)
    {
      return is_food(schema, item) && !is_negative_effect(item);
    }

    int durability_max_get(const Schema& schema, const Schema::ItemEntry& item)
    {
      return item.isDurability ? item.durability : schema.durability;
    }

    float calories_per_bite_get(const Schema& schema, const Schema::ItemEntry& item)
    {
      auto durabilityMax = durability_max_get(schema, item);
      return item.isCalories && durabilityMax > 0 ? item.calories / (float)durabilityMax : 0.0f;
    }

    bool can_eat(const Entity& character, const Schema& schema, const Schema::ItemEntry& item)
    {
      auto caloriesPerBite = calories_per_bite_get(schema, item);
      return caloriesPerBite > 0.0f && character.calories + caloriesPerBite <= character.max_capacity();
    }

    struct FoodScore
    {
      float calories{};
      int bites{};
      float caloriesPerBite{};
      bool isValid{};
    };

    FoodScore food_score_get(const Entity& character, const Schema& schema, const Schema::ItemEntry& item,
                             int durability)
    {
      if (!is_autofeed_food(schema, item)) return {};

      auto caloriesPerBite = calories_per_bite_get(schema, item);
      auto remainingCapacity = character.max_capacity() - character.calories;
      auto durabilityMax = durability_max_get(schema, item);
      auto remainingBites = glm::max(0, durabilityMax - durability);
      if (caloriesPerBite <= 0.0f || remainingCapacity < caloriesPerBite || remainingBites <= 0) return {};

      auto bites = glm::clamp((int)std::floor(remainingCapacity / caloriesPerBite), 0, remainingBites);
      auto calories = (float)bites * caloriesPerBite;
      return {.calories = calories, .bites = bites, .caloriesPerBite = caloriesPerBite, .isValid = bites > 0};
    }

    bool is_better_food_score(const FoodScore& score, const FoodScore& best)
    {
      if (!score.isValid) return false;
      if (!best.isValid) return true;
      if (score.calories != best.calories) return score.calories > best.calories;
      if (score.bites != best.bites) return score.bites < best.bites;
      return score.caloriesPerBite > best.caloriesPerBite;
    }

    bool can_eat_anything(Entity& character, ItemManager& itemManager, menu::Inventory& inventory)
    {
      auto& schema = character.data.itemSchema;
      for (auto& entity : itemManager.items)
      {
        if (entity.schemaID < 0 || entity.schemaID >= (int)schema.items.size()) continue;
        auto& item = schema.items[entity.schemaID];
        if (is_autofeed_food(schema, item) && can_eat(character, schema, item)) return true;
      }

      for (auto& [id, quantity] : inventory.values)
      {
        if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
        auto& item = schema.items[id];
        if (is_autofeed_food(schema, item) && can_eat(character, schema, item)) return true;
      }

      return false;
    }

    int best_inventory_food_get(Entity& character, menu::Inventory& inventory)
    {
      auto& schema = character.data.itemSchema;
      auto bestID = -1;
      auto bestScore = FoodScore{};

      for (auto& [id, quantity] : inventory.values)
      {
        if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
        auto& item = schema.items[id];
        auto score = food_score_get(character, schema, item, 0);

        if (is_better_food_score(score, bestScore))
        {
          bestScore = score;
          bestID = id;
        }
      }

      return bestID;
    }

    bool is_inventory_food(Entity& character, menu::Inventory& inventory)
    {
      auto& schema = character.data.itemSchema;
      for (auto& [id, quantity] : inventory.values)
      {
        if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
        if (is_autofeed_food(schema, schema.items[id])) return true;
      }

      return false;
    }

    bool is_world_food(Entity& character, ItemManager& itemManager)
    {
      auto& schema = character.data.itemSchema;
      for (auto& entity : itemManager.items)
      {
        if (entity.schemaID < 0 || entity.schemaID >= (int)schema.items.size()) continue;
        if (is_autofeed_food(schema, schema.items[entity.schemaID])) return true;
      }

      return false;
    }

    bool is_food(Entity& character, menu::Inventory& inventory, ItemManager& itemManager)
    {
      return character.calories > CALORIE_EPSILON || is_inventory_food(character, inventory) ||
             is_world_food(character, itemManager);
    }

    int best_world_food_get(Entity& character, ItemManager& itemManager, bool requireCanEat)
    {
      auto& schema = character.data.itemSchema;
      auto bestIndex = -1;
      auto bestScore = FoodScore{};

      for (int i = 0; i < (int)itemManager.items.size(); i++)
      {
        auto& entity = itemManager.items[i];
        if (entity.schemaID < 0 || entity.schemaID >= (int)schema.items.size()) continue;

        auto& item = schema.items[entity.schemaID];
        if (!is_autofeed_food(schema, item)) continue;
        if (requireCanEat && !can_eat(character, schema, item)) continue;

        auto score = food_score_get(character, schema, item, entity.durability);
        if (is_better_food_score(score, bestScore))
        {
          bestScore = score;
          bestIndex = i;
        }
      }

      return bestIndex;
    }

    bool eat_rect_get(Entity& character, glm::vec4& rect)
    {
      for (auto* eatArea : character.data.eat_areas_get())
      {
        if (!eatArea) continue;
        auto nullID = character.data.null_id_get(eatArea->null);
        if (nullID == -1) continue;

        rect = character.null_frame_rect(nullID);
        if (is_finite(rect)) return true;

        auto animationIndex = character.animation_index_get(eatArea->animation + character.animation_append_id_get());
        rect = character.null_frame_rect(nullID, animationIndex, 0.0f);
        if (is_finite(rect)) return true;
      }

      return false;
    }

    const Schema::Element* digestion_area_get(Entity& character)
    {
      for (auto* interactArea : character.data.interact_areas_get())
      {
        if (!interactArea) continue;
        if (interactArea->digestionBonusOnClick <= 0.0f && interactArea->digestionBonusOnHover <= 0.0f) continue;
        if (character.data.null_id_get(interactArea->null) == -1) continue;
        return interactArea;
      }

      return nullptr;
    }

    void state_reset(Entity& cursor, bool& isItemMouseHeld, bool& isDigestMouseHeld, int& digestClickCooldown)
    {
      isItemMouseHeld = false;
      isDigestMouseHeld = false;
      digestClickCooldown = 0;
      if (!is_finite(cursor.position)) cursor.position = {};
    }
  }

  void Autofeed::update(Resources& resources, Entity& character, Entity& cursor, menu::Inventory& inventory,
                        ItemManager& itemManager, CharacterManager& characterManager, Canvas& canvas)
  {
    isActive = false;
    cursor.isVisible = false;

    if (!isEnabled)
    {
      state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
      return;
    }

    if (!is_food(character, inventory, itemManager))
    {
      state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
      return;
    }

    if (character.isStageUp || character.isJustStageUp || character.isJustStageFinal)
    {
      state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
      return;
    }

    if (cursor.position == glm::vec2{} && is_finite(character.position)) cursor.position = character.position;

    auto& schema = character.data.itemSchema;
    if (inventory.upgrade_all_possible(schema))
    {
      state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
      return;
    }

    auto itemInput = ItemManager::Input{};
    itemInput.isAutomation = true;
    auto characterInput = CharacterManager::Input{};
    auto speedScale = cursor_speed_scale_get(character);
    auto cursorSpeed = CURSOR_SPEED * speedScale;
    auto digestCursorSpeed = DIGEST_CURSOR_SPEED * speedScale;
    auto eatRect = glm::vec4{};
    auto isEatTarget = eat_rect_get(character, eatRect);

    if (itemManager.heldItemIndex != -1 && itemManager.heldItemIndex < (int)itemManager.items.size())
    {
      auto& heldItem = itemManager.items[itemManager.heldItemIndex];
      if (heldItem.schemaID < 0 || heldItem.schemaID >= (int)schema.items.size())
      {
        isActive = false;
        cursor.isVisible = false;
        state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
        return;
      }
      auto& schemaItem = schema.items[heldItem.schemaID];

      if (isEatTarget && can_eat(character, schema, schemaItem))
      {
        isActive = true;
        cursor.isVisible = true;
        move_toward(cursor, rect_center(eatRect), cursorSpeed);

        itemInput.isMouseLeftDown = true;
        itemManager.inputOverride = itemInput;
        isItemMouseHeld = true;
        isDigestMouseHeld = false;
        return;
      }

      itemInput.isMouseLeftReleased = true;
      itemManager.inputOverride = itemInput;
      isActive = true;
      cursor.isVisible = true;
      isItemMouseHeld = false;
      return;
    }

    auto bestWorldFood = isEatTarget ? best_world_food_get(character, itemManager, true) : -1;

    if (bestWorldFood != -1)
    {
      auto& item = itemManager.items[bestWorldFood];
      auto rect = item.rect();
      if (!is_finite(rect))
      {
        state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
        return;
      }

      isActive = true;
      cursor.isVisible = true;
      move_toward(cursor, rect_center(rect), cursorSpeed);

      auto isInItemRect = math::is_point_in_rectf(rect, cursor.position);

      itemInput.isMouseLeftClicked = isInItemRect && !isItemMouseHeld;
      itemInput.isMouseLeftDown = isInItemRect || isItemMouseHeld;
      itemManager.inputOverride = itemInput;
      if (isInItemRect) isItemMouseHeld = true;
      isDigestMouseHeld = false;
      return;
    }

    auto isInventoryFood = is_inventory_food(character, inventory);
    auto isWorldFood = is_world_food(character, itemManager);
    if (character.calories <= CALORIE_EPSILON && !isInventoryFood && !isWorldFood)
    {
      state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
      return;
    }

    auto bestInventoryFood = isEatTarget ? best_inventory_food_get(character, inventory) : -1;
    if (bestInventoryFood != -1 && !isWorldFood && (int)itemManager.items.size() < ItemManager::LIMIT)
    {
      itemManager.queuedItemIDs.emplace_back(bestInventoryFood);
      inventory.values[bestInventoryFood]--;
      schema.root()->soundSummon.play();
      cursor.isVisible = false;
      return;
    }

    if ((isWorldFood && bestWorldFood == -1) || !can_eat_anything(character, itemManager, inventory))
    {
      auto* interactArea = digestion_area_get(character);
      if (!interactArea)
      {
        state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
        return;
      }

      auto rect = character.null_frame_rect(character.data.null_id_get(interactArea->null));
      if (!is_finite(rect))
      {
        state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
        return;
      }
      auto radius = glm::max(1.0f, glm::min(rect.z, rect.w) * 0.25f);
      digestAngle += 0.08f;
      auto targetWorld = rect_center(rect) + glm::vec2(std::cos(digestAngle), std::sin(digestAngle)) * radius;
      auto isAtTarget = move_toward(cursor, targetWorld, digestCursorSpeed);
      isActive = true;
      cursor.isVisible = true;

      cursor.interactTypeID = character.data.interact_type_id_get(interactArea->typeString);
      if (interactArea->digestionBonusOnHover > 0.0f)
      {
        auto isInDigestRect = math::is_point_in_rectf(rect, cursor.position);
        characterInput.isMouseLeftClick = isInDigestRect && !isDigestMouseHeld;
        characterInput.isMouseLeftDown = isInDigestRect;
        characterInput.isMouseLeftReleased = isDigestMouseHeld && !isInDigestRect;
        isDigestMouseHeld = isInDigestRect;
      }
      else
      {
        if (digestClickCooldown > 0) digestClickCooldown--;
        characterInput.isMouseLeftClick = isAtTarget && digestClickCooldown <= 0 && !isDigestMouseHeld;
        characterInput.isMouseLeftReleased = isDigestMouseHeld;
        if (characterInput.isMouseLeftClick)
        {
          isDigestMouseHeld = true;
          digestClickCooldown = 45;
        }
        else if (characterInput.isMouseLeftReleased)
          isDigestMouseHeld = false;
      }
      characterManager.inputOverride = characterInput;
      isItemMouseHeld = false;
      return;
    }

    state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
  }

  void Autofeed::toggle() { isEnabled = !isEnabled; }
  void Autofeed::disable() { isEnabled = false; }
  bool Autofeed::available_get(Entity& character, menu::Inventory& inventory, ItemManager& itemManager) const
  {
    return is_food(character, inventory, itemManager);
  }
  bool Autofeed::enabled_get() const { return isEnabled; }
  bool Autofeed::active_get() const { return isActive; }
}
