#include "play.hpp"
#include "play/style.hpp"

#include <array>
#include <glm/glm.hpp>
#include <imgui.h>
#include <imgui_impl_opengl3.h>

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

using namespace game::resource;
using namespace game::resource::xml;
using namespace game::util;
using namespace game::state::play;
using namespace glm;

namespace game::state
{
  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;
    }
  }

  World::Focus Play::focus_get()
  {
    if (!isWindows) return World::CENTER;

    auto isToolsOpen = tools.isOpen && !menu.is_fullscreen_visible_get();
    return menu.isOpen && isToolsOpen ? World::MENU_TOOLS
           : menu.isOpen              ? World::MENU
           : isToolsOpen              ? World::TOOLS
                                      : World::CENTER;
  }

  bool Play::cursor_enabled_get() const
  {
    auto* root = character.data.cursorSchema.root();
    return root && root->type == Schema::Element::CURSOR;
  }

  void Play::start_sequence_begin()
  {
    auto& dialogue = character.data.dialogue;
    auto* start = dialogue.get(Schema::Element::START);
    if (!start) return;

    character.play({.animation = start->animation,
                    .appendID = character.animation_append_id_get(),
                    .interrupt = Entity::Interrupt::NEVER});
    character.update();
    text.entry = nullptr;
    text.isEnabled = false;
    isWindows = false;
    isStart = true;
    isStartBegin = false;
    isStartEnd = false;
  }

  void Play::end_sequence_begin()
  {
    auto& dialogue = character.data.dialogue;
    if (!dialogue.get(Schema::Element::END)) return;

    text.entry = nullptr;
    text.isEnabled = false;
    isEnd = true;
    isEndBegin = false;
    isEndEnd = false;
  }

  void Play::set(Resources& resources, int selectedCharacterIndex, enum Game game)
  {
    auto& data = resources.character_get(selectedCharacterIndex);
    auto* saveRoot = data.save.root();
    auto isSaveValid = saveRoot && saveRoot->type == Schema::Element::SAVE;
    auto* saveCharacter = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::CHARACTER) : nullptr;
    auto* saveInventory = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::INVENTORY) : nullptr;
    auto* saveItems = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::ITEMS) : nullptr;
    auto* savePan = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::PAN) : nullptr;
    auto* saveZoom = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::ZOOM) : nullptr;
    auto& itemSchema = data.itemSchema;
    auto& dialogue = data.dialogue;
    auto& menuSchema = data.menuSchema;
    auto* characterRoot = data.root();
    this->characterIndex = selectedCharacterIndex;
    resources.last_character_save(selectedCharacterIndex);
    cheatCodeIndex = 0;
    cheatCodeStartTime = 0.0;

    character = Entity(data, vec2(World::BOUNDS.x + World::BOUNDS.z * 0.5f, World::BOUNDS.w - World::BOUNDS.y));
    character.digestionRate =
        glm::clamp(character.digestionRate, characterRoot->digestionRateMin, characterRoot->digestionRateMax);
    character.eatSpeed =
        glm::clamp(character.eatSpeed, characterRoot->eatSpeedMinMultiplier, characterRoot->eatSpeedMaxMultiplier);
    character.capacity =
        glm::clamp(character.capacity, characterRoot->capacityMinCalories, characterRoot->capacityMaxCalories);

    auto* alternateSpritesheet = data.alternate_spritesheet();
    auto isAlternateSpritesheet =
        alternateSpritesheet && game == NEW_GAME && math::random_percent_roll(alternateSpritesheet->chanceOnNewGame);

    if (isAlternateSpritesheet || (isSaveValid && saveRoot->isAlternateSpritesheet))
    {
      character.spritesheet_set(Entity::ALTERNATE);
      if (game == NEW_GAME && alternateSpritesheet) alternateSpritesheet->soundEntry.sound.play();
    }

    character.totalCaloriesConsumed = saveCharacter ? saveCharacter->totalCaloriesConsumed : 0;
    character.totalFoodItemsEaten = saveCharacter ? saveCharacter->totalFoodItemsEaten : 0;
    autofeed = play::Autofeed{};
    characterManager = CharacterManager{};
    particleManager = ParticleManager{};
    areaManager = AreaManager{};

    cursor = Entity{};
    cursor.entityType = CURSOR;
    cursor.interactTypeID = character.data.interact_type_names_get().empty() ? -1 : 0;
    autofeedCursor = Entity{};
    autofeedCursor.entityType = AUTOCURSOR;
    autofeedCursor.interactTypeID = cursor.interactTypeID;
    autofeedCursor.position = cursor.position;
    autofeedCursor.isVisible = false;
    if (cursor_enabled_get())
    {
      cursor = Entity(character.data.cursorSchema.baseAnm2);
      cursor.entityType = CURSOR;
      cursor.interactTypeID = character.data.interact_type_names_get().empty() ? -1 : 0;
      autofeedCursor = Entity(character.data.cursorSchema.baseAnm2);
      autofeedCursor.entityType = AUTOCURSOR;
      autofeedCursor.interactTypeID = cursor.interactTypeID;
      autofeedCursor.position = cursor.position;
      autofeedCursor.isVisible = false;
    }

    menu.inventory = play::menu::Inventory{};
    for (auto* item : saveInventory ? data.save.children_get(*saveInventory, Schema::Element::ITEM)
                                    : std::vector<Schema::Element*>{})
    {
      if (item->quantity == 0) continue;
      menu.inventory.values[item->id] = item->quantity;
    }

    itemManager = ItemManager{};
    for (auto* item :
         saveItems ? data.save.children_get(*saveItems, Schema::Element::ITEM) : std::vector<Schema::Element*>{})
    {
      auto& anm2 = itemSchema.anm2s.at(item->id);
      auto& schemaItem = itemSchema.items.at(item->id);
      auto durabilityMax = schemaItem.isDurability ? schemaItem.durability : itemSchema.durability;
      auto animationIndex = durability_animation_index_get(itemSchema, anm2, item->durability, durabilityMax);
      auto& saveItem = itemSchema.anm2s.at(item->id);
      itemManager.items.emplace_back(saveItem, item->position, item->id, item->durability, animationIndex,
                                     item->velocity, item->rotation);
    }

    imgui::style::widget_set(menuSchema.rounding);
    imgui::widget::sounds_set(&menuSchema.root()->soundHover, &menuSchema.root()->soundSelect);
    play::style::color_set(resources, character);

    text.entry = nullptr;
    text.isEnabled = false;

