#include <iostream>
#include <nlohmann/json.hpp>
#include <Ogre.h>
#include "goap.h"
#include "CharacterManagerModule.h"
#include "PlayerActionModule.h"
#include "CharacterModule.h"
#include "items.h"
#include "CharacterAIModule.h"
#include <tracy/Tracy.hpp>
namespace ECS
{
struct ActionExec {
    struct PlanExecData {
        TownNPCs::NPCData &npc;
        Blackboard &bb;
        nlohmann::json &memory;
    };
    enum { OK = 0, BUSY, ERROR };
    TownNPCs::NPCData &npc;
    Blackboard &bb;
    nlohmann::json &memory;
    bool complete;
    bool active;
    const goap::BaseAction<Blackboard> *action;
    ActionExec(PlanExecData &data,
           const goap::BaseAction<Blackboard> *action)
        : npc(data.npc)
        , bb(data.bb)
        , memory(data.memory)
        , complete(false)
        , active(false)
        , action(action)
    {
    }
    ActionExec(const ActionExec &other)
        : npc(other.npc)
        , bb(other.bb)
        , memory(other.memory)
        , complete(other.complete)
        , active(other.active)
        , action(other.action)
    {
    }
    ActionExec &operator=(const ActionExec &other)
    {
        npc = other.npc;
        bb = other.bb;
        memory = other.memory;
        complete = other.complete;
        active = other.active;
        action = other.action;
        return *this;
    }

private:
    virtual int update(float delta) = 0;
    virtual void finish(int result) = 0;
    virtual void activate() = 0;

public:
    int operator()(float delta)
    {
        if (!active)
            activate();
        int ret = update(delta);
        if (ret != BUSY)
            finish(ret);
        return ret;
    }
};
struct PlanExec {
    enum { OK = 0, BUSY, ERROR };
    std::vector<struct ActionExec *> action_exec;
    int index;
    PlanExec()
        : index(-1)
    {
    }
    int operator()(float delta)
    {
        if (index == -1)
            index = 0;
        int rc = (*action_exec[index])(delta);
        if (rc == ActionExec::ERROR)
            return ERROR;
        if (action_exec[index]->complete)
            index++;
        if (index >= action_exec.size())
            return OK;
        return BUSY;
    }
};

struct ActionNodeActions {
    struct WalkToAction : public goap::BaseAction<Blackboard> {
        int node;
        WalkToAction(int node, int cost)
            : goap::BaseAction<Blackboard>(
                  "WalkTo(" +
                      Ogre::StringConverter::toString(
                          node) +
                      ")",
                  { { { "at_object", 0 } } },
                  { { { "at_object", 1 } } }, cost)
            , node(node)
        {
        }
        bool can_run(const Blackboard &state,
                 bool debug = false) override
        {
            return (state.distance_to(prereq) == 0);
        }
        void _plan_effects(Blackboard &state) override
        {
            const ActionNodeList &alist =
                ECS::get<ActionNodeList>();
            state.apply(effects);
            const Ogre::Vector3 &nodePosition =
                alist.dynamicNodes[node].position;
            state.setPosition(nodePosition);
        }
        int get_cost(const Blackboard &bb) const override
        {
            int ret = m_cost;
            if (!bb.town.is_valid()) {
                ret += 1000000;
                goto out;
            }

            {
                const ActionNodeList &alist =
                    ECS::get<ActionNodeList>();
                const Ogre::Vector3 &nodePosition =
                    alist.dynamicNodes[node].position;
                const Ogre::Vector3 &npcPosition =
                    bb.getPosition();
                float dist = npcPosition.squaredDistance(
                    nodePosition);
                ret += (int)Ogre::Math::Ceil(dist);
            }
out:
            return ret;
        }
    };
    struct RunActionNode : public goap::BaseAction<Blackboard> {
        int node;
        float radius;
        Ogre::String action;
        RunActionNode(int node, const Ogre::String &action,
                  const Blackboard &prereq,
                  const Blackboard &effects, int cost)
            : goap::BaseAction<Blackboard>(
                  "Use(" + action + "," +
                      Ogre::StringConverter::toString(
                          node) +
                      ")",
                  prereq, effects, cost)
            , node(node)
            , action(action)
        {
            const ActionNodeList &alist =
                ECS::get<ActionNodeList>();
            radius = alist.dynamicNodes[node].radius;
        }
        bool can_run(const Blackboard &state,
                 bool debug = false) override
        {
            bool pre = (state.distance_to(prereq) == 0);
            if (!pre)
                return pre;
            const ActionNodeList &alist =
                ECS::get<ActionNodeList>();
            const Ogre::Vector3 &nodePosition =
                alist.dynamicNodes[node].position;
            const Ogre::Vector3 &npcPosition = state.getPosition();
            return (npcPosition.squaredDistance(nodePosition) <
                radius * radius);
        }
    };
    std::vector<goap::BaseAction<Blackboard> *> m_actions;

public:
    struct ActionExecWalk : ActionExec {
    private:
        Ogre::Vector3 targetPosition;
        float radius;
        int update(float delta) override
        {
            if (npc.e.is_valid()) {
                Ogre::SceneNode *n =
                    ECS::get<CharacterModule>()
                        .characterNodes.at(npc.e);
                Ogre::Vector3 position =
                    n->_getDerivedPosition();
                if (position.squaredDistance(targetPosition) >=
                    radius * radius) {
                    if (npc.e.is_valid())
                        n->_setDerivedPosition(
                            targetPosition);
                    npc.position = targetPosition;
                    return BUSY;
                }
            } else {
                if (npc.position.squaredDistance(
                        targetPosition) >=
                    radius * radius) {
                    npc.position = targetPosition;
                    return BUSY;
                }
            }
            return OK;
        }
        void finish(int rc) override
        {
            if (rc == OK)
                bb.apply(action->effects);
        }
        void activate() override
        {
            const ActionNodeActions::WalkToAction *wtaction =
                static_cast<
                    const ActionNodeActions::WalkToAction *>(
                    action);
            radius = ECS::get<ActionNodeList>()
                     .dynamicNodes[wtaction->node]
                     .radius;
            targetPosition = ECS::get<ActionNodeList>()
                         .dynamicNodes[wtaction->node]
                         .position;
            Ogre::Vector3 direction =
                (targetPosition - npc.position).normalisedCopy();
            targetPosition -= direction * radius;
        }

