// https://github.com/vinniefalco/LuaBridge
// Copyright 2020, Dmitry Tarakanov
// SPDX-License-Identifier: MIT

#include "TestBase.h"

#include <functional>

struct NamespaceTests : TestBase
{
    template<class T>
    T variable(const std::string& name)
    {
        runLua("result = " + name);
        return result<T>();
    }
};

TEST_F(NamespaceTests, Variables)
{
    int int_ = -10;
    auto any = luabridge::newTable(L);
    any["a"] = 1;

    ASSERT_THROW(luabridge::getGlobalNamespace(L).addProperty("int", &int_), std::logic_error);

    runLua("result = int");
    ASSERT_TRUE(result().isNil());

    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("int", &int_)
        .addProperty("any", &any)
        .endNamespace();

    ASSERT_EQ(-10, variable<int>("ns.int"));
    ASSERT_EQ(any, variable<luabridge::LuaRef>("ns.any"));

    runLua("ns.int = -20");
    ASSERT_EQ(-20, int_);

    runLua("ns.any = {b = 2}");
    ASSERT_TRUE(any.isTable());
    ASSERT_TRUE(any["b"].isNumber());
    ASSERT_EQ(2, any["b"].cast<int>());
}

TEST_F(NamespaceTests, ReadOnlyVariables)
{
    int int_ = -10;
    auto any = luabridge::newTable(L);
    any["a"] = 1;

    ASSERT_THROW(luabridge::getGlobalNamespace(L).addProperty("int", &int_), std::logic_error);

    runLua("result = int");
    ASSERT_TRUE(result().isNil());

    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("int", &int_, false)
        .addProperty("any", &any, false)
        .endNamespace();

    ASSERT_EQ(-10, variable<int>("ns.int"));
    ASSERT_EQ(any, variable<luabridge::LuaRef>("ns.any"));

    ASSERT_THROW(runLua("ns.int = -20"), std::runtime_error);
    ASSERT_EQ(-10, variable<int>("ns.int"));

    ASSERT_THROW(runLua("ns.any = {b = 2}"), std::runtime_error);
    ASSERT_EQ(any, variable<luabridge::LuaRef>("ns.any"));
}

namespace {

template<class T>
struct Property
{
    static T value;
};

template<class T>
T Property<T>::value;

template<class T>
void setProperty(const T& value)
{
    Property<T>::value = value;
}

template<class T>
const T& getProperty()
{
    return Property<T>::value;
}

} // namespace

TEST_F(NamespaceTests, Properties)
{
    setProperty<int>(-10);

    ASSERT_THROW(
        luabridge::getGlobalNamespace(L).addProperty("int", &getProperty<int>, &setProperty<int>),
        std::logic_error);

    runLua("result = int");
    ASSERT_TRUE(result().isNil());

    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("int", &getProperty<int>, &setProperty<int>)
        .endNamespace();

    ASSERT_EQ(-10, variable<int>("ns.int"));

    runLua("ns.int = -20");
    ASSERT_EQ(-20, getProperty<int>());
}

TEST_F(NamespaceTests, ReadOnlyProperties)
{
    setProperty<int>(-10);

    ASSERT_THROW(luabridge::getGlobalNamespace(L).addProperty("int", &getProperty<int>),
                 std::logic_error);

    runLua("result = int");
    ASSERT_TRUE(result().isNil());

    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("int", &getProperty<int>)
        .endNamespace();

    ASSERT_EQ(-10, variable<int>("ns.int"));

    ASSERT_THROW(runLua("ns.int = -20"), std::runtime_error);
    ASSERT_EQ(-10, getProperty<int>());
}

namespace {

template<class T>
struct Storage
{
    static T value;
};

template<class T>
T Storage<T>::value;

template<class T>
int getDataC(lua_State* L)
{
    luabridge::Stack<T>::push(L, Storage<T>::value);
    return 1;
}

template<class T>
int setDataC(lua_State* L)
{
    Storage<T>::value = luabridge::Stack<T>::get(L, -1);
    return 0;
}

} // namespace

TEST_F(NamespaceTests, Properties_ProxyCFunctions)
{
    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("value", &getDataC<int>, &setDataC<int>)
        .endNamespace();

    Storage<int>::value = 1;
    runLua("ns.value = 2");
    ASSERT_EQ(2, Storage<int>::value);

    Storage<int>::value = 3;
    runLua("result = ns.value");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(3, result().cast<int>());
}