#if DEBUG
    menu.isCheats = true;
#else
    menu.isCheats = false;
#endif

    isPostgame = isSaveValid && saveRoot->isPostgame;
    if (character.stage_get() >= character.stage_max_get()) isPostgame = true;
    if (isPostgame) menu.isCheats = true;

    if (game == NEW_GAME) isWindows = false;

    if (auto font = character.data.menuSchema.font.get()) ImGui::GetIO().FontDefault = font;

    character.play({.animation = character.idle_animation_get(), .appendID = character.animation_append_id_get()});
    character.update();
    areaManager.set(character);
    worldCanvas.size_set(imgui::to_vec2(ImGui::GetMainViewport()->Size));
    world.set(character, worldCanvas, focus_get());
    if (savePan) worldCanvas.pan = savePan->position;
    if (saveZoom && saveZoom->zoom >= World::ZOOM_MIN)
      worldCanvas.zoom = glm::clamp(World::ZOOM_MIN, saveZoom->zoom, World::ZOOM_MAX);

    if (game == NEW_GAME && dialogue.get(Schema::Element::START)) start_sequence_begin();

    if (isPostgame)
    {
      isEnd = true;
      isEndBegin = true;
      isEndEnd = true;
    }
    else
    {
      isEnd = false;
      isEndBegin = false;
      isEndEnd = false;
    }
  }

  void Play::exit(Resources& resources)
  {
    imgui::style::color_set(resources.settings.root()->color);
    imgui::style::widget_set();
    imgui::widget::sounds_set(nullptr, nullptr);
    ImGui::GetIO().FontDefault = resources.font.get();
    save(resources);
  }

  void Play::update(Resources& resources)
  {
    static constexpr std::array<ImGuiKey, 10> CHEAT_CODE = {
        ImGuiKey_UpArrow,    ImGuiKey_UpArrow,   ImGuiKey_DownArrow,  ImGuiKey_DownArrow, ImGuiKey_LeftArrow,
        ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B,         ImGuiKey_A};
    static constexpr std::array<ImGuiKey, 6> CHEAT_INPUT_KEYS = {
        ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
    static constexpr auto CHEAT_CODE_INPUT_TIME_SECONDS = 5.0;
    static constexpr auto UI_ALPHA_MAX = 1.0f;

    auto focus = focus_get();
    auto& dialogue = character.data.dialogue;
    auto isCursorEnabled = cursor_enabled_get();
    if (isCursorEnabled)
    {
      cursor.isVisible = true;
      autofeedCursor.isVisible = false;
    }

    if (!menu.isCheats)
    {
      for (auto key : CHEAT_INPUT_KEYS)
      {
        if (!ImGui::IsKeyPressed(key, false)) continue;

        if (key == CHEAT_CODE[cheatCodeIndex])
        {
          cheatCodeIndex++;
          cheatCodeStartTime = ImGui::GetTime();
        }
        else if (key == CHEAT_CODE[0])
        {
          cheatCodeIndex = 1;
          cheatCodeStartTime = ImGui::GetTime();
        }
        else
        {
          cheatCodeIndex = 0;
          cheatCodeStartTime = 0.0;
        }

        if (cheatCodeIndex >= (int)CHEAT_CODE.size())
        {
          menu.isCheats = true;
          cheatCodeIndex = 0;
          cheatCodeStartTime = 0.0;
          toasts.push(character.data.strings.get(Strings::ToastCheatsUnlocked));
          character.data.menuSchema.root()->soundCheatsActivated.play();
        }
      }

      if (cheatCodeIndex > 0 && (ImGui::GetTime() - cheatCodeStartTime > CHEAT_CODE_INPUT_TIME_SECONDS))
      {
        cheatCodeIndex = 0;
        cheatCodeStartTime = 0.0;
      }
    }

    if (isWindows)
    {
      auto isToolsDisabled = menu.is_fullscreen_visible_get();
      auto uiAlpha = UI_ALPHA_MAX - menu.fullscreen_alpha_get();
      tools.update(character, cursor, world, focus, worldCanvas, isToolsDisabled, uiAlpha);
      info.update(resources, character, uiAlpha);
      menu.update(resources, itemManager, character, text, autofeed);
      toasts.update();
    }

    auto isEndSequenceActive = isEndBegin && !isEndEnd;
    itemManager.isDisabled = isEndSequenceActive;
    characterManager.isDisabled = isEndSequenceActive;

    if (text.isEnabled) text.update(character, worldCanvas);

    if (isStart)
    {
      if (!isStartBegin)
      {
        if (auto animation = character.animation_get())
        {
          if (animation->isLoop || character.state == Entity::STOPPED)
          {
            auto* start = dialogue.get(Schema::Element::START);
            text.set(start ? dialogue.dialogue_entry_get(*start) : nullptr, character);
            isStartBegin = true;
          }
        }
      }
      else if (!isStartEnd)
      {
        if (text.entry && text.is_terminal() && text.is_finished())
        {
          isWindows = true;
          isStartEnd = true;
          isStart = false;
          world.character_focus(character, worldCanvas, focus_get());
        }
      }
    }

    if (character.isJustStageFinal && !isEnd && !isPostgame) isEnd = true;

    if (isEnd)
    {
      if (!isEndBegin)
      {
        if (character.is_animation_finished())
        {
          auto* end = dialogue.get(Schema::Element::END);
          text.set(end ? dialogue.dialogue_entry_get(*end) : nullptr, character);
          isEndBegin = true;
          isWindows = false;
          tools.isOpen = false;
          menu.isOpen = false;
          character.calories = 0;
          character.digestionProgress = 0;
          itemManager.heldItemIndex = -1;
          world.character_focus(character, worldCanvas, focus_get());
        }
      }
      else if (!isEndEnd)
      {
        if (text.entry && text.is_terminal() && text.is_finished())
        {
          menu.isOpen = true;
          isWindows = true;
          isEndEnd = true;
          isEnd = false;
          isPostgame = true;
          menu.isCheats = true;
          world.character_focus(character, worldCanvas, focus_get());
        }
      }
    }

    autofeed.update(resources, character, autofeedCursor, menu.inventory, itemManager, characterManager, worldCanvas);
    auto& interactionCursor = autofeed.active_get() ? autofeedCursor : cursor;
    itemManager.isAutomation = autofeed.active_get();
    itemManager.update(character, interactionCursor, areaManager, text, World::BOUNDS, worldCanvas);
    characterManager.update(character, interactionCursor, text, worldCanvas);
    for (auto& particle : itemManager.particles)
      particleManager.spawn(character.data, particle.label, particle.position);
    for (auto& particle : characterManager.particles)
      particleManager.spawn(character.data, particle.label, particle.position);

    if (isCursorEnabled && !autofeed.active_get() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
        !itemManager.isItemHovered && !characterManager.isHovering && cursor.cursorState == Entity::DEFAULT)
      if (auto* cursorRoot = character.data.cursorSchema.root())
        if (auto animation = cursorRoot->animationClick.get())
          cursor.play({.animation = *animation, .mode = Entity::PLAY_FORCE, .interrupt = Entity::Interrupt::ALWAYS});

    character.update();
    areaManager.update(character);
    particleManager.update();
    cursor.update();
    if (autofeed.active_get()) autofeedCursor.update();
    world.update(character, cursor, worldCanvas, focus);
    worldCanvas.update();

    if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.settingsMenu.isSave)
    {
      save(resources);
      autosaveTime = 0;
      menu.settingsMenu.isSave = false;
    }
  }

  void Play::render(Resources& resources, Canvas& canvas)
  {
    auto& textureShader = resources.shaders[shader::TEXTURE];
    auto& rectShader = resources.shaders[shader::RECT];
    auto size = imgui::to_ivec2(ImGui::GetMainViewport()->Size);

    auto windowModel = math::quad_model_get(vec2(size));

    worldCanvas.bind();
    worldCanvas.size_set(size);
    worldCanvas.clear();

    areaManager.render(character, textureShader, rectShader, worldCanvas);

    character.render(textureShader, rectShader, worldCanvas);

    for (auto& item : itemManager.items)
      item.render(textureShader, rectShader, worldCanvas);

    auto isCursorEnabled = cursor_enabled_get();
    if (isCursorEnabled && autofeed.active_get()) autofeedCursor.render(textureShader, rectShader, worldCanvas);
    particleManager.render(textureShader, rectShader, worldCanvas);

    worldCanvas.unbind();

    auto isCursor = isCursorEnabled && !resources.isDeveloper;

    canvas.bind();
    canvas.texture_render(textureShader, worldCanvas, windowModel);
    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
    if (isCursor) cursor.render(textureShader, rectShader, canvas);
    canvas.unbind();
  }

  void Play::save(Resources& resources)
  {
    resource::xml::Schema save;
    auto& root = save.element_add(resource::xml::Schema::Element::SAVE);
    root.isPostgame = isPostgame;
    root.isAlternateSpritesheet = character.spritesheetType == Entity::ALTERNATE;

    auto& panElement = save.element_add(resource::xml::Schema::Element::PAN, 0);
    panElement.position = worldCanvas.pan;

    auto& zoomElement = save.element_add(resource::xml::Schema::Element::ZOOM, 0);
    zoomElement.zoom = worldCanvas.zoom;

    auto& characterElement = save.element_add(resource::xml::Schema::Element::CHARACTER, 0);
    characterElement.weightKilograms = character.weight;
    characterElement.calories = character.calories;
    characterElement.capacityCalories = character.capacity;
    characterElement.digestionRate = character.digestionRate;
    characterElement.eatSpeedMultiplier = character.eatSpeed;
    characterElement.digestionProgress = character.digestionProgress;
    characterElement.isDigesting = character.isDigesting;
    characterElement.digestionTimer = character.digestionTimer;
    characterElement.totalCaloriesConsumed = character.totalCaloriesConsumed;
    characterElement.totalFoodItemsEaten = character.totalFoodItemsEaten;

    auto inventoryIndex = (int)save.elements.size();
    save.element_add(resource::xml::Schema::Element::INVENTORY, 0);

    for (auto& [id, quantity] : menu.inventory.values)
    {
      if (quantity == 0) continue;
      auto& item = save.element_add(resource::xml::Schema::Element::ITEM, inventoryIndex);
      item.id = id;
      item.quantity = quantity;
    }

    auto itemsIndex = (int)save.elements.size();
    save.element_add(resource::xml::Schema::Element::ITEMS, 0);
    for (auto& item : itemManager.items)
    {
      auto& itemElement = save.element_add(resource::xml::Schema::Element::ITEM, itemsIndex);
      itemElement.id = item.schemaID;
      itemElement.durability = item.durability;
      itemElement.position = item.position;
      itemElement.velocity = item.velocity;
      itemElement.rotation = *item.overrides[item.rotationOverrideID].frame.rotation;
    }

    resources.character_save_set(characterIndex, save);
    save.serialize(character.data.save_path_get());

    toasts.push(character.data.strings.get(Strings::ToastSaving));
  }
};