    public:
        ActionExecWalk(ActionExec::PlanExecData &data,
                   goap::BaseAction<Blackboard> *action)
            : ActionExec(data, action)
        {
        }
    };
    struct ActionExecUse : ActionExec {
    private:
        int update(float delta) override
        {
            OgreAssert(false, "update");
            return OK;
        }
        void finish(int rc) override
        {
            if (rc == OK)
                bb.apply(action->effects);
            OgreAssert(false, "finish");
        }
        void activate() override
        {
            const ActionNodeActions::RunActionNode *runaction =
                static_cast<const ActionNodeActions::RunActionNode
                            *>(action);
            OgreAssert(false, "activate");
        }

    public:
        ActionExecUse(ActionExec::PlanExecData &data,
                  goap::BaseAction<Blackboard> *action)
            : ActionExec(data, action)
        {
        }
    };
    ActionNodeActions(int node, const Blackboard &prereq, int cost)
    {
        ZoneScoped;
        OgreAssert(
            node < ECS::get<ActionNodeList>().dynamicNodes.size(),
            "bad node " + Ogre::StringConverter::toString(node));
        auto paction = OGRE_NEW WalkToAction(node, 10000);
        m_actions.push_back(paction);
        nlohmann::json jactionPrereq = nlohmann::json::object();
        nlohmann::json jactionEffect = nlohmann::json::object();
        jactionPrereq["at_object"] = 1;
        const nlohmann::json props =
            ECS::get<ActionNodeList>().dynamicNodes[node].props;
        OgreAssert(!props.is_null(),
               "bad node " + Ogre::StringConverter::toString(node));
        Ogre::String prefix = "goap_prereq_";
        for (auto it = props.begin(); it != props.end(); it++) {
            if (it.key().substr(0, prefix.length()) == prefix) {
                Ogre::String key =
                    it.key().substr(prefix.length());
                jactionPrereq[key] = it.value();
            }
        }
        Ogre::String prefix2 = "goap_effect_";
        for (auto it = props.begin(); it != props.end(); it++) {
            if (it.key().substr(0, prefix2.length()) == prefix2) {
                Ogre::String key =
                    it.key().substr(prefix2.length());
                jactionPrereq[key] = it.value();
            }
        }
        OgreAssert(props.find("action") != props.end(),
               "bad action" + props.dump(4));
        const Ogre::String &action =
            props["action"].get<Ogre::String>();
        Ogre::String effectName = "";
#if 0
        if (action == "sit") {
            const Ogre::String &nodeName =
                props["name"].get<Ogre::String>();
            effectName = "is_" + nodeName + "_seated";
        } else if (action == "use") {
            const Ogre::String &nodeName =
                props["name"].get<Ogre::String>();
            effectName = "is_" + nodeName + "_used";
        }
#endif
        // const Ogre::String &nodeName =
        //    props["name"].get<Ogre::String>();
        Ogre::String nodeID = Ogre::StringConverter::toString(node);
        effectName = "is_used";
        if (effectName.length() > 0) {
            jactionPrereq[effectName] = 0;
            jactionEffect[effectName] = 1;
        }
        // FIXME: add this to Blender goap_prereq_ and goap_effect_ variables
        if (action == "sit") {
            jactionPrereq["is_seated"] = 0;
            jactionEffect["is_seated"] = 1;
        } else if (action == "use") {
            OgreAssert(props["tags"].is_array(), "bad formed tags");
            const nlohmann::json &tags = props["tags"];
            if (tags.size() == 1 &&
                tags[0].get<Ogre::String>() == "") {
            } else {
                bool have_bits = false;
                if (std::find(tags.begin(), tags.end(),
                          "dance-pole") != tags.end()) {
                    jactionPrereq["is_pole_dancing"] = 0;
                    jactionEffect["is_pole_dancing"] = 1;
                    have_bits = true;
                }
                if (std::find(tags.begin(), tags.end(),
                          "toilet") != tags.end()) {
                    jactionPrereq["toilet"] = 1;
                    jactionEffect["toilet"] = 0;
                    have_bits = true;
                }
                if (!have_bits) {
                    ZoneScopedN("Use");
                    std::cout << "use: " << props.dump(4)
                          << std::endl;
                    // OgreAssert(false, "props: " + props.dump(4));
                    OgreAssert(tags.size() == 0,
                           "Some tags: " +
                               props.dump(4));
                }
            }
        } else {
            OgreAssert(false, "props: " + props.dump(4));
        }
        Blackboard actionPrereq({ jactionPrereq });
        Blackboard actionEffect({ jactionEffect });
        if (!prereq.is_valid())
            actionPrereq.apply(prereq);
        m_actions.push_back(OGRE_NEW RunActionNode(
            node, action, actionPrereq, actionEffect, cost));
        if (effectName == "") {
            std::cout << props.dump(4) << std::endl;
            std::cout << "Prereq" << std::endl;
            actionPrereq.dump_bits();
            std::cout << "Effect" << std::endl;
            actionEffect.dump_bits();
            OgreAssert(false, "action");
        }
    }
    std::vector<goap::BaseAction<Blackboard> *> getActions() const
    {
        return m_actions;
    }
};
struct ActionExecCommon : ActionExec {
private:
    float delay;
    int update(float delta) override
    {
        delay -= delta;
        if (delay > 0.0f)
            return BUSY;
        else
            return OK;
    }
    void finish(int rc) override
    {
        if (rc == OK)
            bb.apply(action->effects);
    }
    void activate() override
    {
        ZoneScoped;
        ZoneTextF("%s", action->get_name().c_str());
        delay = 1.0f;
    }

public:
    ActionExecCommon(ActionExec::PlanExecData &data,
             goap::BaseAction<Blackboard> *action)
        : ActionExec(data, action)
        , delay(0.0f)
    {
    }
};
CharacterAIModule::CharacterAIModule(flecs::world &ecs)
{
    static std::mutex ecs_mutex;
    ecs.module<CharacterAIModule>();
    ecs.import <CharacterManagerModule>();
    ecs.import <PlayerActionModule>();
    ecs.component<Blackboard>();
    ecs.component<TownAI>().on_add([](flecs::entity e, TownAI &ai) {
        std::lock_guard<std::mutex> lock(ecs_mutex);
        ai.mutex = std::make_shared<std::mutex>();
        std::lock_guard<std::mutex> lock2(*ai.mutex);
        ai.goals.push_back(
            { "HealthGoal",
              { nlohmann::json::object({ { "healthy", 1 } }) } });
        ai.goals.push_back(
            { "NotHungryGoal", { { { "hungry", 0 } } } });
        ai.goals.push_back(
            { "NotThirstyGoal", { { { "thirsty", 0 } } } });
        ai.goals.push_back(
            { "SatisfyToiletNeedGoal", { { { "toilet", 0 } } } });
        struct ActionData {
            Ogre::String name;
            Blackboard prereq;
            Blackboard effects;
            int cost;
        };
        struct ActionData actionData[] = {
#if 0
            { "WalkTo",
              { { { "at_object", 0 } } },
              { { { "at_object", 1 } } },
              1000 },
            { "Sit#",
              { { { "at_object", 1 }, { "is_seated", 0 } } },
              { { { "is_seated", 1 } } },
              10 },
#endif
            { "EatFoodSeated",
              { { { "have_food", 1 },
                  { "is_seated", 1 },
                  { "hungry", 1 } } },
              { { { "healthy", 1 }, { "hungry", 0 } } },
              10 },
            { "EatFood",
              { { { "have_food", 1 }, { "hungry", 1 } } },
              { { { "healthy", 1 }, { "hungry", 0 } } },
              2000 },
            { "DrinkWaterSeated",
              { { { "have_water", 1 },
                  { "is_seated", 1 },
                  { "thirsty", 1 } } },
              { { { "thirsty", 0 } } },
              10 },
            { "DrinkWater",
              { { { "have_water", 1 },
                  { "thirsty", 1 },
                  { "is_seated", 0 } } },
              { { { "thirsty", 0 } } },
              2000 },
            { "EatMedicineSeated",
              { { { "have_medicine", 1 },
                  { "healthy", 0 },
                  { "is_seated", 1 } } },
              { { { "healthy", 1 } } },
              100 },
            { "EatMedicine",
              { { { "have_medicine", 1 },
                  { "healthy", 0 },
                  { "is_seated", 0 } } },
              { { { "healthy", 1 } } },
              10000 },
#if 0
            { "UseToilet",
              { { { "toilet", 1 } } },
              { { { "toilet", 0 } } },
              100 },
#endif
            { "GetFood",
              { { { "have_food", 0 } } },
              { { { "have_food", 1 } } },
              1000 },
            { "GetWater",
              { { { "have_water", 0 } } },
              { { { "have_water", 1 } } },
              1000 },
            { "GetMedicine",
              { { { "have_medicine", 0 } } },
              { { { "have_medicine", 1 } } },
              1000 },
        };
        for (const auto &adata : actionData)
            ai.actions.push_back(
                OGRE_NEW goap::BaseAction<Blackboard>(
                    adata.name, adata.prereq, adata.effects,
                    adata.cost));
        ai.planner = std::make_shared<goap::BasePlanner<
            Blackboard, goap::BaseAction<Blackboard> > >();
    });
    ecs.system<TownAI, TownNPCs>("CreateBlackboards")
        .kind(flecs::OnUpdate)
        .each([this](flecs::entity town, TownAI &ai,
                 const TownNPCs &npcs) {
            ZoneScopedN("CreateBlackboards");
            std::lock_guard<std::mutex> lock(ecs_mutex);
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            createBlackboards(town, npcs, ai);
        });
    ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateDynamicActions")
        .kind(flecs::OnUpdate)
        .each([](flecs::entity e, ActionNodeList &alist, TownAI &ai,
             TownNPCs &npcs) {
            ZoneScopedN("UpdateDynamicActions");
            std::lock_guard<std::mutex> lock(ecs_mutex);
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            if (ai.nodeActions.size() > 0)
                return;
            if (alist.dynamicNodes.size() == 0)
                ECS::get_mut<ActionNodeList>()
                    .updateDynamicNodes();
            OgreAssert(alist.nodes.size() > 0, "bad  nodes");
            if (alist.dynamicNodes.size() == 0)
                return;
            OgreAssert(alist.dynamicNodes.size() > 0,
                   "bad dynamic nodes");
            int nodeIndex;
            for (nodeIndex = 0;
                 nodeIndex < alist.dynamicNodes.size();
                 nodeIndex++) {
                ActionNodeActions aactions(
                    nodeIndex,
                    Blackboard(
                        { nlohmann::json::object() }),
                    10);
                ai.nodeActions[nodeIndex] =
                    aactions.getActions();
                OgreAssert(ai.nodeActions[nodeIndex].size() > 0,
                       "bad action count");
            }
            OgreAssert(ai.nodeActions.size() > 0,
                   "no dynamic actions?");
        });
    ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateDynamicNodes")
        .kind(flecs::OnUpdate)
        .interval(0.1f)
        .each([this](flecs::entity town, ActionNodeList &alist,
                 TownAI &ai, TownNPCs &npcs) {
            ZoneScopedN("UpdateDynamicNodes");
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            std::lock_guard<std::mutex> lock(ecs_mutex);
            ECS::get_mut<ActionNodeList>().updateDynamicNodes();
        });
    struct MeasureTime {
        std::chrono::system_clock::time_point start;
        std::string what;
        MeasureTime(const std::string &s)
            : start(std::chrono::high_resolution_clock::now())
            , what(s)
        {
        }
        ~MeasureTime()
        {
            auto end = std::chrono::high_resolution_clock::now();
            std::chrono::duration<float, std::milli> elapsed =
                end - start;
            std::cout << what << " " << elapsed.count()
                  << std::endl;
        }
    };
    ecs.system<TownNPCs>("UpdateNPCPositions")
        .kind(flecs::OnUpdate)
        .each([](flecs::entity e, TownNPCs &npcs) {
            ZoneScopedN("UpdateNPCPositions");
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            for (auto it = npcs.npcs.begin(); it != npcs.npcs.end();
                 it++) {
                auto &npc = npcs.npcs.at(it->first);
                if (npc.e.is_valid() &&
                    npc.e.has<CharacterBase>()) {
                    Ogre::SceneNode *n =
                        ECS::get<CharacterModule>()
                            .characterNodes.at(
                                npc.e);
                    npc.position = n->_getDerivedPosition();
                }
            }
        });
    ecs.system<ActionNodeList, TownAI, TownNPCs>("UpdateBlackboards")
        .kind(flecs::OnUpdate)
        .interval(0.1f)
        .each([this](flecs::entity town, ActionNodeList &alist,
                 TownAI &ai, const TownNPCs &npcs) {
            ZoneScopedN("UpdateBlackboards");
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");

            Ogre::Root::getSingleton().getWorkQueue()->addTask([this,
                                        town,
                                        &alist,
                                        npcs,
                                        &ai]() {
                {
                    ZoneScopedN(
                        "UpdateBlackboards::Thread");
                    std::lock_guard<std::mutex> lock(
                        ecs_mutex);

                    updateBlackboards(town, alist, npcs,
                              ai);
                }
                Ogre::Root::getSingleton()
                    .getWorkQueue()
                    ->addMainThreadTask([this, town,
                                 &alist]() {
                        ZoneScopedN(
                            "UpdateBlackboards::MainThread");
                        town.modified<TownAI>();
                        town.modified<TownNPCs>();
                        ECS::modified<ActionNodeList>();
                    });
            });
        });
    ecs.system<TownAI, TownNPCs>("PlanAI")
        .kind(flecs::OnUpdate)
        .interval(0.5f)
        .each([&](flecs::entity town, TownAI &ai,
              const TownNPCs &npcs) {
            ZoneScopedN("PlanAI");
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            OgreAssert(ai.blackboards.size() > 0,
                   "blackboards not crated");
            OgreAssert(ai.memory.size() > 0, "memory not crated");
            Ogre::Root::getSingleton().getWorkQueue()->addTask([this,
                                        town,
                                        npcs,
                                        &ai]() {
                ZoneScopedN("PlanAI::Thread");
                std::lock_guard<std::mutex> lock(ecs_mutex);
                buildPlans(town, npcs, ai);
                Ogre::Root::getSingleton()
                    .getWorkQueue()
                    ->addMainThreadTask([this, town]() {
                        ZoneScopedN(
                            "PlanAI::MainThread");
                        town.modified<TownAI>();
                    });
            });
        });
    static std::unordered_map<int, struct PlanExec> plan_exec;
    ecs.system<const EngineData, TownNPCs, TownAI>("RunPLAN")
        .kind(flecs::OnUpdate)
        .each([&](flecs::entity town, const EngineData &eng,
              TownNPCs &npcs, TownAI &ai) {
            ZoneScopedN("RunPLAN");
            OgreAssert(npcs.npcs.size() > 0, "npcs not crated");
            OgreAssert(ai.blackboards.size() > 0,
                   "blackboards not crated");
            OgreAssert(ai.memory.size() > 0, "memory not crated");
            for (const auto &plans : ai.plans) {
                if (plan_exec.find(plans.first) !=
                    plan_exec.end()) {
                    int rc = plan_exec[plans.first](
                        eng.delta);
                    if (rc != PlanExec::BUSY) {
                        plan_exec.erase(plans.first);
                        ai.plans[plans.first].erase(
                            ai.plans[plans.first]
                                .begin());
                    }
                    continue;
                }
                // std::cout << "NPC: " << plans.first;
                // std::cout << " Plans: " << plans.second.size();
                for (const auto &plan : plans.second) {
                    struct PlanExec pexec;
                    if (plan.plan.size() == 0)
                        continue;
                    // std::cout << " Goal: ";
                    plan.goal->goal.dump_bits();
                    for (const auto &action : plan.plan) {
                        TownNPCs::NPCData &npc =
                            npcs.npcs.at(
                                plans.first);
                        Blackboard &bb =
                            ai.blackboards.at(
                                plans.first);
                        nlohmann::json &mem =
                            ai.memory.at(
                                plans.first);
                        ActionExec::PlanExecData data({
                            npc,
                            bb,
                            mem,

                        });
                        // TODO: executor factory is needed
                        if (action->get_name().substr(
                                0, 4) == "Walk") {
                            ActionExec *e = OGRE_NEW
                                ActionNodeActions::ActionExecWalk(
                                    data,
                                    action);
                            pexec.action_exec
                                .push_back(e);
                        } else if (action->get_name()
                                   .substr(0,
                                       4) ==
                               "Use(") {
                            ActionExec *e = OGRE_NEW
                                ActionNodeActions::ActionExecUse(
                                    data,
                                    action);
                            pexec.action_exec
                                .push_back(e);
                        } else {
                            // std::cout
                            //    << action->get_name()
                            //    << " ";
                            ActionExec *e = OGRE_NEW
                                ActionExecCommon(
                                    data,
                                    action);
                            pexec.action_exec
                                .push_back(e);
                        }
                        // std::cout << action->get_name()
                        //      << " ";
                    }
                    plan_exec[plans.first] = pexec;
                    break;
                }
                //std::cout << std::endl;
            }
        });
}

void CharacterAIModule::createAI(flecs::entity town)
{
    town.add<TownAI>();
}

struct PlanTask {
    Blackboard blackboard;
    TownAI::goal_t goal;
    TownAI::Plan plan;
    TownAI::planner_t *planner;
    bool operator()()
    {
        auto buildPlan = [this](Blackboard &blackboard,
                    const TownAI::goal_t &goal,
                    TownAI::Plan &plan) -> bool {
            if (goal.is_reached(blackboard))
                return false;
            plan.goal = &goal;
            std::vector<goap::BaseAction<Blackboard> *> path;
            int actionCount = blackboard.getActionsCount();
            auto actionsData = blackboard.getActionsData();
            path.resize(actionCount * actionCount);
            int path_length = planner->plan(
                blackboard, goal, actionsData, actionCount,
                path.data(), path.size());
            if (path_length > 0) {
                plan.goal = &goal;
                plan.plan.insert(plan.plan.end(), path.begin(),
                         path.begin() + path_length);
                return true;
            }
            return false;
        };
        return buildPlan(blackboard, goal, plan);
    }
    PlanTask(Blackboard &blackboard, const TownAI::goal_t &goal,
         TownAI::planner_t *planner)
        : blackboard(blackboard)
        , goal(goal)
        , planner(planner)
    {
    }
};
static std::deque<PlanTask> plan_tasks;

void CharacterAIModule::buildPlans(flecs::entity town, const TownNPCs &npcs,
                   TownAI &ai)
{
    ZoneScopedN("buildPlans");
    OgreAssert(town.is_valid(), "Bad town entity");
    std::lock_guard<std::mutex> lock(*ai.mutex);
    auto planner = ai.planner;
    if (plan_tasks.size() > 0) {
        bool created = (plan_tasks.front())();
        if (created) {
            ZoneTextF("%d: Goal: %s",
                  plan_tasks.front().blackboard.index,
                  plan_tasks.front().goal.get_name().c_str());
            {
                std::cout << plan_tasks.front().blackboard.index
                      << " ";
                std::cout << "Goal: "
                      << plan_tasks.front().goal.get_name();
                plan_tasks.front().goal.goal.dump_bits();
                std::cout << std::endl;
                std::cout << "Path: ";
                for (auto &action :
                     plan_tasks.front().plan.plan) {
                    OgreAssert(action, "No action");
                    std::cout << action->get_name() + " ";
                }
                std::cout << " size: "
                      << plan_tasks.front().plan.plan.size()
                      << std::endl;
            }
            ai.plans[plan_tasks.front().blackboard.index].push_back(
                plan_tasks.front().plan);
        }
        plan_tasks.pop_front();
    } else
        for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
            if (ai.blackboards.find(it->first) ==
                ai.blackboards.end())
                continue;
            auto &bb = ai.blackboards.at(it->first);
            /* if there are plans, skip until these get discarded */
            if (ai.plans.find(it->first) != ai.plans.end() &&
                ai.plans.at(it->first).size() > 0)
                continue;
            const auto &npc = npcs.npcs.at(it->first);
            int index = it->first;
            ai.plans[index] = {};
            bb.query_ai();
            for (const auto &goal : ai.goals)
                plan_tasks.emplace_back(bb, goal,
                            ai.planner.get());
        }
}