TEST_F(NamespaceTests, Properties_ProxyCFunctions_ReadOnly)
{
    luabridge::getGlobalNamespace(L)
        .beginNamespace("ns")
        .addProperty("value", &getDataC<int>)
        .endNamespace();

    Storage<int>::value = 1;
    ASSERT_THROW(runLua("ns.value = 2"), std::exception);
    ASSERT_EQ(1, Storage<int>::value);

    Storage<int>::value = 3;
    runLua("result = ns.value");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(3, result().cast<int>());
}

namespace {
struct Class
{
};
} // namespace

TEST_F(NamespaceTests, LuaStackIntegrity)
{
    ASSERT_EQ(1, lua_gettop(L)); // Stack: ...

    {
        auto ns2 =
            luabridge::getGlobalNamespace(L).beginNamespace("namespace").beginNamespace("ns2");

        ASSERT_EQ(
            4,
            lua_gettop(L)); // Stack: ..., global namespace table (gns), namespace table (ns), ns2

        ns2.endNamespace(); // Stack: ...
        ASSERT_EQ(1, lua_gettop(L)); // Stack: ...
    }
    ASSERT_EQ(1, lua_gettop(L)); // Stack: ...

    {
        auto globalNs = luabridge::getGlobalNamespace(L);
        ASSERT_EQ(2, lua_gettop(L)); // Stack: ..., gns

        {
            auto ns = luabridge::getGlobalNamespace(L).beginNamespace("namespace");
            // both globalNs an ns are active
            ASSERT_EQ(4, lua_gettop(L)); // Stack: ..., gns, gns, ns
        }
        ASSERT_EQ(2, lua_gettop(L)); // Stack: ..., gns

        {
            auto ns = globalNs.beginNamespace("namespace");
            // globalNs became inactive
            ASSERT_EQ(3, lua_gettop(L)); // Stack: ..., gns, ns
        }
        ASSERT_EQ(1, lua_gettop(L)); // Stack: ...

        ASSERT_THROW(globalNs.beginNamespace("namespace"), std::exception);

        ASSERT_THROW(globalNs.beginClass<Class>("Class"), std::exception);
    }

    {
        auto globalNs = luabridge::getGlobalNamespace(L).beginNamespace("namespace").endNamespace();
        // globalNs is active
        ASSERT_EQ(2, lua_gettop(L)); // Stack: ..., gns
    }
    ASSERT_EQ(1, lua_gettop(L)); // StacK: ...

    {
        auto cls =
            luabridge::getGlobalNamespace(L).beginNamespace("namespace").beginClass<Class>("Class");
        ASSERT_EQ(6, lua_gettop(L)); // Stack: ..., gns, ns, const table, class table, static table
        {
            auto ns = cls.endClass();
            ASSERT_EQ(3, lua_gettop(L)); // Stack: ..., gns, ns
        }
        ASSERT_EQ(1, lua_gettop(L)); // Stack: ...
    }
    ASSERT_EQ(1, lua_gettop(L)); // StacK: ...

    // Test class continuation
    {
        auto cls =
            luabridge::getGlobalNamespace(L).beginNamespace("namespace").beginClass<Class>("Class");
        ASSERT_EQ(6, lua_gettop(L)); // Stack: ..., gns, ns, const table, class table, static table
    }
    ASSERT_EQ(1, lua_gettop(L)); // Stack: ...
}

namespace {

template<class T>
T Function(T param)
{
    return param;
}

} // namespace

TEST_F(NamespaceTests, Functions)
{
    luabridge::getGlobalNamespace(L).addFunction("Function", &Function<double>);

    runLua("result = Function (3.14)");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(3.14, result<double>());
}

TEST_F(NamespaceTests, StdFunctions)
{
    luabridge::getGlobalNamespace(L).addFunction("Function",
                                                 std::function<int(int)>(&Function<int>));

    runLua("result = Function (12)");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(12, result<int>());
}

#ifdef _M_IX86 // Windows 32bit only

namespace {

int __stdcall StdCall(int i)
{
    return i + 10;
}

} // namespace

TEST_F(NamespaceTests, StdCallFunctions)
{
    luabridge::getGlobalNamespace(L).addFunction("StdCall", &StdCall);

    runLua("result = StdCall (2)");
    ASSERT_TRUE(result().isNumber());
    ASSERT_EQ(12, result<int>());
}

#endif // _M_IX86
