#include "luabindings.hpp"

#include <components/lua/luastate.hpp>
#include <components/queries/query.hpp>

#include "../mwclass/door.hpp"

#include "../mwworld/containerstore.hpp"
#include "../mwworld/inventorystore.hpp"

#include "eventqueue.hpp"
#include "luamanagerimp.hpp"

namespace MWLua
{
    template <typename ObjectT>
    struct Inventory
    {
        ObjectT mObj;
    };
}

namespace sol
{
    template <>
    struct is_automagical<MWLua::LObject> : std::false_type {};
    template <>
    struct is_automagical<MWLua::GObject> : std::false_type {};
    template <>
    struct is_automagical<MWLua::LObjectList> : std::false_type {};
    template <>
    struct is_automagical<MWLua::GObjectList> : std::false_type {};
    template <>
    struct is_automagical<MWLua::Inventory<MWLua::LObject>> : std::false_type {};
    template <>
    struct is_automagical<MWLua::Inventory<MWLua::GObject>> : std::false_type {};
}

namespace MWLua
{

    template <typename ObjT>
    using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;

    template <class Class>
    static const MWWorld::Ptr& requireClass(const MWWorld::Ptr& ptr)
    {
        if (typeid(Class) != typeid(ptr.getClass()))
        {
            std::string msg = "Requires type '";
            msg.append(getMWClassName(typeid(Class)));
            msg.append("', but applied to ");
            msg.append(ptrToString(ptr));
            throw std::runtime_error(msg);
        }
        return ptr;
    }

    template <class ObjectT>
    static void registerObjectList(const std::string& prefix, const Context& context)
    {
        using ListT = ObjectList<ObjectT>;
        sol::state& lua = context.mLua->sol();
        ObjectRegistry* registry = context.mWorldView->getObjectRegistry();
        sol::usertype<ListT> listT = lua.new_usertype<ListT>(prefix + "ObjectList");
        listT[sol::meta_function::to_string] =
            [](const ListT& list) { return "{" + std::to_string(list.mIds->size()) + " objects}"; };
        listT[sol::meta_function::length] = [](const ListT& list) { return list.mIds->size(); };
        listT[sol::meta_function::index] = [registry](const ListT& list, size_t index)
        {
            if (index > 0 && index <= list.mIds->size())
                return ObjectT((*list.mIds)[index - 1], registry);
            else
                throw std::runtime_error("Index out of range");
        };
        listT["ipairs"] = [registry](const ListT& list)
        {
            auto iter = [registry](const ListT& l, int64_t i) -> sol::optional<std::tuple<int64_t, ObjectT>>
            {
                if (i >= 0 && i < static_cast<int64_t>(l.mIds->size()))
                    return std::make_tuple(i + 1, ObjectT((*l.mIds)[i], registry));
                else
                    return sol::nullopt;
            };
            return std::make_tuple(iter, list, 0);
        };
        listT["select"] = [context](const ListT& list, const Queries::Query& query)
        {
            return ListT{selectObjectsFromList(query, list.mIds, context)};
        };
    }

    template <class ObjectT>
    static void addBasicBindings(sol::usertype<ObjectT>& objectT, const Context& context)
    {
        objectT["isValid"] = [](const ObjectT& o) { return o.isValid(); };
        objectT["recordId"] = sol::readonly_property([](const ObjectT& o) -> std::string
        {
            return o.ptr().getCellRef().getRefId();
        });
        objectT["cell"] = sol::readonly_property([](const ObjectT& o) -> sol::optional<Cell<ObjectT>>
        {
            const MWWorld::Ptr& ptr = o.ptr();
            if (ptr.isInCell())
                return Cell<ObjectT>{ptr.getCell()};
            else
                return sol::nullopt;
        });
        objectT["position"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f
        {
            return o.ptr().getRefData().getPosition().asVec3();
        });
        objectT["rotation"] = sol::readonly_property([](const ObjectT& o) -> osg::Vec3f
        {
            return o.ptr().getRefData().getPosition().asRotationVec3();
        });
        objectT["type"] = sol::readonly_property(&ObjectT::type);
        objectT["count"] = sol::readonly_property([](const ObjectT& o) { return o.ptr().getRefData().getCount(); });
        objectT[sol::meta_function::equal_to] = [](const ObjectT& a, const ObjectT& b) { return a.id() == b.id(); };
        objectT[sol::meta_function::to_string] = &ObjectT::toString;
        objectT["sendEvent"] = [context](const ObjectT& dest, std::string eventName, const sol::object& eventData)
        {
            context.mLocalEventQueue->push_back({dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)});
        };