void CharacterAIModule::createBlackboards(flecs::entity town,
                      const TownNPCs &npcs, TownAI &ai)
{
    ZoneScopedN("createBlackboards");
    OgreAssert(town.is_valid(), "Bad town entity");
    std::lock_guard<std::mutex> lock(*ai.mutex);
    for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
        if (ai.blackboards.find(it->first) == ai.blackboards.end()) {
            int strength = 10;
            int dexterity = 10;
            int health = 100;
            int stamina = 100;
            int sex = it->second.props["sex"].get<int>();
            if (sex == 0) { // male
                strength += 10;
                dexterity += 10;
            }
            // FIXME: use separate "memory" for stats
            // Do not keep actual stats in blackboard
            nlohmann::json memory;
            memory["strength"] = strength;
            memory["dexterity"] = dexterity;
            memory["health"] = health;
            memory["stamina"] = stamina;

            memory["needs_hunger"] = 0;
            memory["needs_thirst"] = 0;
            memory["needs_toilet"] = 0;

            nlohmann::json bb;
            bb["have_water"] = 0;
            bb["have_food"] = 0;
            bb["have_medicine"] = 0;
            bb["at_object"] = 0;
            ai.memory[it->first] = memory;
            ai.blackboards[it->first] = Blackboard(bb);
            ai.blackboards[it->first].index = it->first;
            ai.blackboards.at(it->first).town = town;
            // FIXME: do this once
            ai.blackboards.at(it->first).actionRefResize(0);
            ai.blackboards.at(it->first).actionRefAddActions(
                ai.actions);
            ai.conditions =
                TownAI::BitEvolutionBuilder()
                    .addRule(
                        "healthy",
                        [](const nlohmann::json &data)
                            -> bool {
                            return data["health"]
                                       .get<int>() >
                                   20;
                        })
                    ->addRule(
                        "hungry",
                        [](const nlohmann::json &data)
                            -> bool {
                            return data["needs_hunger"]
                                       .get<int>() >
                                   2000;
                        })
                    ->addRule(
                        "thirsty",
                        [](const nlohmann::json &data)
                            -> bool {
                            return data["needs_thirst"]
                                       .get<int>() >
                                   1000;
                        })
                    ->addRule(
                        "toilet",
                        [](const nlohmann::json &data)
                            -> bool {
                            return data["needs_toilet"]
                                       .get<int>() >
                                   1500;
                        })
                    ->build();
            ai.memoryUpdates =
                TownAI::MemoryUpdateBuilder()
                    .addUpdate(
                        "health",
                        [](int index,
                           const std::string &name,
                           int value) -> int {
                            if (value < 50)
                                return value +
                                       1;
                            else
                                return value;
                        })
                    .addUpdate("needs_hunger",
                           [](int index,
                              const std::string &name,
                              int value) -> int {
                               return value + 1;
                           })
                    .addUpdate("needs_thirst",
                           [](int index,
                              const std::string &name,
                              int value) -> int {
                               return value + 1;
                           })
                    .addUpdate("needs_toilet",
                           [](int index,
                              const std::string &name,
                              int value) -> int {
                               return value + 1;
                           })
                    .build();
        }
    }
}

