#include "console.hpp"

#include <algorithm>
#include <charconv>
#include <format>
#include <string_view>
#include <utility>

#include <imgui.h>
#include <misc/cpp/imgui_stdlib.h>

#include "../resources.hpp"
#include "../state/play.hpp"

namespace game::developer
{
  namespace console
  {
#define GAME_CONSOLE_COMMANDS(X)                                                                                       \
  X(ANIMATIONS, "animations", "animations", true)                                                                     \
  X(CAPACITY, "capacity", "capacity <calories>", true)                                                                \
  X(CHARACTER, "character", "character <archive>", false)                                                             \
  X(CHARACTERS, "characters", "characters", false)                                                                    \
  X(DIGEST, "digest", "digest", true)                                                                                 \
  X(DIGESTION, "digestion", "digestion <% per second>", true)                                                         \
  X(DIALOGUE, "dialogue", "dialogue", true)                                                                           \
  X(EAT_SPEED, "eat_speed", "eat_speed <multiplier>", true)                                                           \
  X(GIVE, "give", "give <item_id> <quantity>", true)                                                                  \
  X(HELP, "help", "help", false)                                                                                      \
  X(ITEMS, "items", "items", true)                                                                                    \
  X(MENU, "menu", "menu", true)                                                                                       \
  X(PLAY_ANIMATION, "play_animation", "play_animation <animation>", true)                                             \
  X(PLAY_DIALOGUE, "play_dialogue", "play_dialogue <label>", true)                                                    \
  X(RELOAD, "reload", "reload", false)                                                                                \
  X(STAGE, "stage", "stage <stage>", true)                                                                            \
  X(WEIGHT, "weight", "weight <kilograms>", true)

    enum class Command
    {
#define GAME_CONSOLE_COMMAND_ENUM(SYMBOL, ...) SYMBOL,
      GAME_CONSOLE_COMMANDS(GAME_CONSOLE_COMMAND_ENUM)
#undef GAME_CONSOLE_COMMAND_ENUM
          UNKNOWN
    };

    struct CommandInfo
    {
      Command command{};
      std::string_view name{};
      std::string_view usage{};
      bool isGameOnly{};
    };

    constexpr CommandInfo COMMANDS[]{
#define GAME_CONSOLE_COMMAND_INFO(SYMBOL, NAME, USAGE, IS_GAME_ONLY)                                                   \
  {.command = Command::SYMBOL, .name = NAME, .usage = USAGE, .isGameOnly = IS_GAME_ONLY},
        GAME_CONSOLE_COMMANDS(GAME_CONSOLE_COMMAND_INFO)
#undef GAME_CONSOLE_COMMAND_INFO
    };

#undef GAME_CONSOLE_COMMANDS

    constexpr auto HISTORY_MIN_HEIGHT = 1.0f;
    constexpr auto SCROLL_BOTTOM = 1.0f;
    constexpr auto SCROLL_EPSILON = 1.0f;
    constexpr auto INPUT_FLAGS = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackHistory;
    constexpr auto ARG_VALUE = 1;
    constexpr auto ARG_QUANTITY = ARG_VALUE + 1;
    constexpr auto HISTORY_PREVIOUS = -1;
    constexpr auto HISTORY_NEXT = 1;

    const CommandInfo* command_info_find(std::string_view name)
    {
      for (auto& command : COMMANDS)
        if (command.name == name) return &command;
      return nullptr;
    }

    int int_parse(std::string_view value)
    {
      int result{};
      if (value.empty()) return result;
      std::from_chars(value.data(), value.data() + value.size(), result);
      return result;
    }

    float float_parse(std::string_view value)
    {
      float result{};
      if (value.empty()) return result;
      std::from_chars(value.data(), value.data() + value.size(), result);
      return result;
    }

