#include "adapters/eft/quests/conditions/QuestConditionGraph.h"

#include <algorithm>
#include <optional>
#include <unordered_set>
#include <utility>

#include "adapters/eft/quests/conditions/QuestConditionInterop.h"

namespace
{
    [[nodiscard]] std::string condition_text(Il2CppObject* condition)
    {
        std::string text = ta::adapters::eft::quests::QuestConditionInterop::formatted_description(condition);
        return text.empty()
            ? ta::adapters::eft::quests::QuestConditionInterop::condition_type(condition)
            : text;
    }
}

namespace ta::adapters::eft::quests
{
    QuestConditionGraph QuestConditionGraph::build(
        ta::adapters::il2cpp::Il2CppObjectInstance& quest,
        ta::adapters::il2cpp::Il2CppObjectInstance main_player)
    {
        QuestConditionGraph graph;
        Il2CppObject* completed_set = QuestConditionInterop::completed_conditions(quest);

        const auto add_node = [&](Il2CppObject* condition,
                                  const ta::adapters::eft::common::EftMongoId& id,
                                  Il2CppObject* counter,
                                  const int current,
                                  const int target,
                                  const int depth,
                                  const bool counter_root) -> std::optional<std::size_t>
        {
            const std::string id_string = QuestConditionInterop::mongo_id_string(id);
            if (id_string.empty())
                return std::nullopt;

            if (const auto found = graph.index_by_id_.find(id_string); found != graph.index_by_id_.end())
            {
                QuestConditionNode& existing = graph.nodes_[found->second];
                if (counter_root)
                {
                    existing.counter = counter;
                    existing.current = current;
                    existing.target = target;
                    existing.counter_root = true;
                }
                existing.depth = (std::min)(existing.depth, depth);
                return found->second;
            }

            QuestConditionNode node;
            node.id = id;
            node.id_string = id_string;
            node.text = condition_text(condition);
            node.target_template_id = QuestConditionInterop::primary_target_id(condition);
            node.condition = condition;
            node.counter = counter;
            node.current = current;
            node.target = target;
            node.depth = depth;
            node.counter_root = counter_root;
            if (completed_set)
            {
                const auto completed = QuestConditionInterop::contains_completed(completed_set, id);
                node.completed = completed && completed.value();
            }

            const std::size_t index = graph.nodes_.size();
            graph.index_by_id_.emplace(node.id_string, index);
            graph.nodes_.push_back(std::move(node));
            return index;
        };

        for (const QuestTaskConditionCounter& counter : QuestConditionInterop::collect_counters(quest, main_player))
        {
            const std::optional<int> current = QuestConditionInterop::counter_value(counter.counter);
            const std::optional<int> target = QuestConditionInterop::condition_target(counter.condition);
            if (!current || !target)
                continue;

            const auto root_index = add_node(
                counter.condition, counter.id, counter.counter, *current, *target, 0, true);
            if (!root_index)
                continue;

            std::unordered_set<Il2CppObject*> visited;
            const auto append_children = [&](auto&& self,
                                             Il2CppObject* parent,
                                             const std::size_t parent_index,
                                             const int depth) -> void
            {
                if (!parent || depth > 8 || !visited.insert(parent).second)
                    return;

                for (Il2CppObject* child : QuestConditionInterop::display_conditions(parent))
                {
                    if (!child || child == parent)
                        continue;

                    ta::adapters::eft::common::EftMongoId child_id{};
                    if (!QuestConditionInterop::condition_id(child, child_id))
                        continue;

                    const auto child_index = add_node(child, child_id, nullptr, 0, 0, depth, false);
                    if (!child_index)
                        continue;

                    auto& children = graph.nodes_[parent_index].children;
                    if (std::find(children.begin(), children.end(), *child_index) == children.end())
                        children.push_back(*child_index);
                    self(self, child, *child_index, depth + 1);
                }
            };
            append_children(append_children, counter.condition, *root_index, 1);
        }

        return graph;
    }

    const std::vector<QuestConditionNode>& QuestConditionGraph::nodes() const noexcept { return nodes_; }
    std::vector<QuestConditionNode>& QuestConditionGraph::nodes() noexcept { return nodes_; }

    const QuestConditionNode* QuestConditionGraph::find(const std::string_view id) const noexcept
    {
        const auto found = index_by_id_.find(std::string(id));
        return found == index_by_id_.end() ? nullptr : &nodes_[found->second];
    }

}