void CharacterAIModule::updateBlackboards(flecs::entity town,
                      ActionNodeList &alist,
                      const TownNPCs &npcs, TownAI &ai)
{
    ZoneScopedN("updateBlackboards");
    std::lock_guard<std::mutex> lock(*ai.mutex);
    OgreAssert(town.is_valid(), "Bad town entity");
    alist.build();
    for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
        if (ai.blackboards.find(it->first) == ai.blackboards.end())
            continue;
        ai.blackboards.at(it->first).index = it->first;
        ai.blackboards.at(it->first).town = town;
        ai.blackboards.at(it->first).setPosition(
            npcs.npcs.at(it->first).position);
        for (auto e : ai.memoryUpdates) {
            std::string key = e.name;
            auto &memory = ai.memory.at(it->first);
            if (memory.find(key) == memory.end())
                memory[key] = 0;
            int value = memory[key].get<int>();
            int new_value = e.update(it->first, key, value);
            if (value != new_value)
                memory[key] = new_value;
        }
        auto &bb = ai.blackboards.at(it->first);
        bb.updateBits(ai, ai.memory.at(it->first), it->second.props);
    }
}

void Blackboard::_actionRefResize(int count)
{
    if (count >= actionRef.size()) {
        int allocate = count;
        if (allocate < 1000)
            allocate = 1000;
        actionRef.resize(allocate);
    }
    OgreAssert(count < actionRef.size(), "out of memory");
    actionRefCount = count;
    actionRefPtr = count;
}