    std::vector<std::string_view> args_get(const std::string& input)
    {
      std::vector<std::string_view> result{};
      auto view = std::string_view{input};
      while (!view.empty())
      {
        auto start = view.find_first_not_of(' ');
        if (start == std::string_view::npos) break;
        view.remove_prefix(start);
        auto end = view.find(' ');
        result.emplace_back(view.substr(0, end));
        if (end == std::string_view::npos) break;
        view.remove_prefix(end);
      }
      return result;
    }

    int input_callback(ImGuiInputTextCallbackData* data)
    {
      auto* console = static_cast<Console*>(data->UserData);
      auto offset = data->EventKey == ImGuiKey_UpArrow     ? HISTORY_PREVIOUS
                    : data->EventKey == ImGuiKey_DownArrow ? HISTORY_NEXT
                                                           : 0;
      if (offset == 0) return 0;
      auto& historyInput = console->command_history_offset(offset);
      data->DeleteChars(0, data->BufTextLen);
      data->InsertChars(0, historyInput.c_str());
      return 0;
    }
  }

  using namespace console;

  void Console::focus_request() { isFocusRequested = true; }

  void Console::history_push(std::string value) { history.emplace_back(std::move(value)); }

  void Console::command_history_push()
  {
    if (input.empty()) return;
    if (commandHistory.empty() || commandHistory.back() != input) commandHistory.emplace_back(input);
    commandHistoryIndex = (int)commandHistory.size();
  }

  const std::string& Console::command_history_offset(int offset)
  {
    static const std::string EMPTY{};
    if (commandHistory.empty()) return EMPTY;
    if (commandHistoryIndex < 0) commandHistoryIndex = (int)commandHistory.size();
    commandHistoryIndex = std::clamp(commandHistoryIndex + offset, 0, (int)commandHistory.size());
    return commandHistoryIndex >= (int)commandHistory.size() ? EMPTY : commandHistory[commandHistoryIndex];
  }

  void Console::help_print(bool isGame)
  {
    history_push("Commands:");
    for (auto& command : COMMANDS)
      if (isGame || !command.isGameOnly) history_push(std::format("  {}", command.usage));
  }