        objectT["canMove"] = [](const ObjectT& o)
        {
            const MWWorld::Class& cls = o.ptr().getClass();
            return cls.getMaxSpeed(o.ptr()) > 0;
        };
        objectT["getRunSpeed"] = [](const ObjectT& o)
        {
            const MWWorld::Class& cls = o.ptr().getClass();
            return cls.getRunSpeed(o.ptr());
        };
        objectT["getWalkSpeed"] = [](const ObjectT& o)
        {
            const MWWorld::Class& cls = o.ptr().getClass();
            return cls.getWalkSpeed(o.ptr());
        };

        if constexpr (std::is_same_v<ObjectT, GObject>)
        {  // Only for global scripts
            objectT["addScript"] = [luaManager=context.mLuaManager](const GObject& object, const std::string& path)
            {
                luaManager->addLocalScript(object.ptr(), path);
            };

            objectT["teleport"] = [luaManager=context.mLuaManager](const GObject& object, std::string_view cell,
                                                                   const osg::Vec3f& pos, const sol::optional<osg::Vec3f>& optRot)
            {
                MWWorld::Ptr ptr = object.ptr();
                osg::Vec3f rot = optRot ? *optRot : ptr.getRefData().getPosition().asRotationVec3();
                auto action = std::make_unique<TeleportAction>(object.id(), std::string(cell), pos, rot);
                if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
                    luaManager->addTeleportPlayerAction(std::move(action));
                else
                    luaManager->addAction(std::move(action));
            };
        }
        else
        {  // Only for local scripts
            objectT["isOnGround"] = [](const ObjectT& o)
            {
                return MWBase::Environment::get().getWorld()->isOnGround(o.ptr());
            };
            objectT["isSwimming"] = [](const ObjectT& o)
            {
                return MWBase::Environment::get().getWorld()->isSwimming(o.ptr());
            };
            objectT["isInWeaponStance"] = [](const ObjectT& o)
            {
                const MWWorld::Class& cls = o.ptr().getClass();
                return cls.isActor() && cls.getCreatureStats(o.ptr()).getDrawState() == MWMechanics::DrawState_Weapon;
            };
            objectT["isInMagicStance"] = [](const ObjectT& o)
            {
                const MWWorld::Class& cls = o.ptr().getClass();
                return cls.isActor() && cls.getCreatureStats(o.ptr()).getDrawState() == MWMechanics::DrawState_Spell;
            };
            objectT["getCurrentSpeed"] = [](const ObjectT& o)
            {
                const MWWorld::Class& cls = o.ptr().getClass();
                return cls.getCurrentSpeed(o.ptr());
            };
        }
    }

    template <class ObjectT>
    static void addDoorBindings(sol::usertype<ObjectT>& objectT, const Context& context)
    {
        auto ptr = [](const ObjectT& o) -> const MWWorld::Ptr& { return requireClass<MWClass::Door>(o.ptr()); };

        objectT["isTeleport"] = sol::readonly_property([ptr](const ObjectT& o)
        {
            return ptr(o).getCellRef().getTeleport();
        });
        objectT["destPosition"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f
        {
            return ptr(o).getCellRef().getDoorDest().asVec3();
        });
        objectT["destRotation"] = sol::readonly_property([ptr](const ObjectT& o) -> osg::Vec3f
        {
            return ptr(o).getCellRef().getDoorDest().asRotationVec3();
        });
        objectT["destCell"] = sol::readonly_property(
            [ptr, worldView=context.mWorldView](const ObjectT& o) -> sol::optional<Cell<ObjectT>>
        {
            const MWWorld::CellRef& cellRef = ptr(o).getCellRef();
            if (!cellRef.getTeleport())
                return sol::nullopt;
            MWWorld::CellStore* cell = worldView->findCell(cellRef.getDestCell(), cellRef.getDoorDest().asVec3());
            if (cell)
                return Cell<ObjectT>{cell};
            else
                return sol::nullopt;
        });
    }

    static SetEquipmentAction::Equipment parseEquipmentTable(sol::table equipment)
    {
        SetEquipmentAction::Equipment eqp;
        for (auto& [key, value] : equipment)
        {
            int slot = key.as<int>();
            if (value.is<GObject>())
                eqp[slot] = value.as<GObject>().id();
            else
                eqp[slot] = value.as<std::string>();
        }
        return eqp;
    }
    
    template <class ObjectT>
    static void addInventoryBindings(sol::usertype<ObjectT>& objectT, const std::string& prefix, const Context& context)
    {
        using InventoryT = Inventory<ObjectT>;
        sol::usertype<InventoryT> inventoryT = context.mLua->sol().new_usertype<InventoryT>(prefix + "Inventory");

        objectT["getEquipment"] = [context](const ObjectT& o)
        {
            const MWWorld::Ptr& ptr = o.ptr();
            sol::table equipment(context.mLua->sol(), sol::create);
            if (!ptr.getClass().hasInventoryStore(ptr))
                return equipment;

            MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
            for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot)
            {
                auto it = store.getSlot(slot);
                if (it == store.end())
                    continue;
                context.mWorldView->getObjectRegistry()->registerPtr(*it);
                equipment[slot] = ObjectT(getId(*it), context.mWorldView->getObjectRegistry());
            }
            return equipment;
        };
        objectT["isEquipped"] = [](const ObjectT& actor, const ObjectT& item)
        {
            const MWWorld::Ptr& ptr = actor.ptr();
            if (!ptr.getClass().hasInventoryStore(ptr))
                return false;
            MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
            return store.isEquipped(item.ptr());
        };

        objectT["inventory"] = sol::readonly_property([](const ObjectT& o) { return InventoryT{o}; });
        inventoryT[sol::meta_function::to_string] =
            [](const InventoryT& inv) { return "Inventory[" + inv.mObj.toString() + "]"; };

        auto getWithMask = [context](const InventoryT& inventory, int mask)
        {
            const MWWorld::Ptr& ptr = inventory.mObj.ptr();
            MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
            ObjectIdList list = std::make_shared<std::vector<ObjectId>>();
            auto it = store.begin(mask);
            while (it.getType() != -1)
            {
                const MWWorld::Ptr& item = *(it++);
                context.mWorldView->getObjectRegistry()->registerPtr(item);
                list->push_back(getId(item));
            }
            return ObjectList<ObjectT>{list};
        };

        inventoryT["getAll"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_All); };
        inventoryT["getPotions"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Potion); };
        inventoryT["getApparatuses"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Apparatus); };
        inventoryT["getArmor"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Armor); };
        inventoryT["getBooks"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Book); };
        inventoryT["getClothing"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Clothing); };
        inventoryT["getIngredients"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Ingredient); };
        inventoryT["getLights"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Light); };
        inventoryT["getLockpicks"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Lockpick); };
        inventoryT["getMiscellaneous"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Miscellaneous); };
        inventoryT["getProbes"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Probe); };
        inventoryT["getRepairKits"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Repair); };
        inventoryT["getWeapons"] =
            [getWithMask](const InventoryT& inventory) { return getWithMask(inventory, MWWorld::ContainerStore::Type_Weapon); };
            
        inventoryT["countOf"] = [](const InventoryT& inventory, const std::string& recordId)
        {
            const MWWorld::Ptr& ptr = inventory.mObj.ptr();
            MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr);
            return store.count(recordId);
        };

        if constexpr (std::is_same_v<ObjectT, GObject>)
        {  // Only for global scripts
            objectT["setEquipment"] = [manager=context.mLuaManager](const GObject& obj, sol::table equipment)
            {
                if (!obj.ptr().getClass().hasInventoryStore(obj.ptr()))
                {
                    if (!equipment.empty())
                        throw std::runtime_error(ptrToString(obj.ptr()) + " has no equipment slots");
                    return;
                }
                manager->addAction(std::make_unique<SetEquipmentAction>(obj.id(), parseEquipmentTable(equipment)));
            };

            // TODO
            // obj.inventory:drop(obj2, [count])
            // obj.inventory:drop(recordId, [count])
            // obj.inventory:addNew(recordId, [count])
            // obj.inventory:remove(obj/recordId, [count])
            /*objectT["moveInto"] = [](const GObject& obj, const InventoryT& inventory) {};
            inventoryT["drop"] = [](const InventoryT& inventory) {};
            inventoryT["addNew"] = [](const InventoryT& inventory) {};
            inventoryT["remove"] = [](const InventoryT& inventory) {};*/
        }
    }

    template <class ObjectT>
    static void initObjectBindings(const std::string& prefix, const Context& context)
    {
        sol::usertype<ObjectT> objectT = context.mLua->sol().new_usertype<ObjectT>(prefix + "Object");
        addBasicBindings<ObjectT>(objectT, context);
        addDoorBindings<ObjectT>(objectT, context);
        addInventoryBindings<ObjectT>(objectT, prefix, context);

        registerObjectList<ObjectT>(prefix, context);
    }

    void initObjectBindingsForLocalScripts(const Context& context)
    {
        initObjectBindings<LObject>("L", context);
    }

    void initObjectBindingsForGlobalScripts(const Context& context)
    {
        initObjectBindings<GObject>("G", context);
    }

}