Blackboard::Blackboard()
    : object(-1)
    , index(-1)
    , actionRefCount(0)
    , mutex(std::make_shared<std::mutex>())
    , bits(0)
    , mask(0)
{
}

std::unordered_map<std::string, size_t> Blackboard::mapping;
Blackboard::Blackboard(const nlohmann::json &stats)
    : Blackboard()
{
    populate(stats, mapping);
}

void Blackboard::populate(const nlohmann::json &stats,
              std::unordered_map<std::string, size_t> &mapping)
{
    if (stats.empty())
        return;

    for (auto &[key, value] : stats.items()) {
        if (value.is_number_integer()) {
            int val = value.get<int>();
            if (val == 0 || val == 1) {
                // Update mapping if key is new
                if (mapping.find(key) == mapping.end()) {
                    size_t next_bit = mapping.size();
                    if (next_bit < 64) {
                        mapping[key] = next_bit;
                    } else {
                        OgreAssert(false,
                               "Out of bits");
                        continue;
                    }
                }

                size_t bit_idx = mapping[key];
                bits.set(bit_idx, val == 1);
                mask.set(bit_idx);
            }
        }
    }
}

bool Blackboard::operator==(const Blackboard &other) const
{
    OgreAssert(mask != 0 && other.mask != 0, "blackboard not prepared");
    return (bits & other.mask) == (other.bits & other.mask);
}