  void Console::command_execute(Resources& resources, state::Play* play)
  {
    action = {};
    auto args = args_get(input);
    if (args.empty()) return;

    auto isGame = play != nullptr;
    auto* info = command_info_find(args.front());
    if (!info)
    {
      history_push(std::format("Unknown command: {}", args.front()));
      return;
    }
    if (info->isGameOnly && !isGame)
    {
      history_push(std::format("Command unavailable outside game: {}", info->name));
      return;
    }

    switch (info->command)
    {
      case Command::ANIMATIONS:
      {
        history_push("Animations:");
        for (int i = 0; i < (int)play->character.animations.size(); ++i)
          history_push(std::format("  {} {}", i, play->character.animations[i].name));
        break;
      }
      case Command::CAPACITY:
      {
        play->character.capacity = float_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        history_push(std::format("Capacity set to {} kcal.", play->character.capacity));
        break;
      }
      case Command::CHARACTER:
      {
        auto key = args.size() > ARG_VALUE ? std::string{args[ARG_VALUE]} : std::string{};
        action.character = resources.character_index_get(key);
        history_push(std::format("Character set to {}.", key));
        break;
      }
      case Command::CHARACTERS:
      {
        history_push("Characters:");
        for (int i = 0; i < (int)resources.characters.size(); ++i)
          history_push(std::format("  {}", resources.character_key_get(i)));
        break;
      }
      case Command::DIGEST:
      {
        play->character.isDigesting = true;
        play->character.digestionTimer = 1;
        history_push("Digesting.");
        break;
      }
      case Command::DIGESTION:
      {
        auto digestion = float_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        play->character.digestionRate = digestion / Entity::UPDATE_RATE;
        history_push(std::format("Digestion set to {}%/sec.", digestion));
        break;
      }
      case Command::DIALOGUE:
      {
        history_push("Dialogue:");
        for (auto& [label, index] : play->character.data.dialogue.entryIDMap)
          history_push(std::format("  {}", label));
        break;
      }
      case Command::PLAY_DIALOGUE:
      {
        auto label = args.size() > ARG_VALUE ? std::string{args[ARG_VALUE]} : std::string{};
        auto* entry = play->character.data.dialogue.dialogue_entry_get(label);
        play->text.set(entry, play->character);
        history_push(std::format("Playing dialogue: {}", label));
        break;
      }
      case Command::EAT_SPEED:
      {
        play->character.eatSpeed = float_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        history_push(std::format("Eat speed set to {}x.", play->character.eatSpeed));
        break;
      }
      case Command::GIVE:
      {
        auto itemID = int_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        auto quantity = int_parse(args.size() > ARG_QUANTITY ? args[ARG_QUANTITY] : std::string_view{});
        play->menu.inventory.values[itemID] += quantity;
        history_push(std::format("Gave item {} x{}.", itemID, quantity));
        break;
      }
      case Command::HELP:
        help_print(isGame);
        break;
      case Command::ITEMS:
      {
        auto& items = play->character.data.itemSchema.items;
        history_push("Items:");
        for (int i = 0; i < (int)items.size(); ++i)
          history_push(std::format("  {} {}", i, items[i].name));
        break;
      }
      case Command::MENU:
      {
        action.isMenu = true;
        history_push("Returning to menu.");
        break;
      }
      case Command::PLAY_ANIMATION:
      {
        auto animation = args.size() > ARG_VALUE ? std::string{args[ARG_VALUE]} : std::string{};
        play->character.play(
            {.animation = animation, .mode = Entity::PLAY_FORCE, .interrupt = Entity::Interrupt::ALWAYS});
        history_push(std::format("Playing animation: {}", animation));
        break;
      }
      case Command::RELOAD:
      {
        if (play)
        {
          auto key = resources.character_key_get(play->characterIndex);
          play->save(resources);
          resources.characters_refresh();
          play->characterIndex = resources.character_index_get(key);
          play->set(resources, play->characterIndex, state::Play::CONTINUE);
        }
        else
          resources.characters_refresh();
        history_push(std::format("Reloaded character archives: {}", resources.characters.size()));
        break;
      }
      case Command::STAGE:
      {
        auto stage = int_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        play->character.stage = stage - 1;
        play->character.weight = play->character.stage_threshold_get(play->character.stage);
        history_push(std::format("Stage set to {}.", stage));
        break;
      }
      case Command::WEIGHT:
      {
        play->character.weight = float_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
        history_push(std::format("Weight set to {} kg.", play->character.weight));
        break;
      }
      case Command::UNKNOWN:
        break;
    }
  }

  void Console::update(Resources& resources, state::Play* play)
  {
    auto historySize = ImGui::GetContentRegionAvail();
    historySize.y = std::max(HISTORY_MIN_HEIGHT,
                             historySize.y - ImGui::GetFrameHeight() - ImGui::GetStyle().ItemSpacing.y);

    if (ImGui::BeginChild("##Console History", historySize, ImGuiChildFlags_Borders))
    {
      for (auto& entry : history)
        ImGui::TextUnformatted(entry.c_str());

      if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - SCROLL_EPSILON)
        ImGui::SetScrollHereY(SCROLL_BOTTOM);
    }
    ImGui::EndChild();

    if (isFocusRequested)
    {
      ImGui::SetKeyboardFocusHere();
      isFocusRequested = false;
    }

    ImGui::SetNextItemWidth(-FLT_MIN);
    if (ImGui::InputText("##Console Input", &input, INPUT_FLAGS, input_callback, this) && !input.empty())
    {
      history_push(std::format("> {}", input));
      command_history_push();
      command_execute(resources, play);
      input.clear();
      isFocusRequested = true;
    }
  }
}