bool Blackboard::operator!=(const Blackboard &other) const
{
    return !(*this == other);
}

void Blackboard::apply(const Blackboard &other)
{
    std::lock_guard<std::mutex> lock(*mutex);
    // stats.update(other.stats);
    // 1. Clear bits in 'values' that are about to be overwritten (where effect mask is 1)
    // 2. OR the result with the effect values (filtered by the effect mask)
    bits = (bits & ~other.mask) | (other.bits & other.mask);

    // 3. Update our mask to include any new state definitions introduced by the effect
    mask |= other.mask;
}

Ogre::String Blackboard::dumpActions()
{
    std::lock_guard<std::mutex> lock(*mutex);
    Ogre::String ret;
    ret += "Actions:\n";
    int count = 0;
    for (count = 0; count < actionRefCount; count++) {
        auto &action = actionRef[count];
        ret += "name: " + action->get_name() + "\n";
        ret += "\tprereq:\n";
        action->prereq.dump_bits();
        ret += "\teffects:\n";
        action->effects.dump_bits();
    }
    return ret;
}

void Blackboard::printActions()
{
    std::cout << dumpActions() << std::endl;
}

void Blackboard::actionRefResize(int count)
{
    std::lock_guard<std::mutex> lock(*mutex);
    _actionRefResize(count);
}

void Blackboard::actionRefAddAction(goap::BaseAction<Blackboard> *action)
{
    std::lock_guard<std::mutex> lock(*mutex);
    if (actionRef.size() <= actionRefCount + 16)
        actionRef.resize(actionRefCount + 32);
    OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
    OgreAssert(action, "bad action");
    actionRef[actionRefPtr++] = action;
    actionRefCount++;
}

void Blackboard::actionRefAddActions(
    const std::vector<goap::BaseAction<Blackboard> *> &actions)
{
    std::lock_guard<std::mutex> lock(*mutex);
    _actionRefAddActions(actions);
}

void Blackboard::_actionRefAddActions(
    const std::vector<goap::BaseAction<Blackboard> *> &actions)
{
    if (actionRef.size() <= actionRefCount + actions.size())
        actionRef.resize(actionRefCount + actions.size() * 2);
    for (const auto &action : actions) {
        OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
        OgreAssert(action, "bad action");
        actionRef[actionRefPtr++] = action;
        actionRefCount++;
    }
}

void Blackboard::actionRefAddActions(goap::BaseAction<Blackboard> **actions,
                     int count)
{
    int i;
    std::lock_guard<std::mutex> lock(*mutex);
    if (actionRef.size() <= actionRefCount + count)
        actionRef.resize(actionRefCount + count * 2);
    for (i = 0; i < count; i++) {
        OgreAssert(actionRefPtr < actionRef.size(), "out of memory");
        OgreAssert(actions[i], "bad action");
        actionRef[actionRefPtr++] = actions[i];
        actionRefCount++;
    }
}

void Blackboard::updateBits(const TownAI &ai, const nlohmann::json &memory,
                const nlohmann::json &props)
{
    for (const auto &mcond : ai.conditions) {
        if (mapping.find(mcond.key) == mapping.end())
            populate(nlohmann::json::object({ { mcond.key, 0 } }),
                 mapping);
        bits.set(mapping[mcond.key], mcond.condition(memory));
    }
}

struct ComparePair {
    const nlohmann::json &current;
    const nlohmann::json &target;
};

bool Blackboard::is_satisfied_by(const nlohmann::json &current,
                 const nlohmann::json &target, float epsilon)
{
    std::deque<ComparePair> queue;
    queue.push_back({ current, target });
    while (!queue.empty()) {
        ComparePair pair = queue.front();
        queue.pop_front();
        const nlohmann::json &curr = pair.current;
        const nlohmann::json &tgt = pair.target;
        if (curr.type() != tgt.type() &&
            !(curr.is_number() && tgt.is_number()))
            return false;
        if (tgt.is_object())
            for (auto it = tgt.begin(); it != tgt.end(); ++it) {
                auto found = curr.find(it.key());
                if (found == curr.end())
                    return false;
                queue.push_back({ *found, it.value() });
            }
        else if (tgt.is_array()) {
            if (curr.size() != tgt.size())
                return false;
            for (int i = 0; i < tgt.size(); ++i)
                queue.push_back({ curr[i], tgt[i] });
        } else if (tgt.is_number_float() || curr.is_number_float()) {
            if (std::abs(curr.get<float>() - tgt.get<float>()) >=
                epsilon)
                return false;
        } else if (curr != tgt)
            return false;
    }
    return true;
}

int Blackboard::distance_to(const Blackboard &goal) const
{
    int distance = 0;
    OgreAssert(mask != 0 && goal.mask != 0, "blackboard not prepared");
    return ((bits ^ goal.bits) & goal.mask).count();
}

void Blackboard::setPosition(const Ogre::Vector3 &position)
{
    std::lock_guard<std::mutex> lock(*mutex);
    this->position = position;
}

void Blackboard::query_ai()
{
    std::lock_guard<std::mutex> lock(*mutex);
    TownAI &ai = town.get_mut<TownAI>();
    const TownNPCs &npcs = town.get<TownNPCs>();
    const float distance = 10000.0f;
    Ogre::Vector3 position(0, 0, 0);
    if (npcs.npcs.at(index).e.is_valid() &&
        npcs.npcs.at(index).e.has<CharacterBase>()) {
        Ogre::SceneNode *n =
            ECS::get<CharacterModule>().characterNodes.at(
                npcs.npcs.at(index).e);
        position = n->_getDerivedPosition();
    } else
        from_json(npcs.npcs.at(index).props["position"], position);
    this->position = position;
    ActionNodeList &alist = ECS::get_mut<ActionNodeList>();
    alist.query_ai(position, distance, points, distances);
    _actionRefResize(ai.actions.size());
    int nodeActionCount = 0;
    for (size_t point : points) {
        Ogre::Vector3 &p = alist.dynamicNodes[point].position;
        float radius = alist.dynamicNodes[point].radius;
        float distance = p.squaredDistance(position);
        if (object >= 0 && (size_t)object == point &&
            distance > radius * radius) {
            object = -1;
            bits.set(mapping["at_object"], false);
        } else if (object >= 0 && (size_t)object == point &&
               distance <= radius * radius)
            bits.set(mapping["at_object"], true);
        /* some nodes do not have usable actions */
        if (ai.nodeActions[point].size() > 0) {
            OgreAssert(ai.nodeActions[point].size() > 0,

                   "bad node actions count " +
                       alist.dynamicNodes[point].props.dump(
                           4));
            _actionRefAddActions(ai.nodeActions[point]);
            nodeActionCount += ai.nodeActions[point].size();
        }
        nlohmann::json nodes = nlohmann::json::array();
        nlohmann::json j = alist.dynamicNodes[point].props;
        j["global_position_x"] = p.x;
        j["global_position_y"] = p.y;
        j["global_position_z"] = p.z;
        nodes.push_back(j);
    }
}
}
