#include <array>
#include <iterator>
#include <string>
#include <utility>
#include <gtest/gtest.h>
#include <entt/core/type_info.hpp>
#include <entt/locator/locator.hpp>
#include <entt/meta/context.hpp>
#include <entt/meta/factory.hpp>
#include <entt/meta/meta.hpp>
#include <entt/meta/range.hpp>
#include <entt/meta/resolve.hpp>
#include "../../common/config.h"
#include "../../common/meta_traits.h"
#include "../../common/value_type.h"

struct MetaFactory: ::testing::Test {
    struct base {
        char member{};
    };

    struct clazz: base {
        clazz(int val)
            : value{val} {}

        [[nodiscard]] explicit operator int() const noexcept {
            return get_int();
        }

        void set_int(int val) noexcept {
            value = val;
        }

        void set_boxed_int(test::boxed_int val) noexcept {
            value = val.value;
        }

        [[nodiscard]] int get_int() const noexcept {
            return value;
        }

        static void set_double(clazz &instance, double val) noexcept {
            instance.value = static_cast<int>(val);
        }

        [[nodiscard]] static double get_double(const clazz &instance) noexcept {
            return instance.value;
        }

        [[nodiscard]] static std::string to_string(const clazz &instance) {
            return std::to_string(instance.get_int());
        }

        [[nodiscard]] static clazz from_string(const std::string &value) {
            return clazz{std::stoi(value)};
        }

    private:
        int value{};
    };

    void TearDown() override {
        entt::meta_reset();
    }
};

using MetaVoidFactory = MetaFactory;
using MetaFactoryDeathTest = MetaFactory;

TEST_F(MetaFactory, Constructors) {
    using namespace entt::literals;

    entt::meta_ctx ctx{};

    ASSERT_EQ(entt::resolve(entt::type_id<int>()), entt::meta_type{});
    ASSERT_EQ(entt::resolve(ctx, entt::type_id<int>()), entt::meta_type{});

    entt::meta_factory<int> factory{};

    ASSERT_NE(entt::resolve(entt::type_id<int>()), entt::meta_type{});
    ASSERT_EQ(entt::resolve(ctx, entt::type_id<int>()), entt::meta_type{});
    ASSERT_TRUE(entt::resolve(entt::type_id<int>()).is_integral());

    factory = entt::meta_factory<int>{ctx};

    ASSERT_NE(entt::resolve(entt::type_id<int>()), entt::meta_type{});
    ASSERT_NE(entt::resolve(ctx, entt::type_id<int>()), entt::meta_type{});
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()).is_integral());

    entt::meta_factory<double> other{"custom"_hs};

    ASSERT_EQ(entt::resolve(entt::type_id<double>()), entt::meta_type{});
    ASSERT_NE(entt::resolve("custom"_hs), entt::meta_type{});
    ASSERT_EQ(entt::resolve(ctx, entt::type_id<double>()), entt::meta_type{});
    ASSERT_EQ(entt::resolve(ctx, "custom"_hs), entt::meta_type{});
    ASSERT_TRUE(entt::resolve("custom"_hs).is_arithmetic());

    other = entt::meta_factory<double>{ctx, "custom"_hs};

    ASSERT_EQ(entt::resolve(entt::type_id<double>()), entt::meta_type{});
    ASSERT_NE(entt::resolve("custom"_hs), entt::meta_type{});
    ASSERT_EQ(entt::resolve(ctx, entt::type_id<double>()), entt::meta_type{});
    ASSERT_NE(entt::resolve(ctx, "custom"_hs), entt::meta_type{});
    ASSERT_TRUE(entt::resolve("custom"_hs).is_arithmetic());
}

TEST_F(MetaFactory, Type) {
    using namespace entt::literals;

    entt::meta_factory<int> factory{};

    ASSERT_EQ(entt::resolve("foo"_hs), entt::meta_type{});

    factory.type("foo"_hs);

    ASSERT_NE(entt::resolve("foo"_hs), entt::meta_type{});
    ASSERT_EQ(entt::resolve<int>().alias(), "foo"_hs);

    factory.type("bar"_hs);

    ASSERT_EQ(entt::resolve("foo"_hs), entt::meta_type{});
    ASSERT_NE(entt::resolve("bar"_hs), entt::meta_type{});
    ASSERT_EQ(entt::resolve<int>().alias(), "bar"_hs);
}

TEST_F(MetaFactory, OverloadedType) {
    using namespace entt::literals;

    entt::meta_factory<int>{}
        .type("foo"_hs)
        .data<1>("value"_hs);

    entt::meta_factory<int>{"integral"_hs}
        .type("bar"_hs)
        .data<2>("value"_hs);

    ASSERT_NE(entt::resolve<int>(), entt::meta_type{});
    ASSERT_NE(entt::resolve("integral"_hs), entt::meta_type{});
    ASSERT_NE(entt::resolve<int>(), entt::resolve("integral"_hs));

    ASSERT_NE(entt::resolve("foo"_hs), entt::meta_type{});
    ASSERT_NE(entt::resolve("bar"_hs), entt::meta_type{});

    ASSERT_EQ(entt::resolve("foo"_hs), entt::resolve<int>());
    ASSERT_EQ(entt::resolve("bar"_hs), entt::resolve("integral"_hs));

    ASSERT_EQ(entt::resolve<int>().data("value"_hs).get({}).cast<int>(), 1);
    ASSERT_EQ(entt::resolve("integral"_hs).data("value"_hs).get({}).cast<int>(), 2);
}

ENTT_DEBUG_TEST_F(MetaFactoryDeathTest, Type) {
    using namespace entt::literals;

    entt::meta_factory<int> factory{};
    entt::meta_factory<double> other{};

    factory.type("foo"_hs);

    ASSERT_DEATH(other.type("foo"_hs), "");
    ASSERT_DEATH(factory.type(entt::type_hash<double>::value()), "");
}

ENTT_DEBUG_TEST_F(MetaFactoryDeathTest, OverloadedType) {
    using namespace entt::literals;

    entt::meta_factory<int> factory{};
    entt::meta_factory<double> other{"overloaded"_hs};

    ASSERT_DEATH(other.type(entt::type_hash<int>::value()), "");
    ASSERT_DEATH(factory.type("overloaded"_hs), "");
}

TEST_F(MetaFactory, Base) {
    entt::meta_factory<clazz> factory{};
    decltype(std::declval<entt::meta_type>().base()) range{};

    ASSERT_NE(entt::resolve(entt::type_id<clazz>()), entt::meta_type{});
    ASSERT_EQ(entt::resolve(entt::type_id<base>()), entt::meta_type{});

    range = entt::resolve<clazz>().base();

    ASSERT_EQ(range.begin(), range.end());

    factory.base<base>();
    range = entt::resolve<clazz>().base();

    ASSERT_EQ(entt::resolve(entt::type_id<base>()), entt::meta_type{});
    ASSERT_NE(range.begin(), range.end());
    ASSERT_EQ(std::distance(range.begin(), range.end()), 1);
    ASSERT_EQ(range.begin()->first, entt::type_id<base>().hash());
    ASSERT_EQ(range.begin()->second.type().info(), entt::type_id<base>());
}

TEST_F(MetaFactory, Conv) {
    const clazz instance{3};
    entt::meta_factory<clazz> factory{};
    const entt::meta_any any = entt::forward_as_meta(instance);

    ASSERT_FALSE(any.allow_cast<int>());
    ASSERT_FALSE(any.allow_cast<std::string>());

    factory.conv<int>().conv<&clazz::to_string>();

    ASSERT_TRUE(any.allow_cast<int>());
    ASSERT_TRUE(any.allow_cast<std::string>());
    ASSERT_EQ(any.allow_cast<int>().cast<int>(), instance.get_int());
    ASSERT_EQ(any.allow_cast<std::string>().cast<std::string>(), clazz::to_string(instance));
}

TEST_F(MetaFactory, Ctor) {
    const std::array values{1, 3};
    entt::meta_factory<clazz> factory{};

    ASSERT_FALSE(entt::resolve<clazz>().construct(values[0u]));
    ASSERT_FALSE(entt::resolve<clazz>().construct(std::to_string(values[1u])));

    factory.ctor<int>().ctor<&clazz::from_string>();

    const auto instance = entt::resolve<clazz>().construct(values[0u]);
    const auto other = entt::resolve<clazz>().construct(std::to_string(values[1u]));

    ASSERT_TRUE(instance);
    ASSERT_TRUE(other);
    ASSERT_TRUE(instance.allow_cast<clazz>());
    ASSERT_TRUE(other.allow_cast<clazz>());
    ASSERT_EQ(instance.cast<const clazz &>().get_int(), values[0u]);
    ASSERT_EQ(other.cast<const clazz &>().get_int(), values[1u]);
}

TEST_F(MetaFactory, DataMemberObject) {
    using namespace entt::literals;

    base instance{'c'};
    entt::meta_factory<base> factory{};
    entt::meta_type type = entt::resolve<base>();

    ASSERT_FALSE(type.data("member"_hs));

    factory.data<&base::member>("member"_hs);
    type = entt::resolve<base>();

    ASSERT_TRUE(type.data("member"_hs));
    ASSERT_EQ(type.get("member"_hs, std::as_const(instance)), instance.member);
    ASSERT_EQ(type.get("member"_hs, instance), instance.member);
    ASSERT_FALSE(type.set("member"_hs, std::as_const(instance), instance.member));
    ASSERT_TRUE(type.set("member"_hs, instance, instance.member));
}

TEST_F(MetaFactory, DataFreeFunction) {
    using namespace entt::literals;

    clazz instance{1};
    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.data("member"_hs));

    factory.data<&clazz::set_double, &clazz::get_double>("member"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.data("member"_hs));
    ASSERT_EQ(type.get("member"_hs, std::as_const(instance)).cast<double>(), instance.get_int());
    ASSERT_EQ(type.get("member"_hs, instance).cast<double>(), instance.get_int());
    ASSERT_FALSE(type.set("member"_hs, std::as_const(instance), instance.member));
    ASSERT_TRUE(type.set("member"_hs, instance, instance.member));
}

TEST_F(MetaFactory, DataPointer) {
    using namespace entt::literals;

    entt::meta_factory<int> factory{};
    entt::meta_type type = entt::resolve<int>();

    ASSERT_FALSE(type.data("value"_hs));

    static int value = 1;
    factory.data<&value>("value"_hs);
    type = entt::resolve<int>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_EQ(type.get("value"_hs, {}), value);
    ASSERT_TRUE(type.set("value"_hs, {}, value));
}

TEST_F(MetaFactory, DataValue) {
    using namespace entt::literals;

    constexpr int value = 1;
    entt::meta_factory<int> factory{};
    entt::meta_type type = entt::resolve<int>();

    ASSERT_FALSE(type.data("value"_hs));

    factory.data<value>("value"_hs);
    type = entt::resolve<int>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_EQ(type.get("value"_hs, {}), value);
    ASSERT_FALSE(type.set("value"_hs, {}, value));
}

TEST_F(MetaFactory, DataGetterOnly) {
    using namespace entt::literals;

    clazz instance{1};
    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.data("value"_hs));

    factory.data<nullptr, &clazz::get_int>("value"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_EQ(type.get("value"_hs, std::as_const(instance)), instance.get_int());
    ASSERT_EQ(type.get("value"_hs, instance), instance.get_int());
    ASSERT_FALSE(type.set("value"_hs, std::as_const(instance), instance.get_int()));
    ASSERT_FALSE(type.set("value"_hs, instance, instance.get_int()));
}

TEST_F(MetaFactory, DataSetterGetter) {
    using namespace entt::literals;

    clazz instance{1};
    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.data("value"_hs));

    factory.data<&clazz::set_int, &clazz::get_int>("value"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_EQ(type.get("value"_hs, std::as_const(instance)), instance.get_int());
    ASSERT_EQ(type.get("value"_hs, instance), instance.get_int());
    ASSERT_FALSE(type.set("value"_hs, std::as_const(instance), instance.get_int()));
    ASSERT_TRUE(type.set("value"_hs, instance, instance.get_int()));
}

TEST_F(MetaFactory, DataOverwrite) {
    using namespace entt::literals;

    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.data("value"_hs));

    factory.data<nullptr, &clazz::get_int>("value"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_TRUE(type.data("value"_hs).is_const());

    factory.data<&clazz::set_int, &clazz::get_int>("value"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.data("value"_hs));
    ASSERT_FALSE(type.data("value"_hs).is_const());
}

TEST_F(MetaFactory, DataTypeNameClash) {
    using namespace entt::literals;

    const auto value = 2;
    entt::meta_factory<base> factory{};
    const auto member = entt::type_id<base>().hash();
    entt::meta_data data = entt::resolve<base>().data(member);

    ASSERT_FALSE(data);

    factory.data<&base::member>(member).custom<int>(value);
    data = entt::resolve<base>().data(member);

    ASSERT_TRUE(data);
    ASSERT_NE(static_cast<const int *>(data.custom()), nullptr);
    ASSERT_EQ(static_cast<int>(data.custom()), value);
}

TEST_F(MetaFactory, Func) {
    using namespace entt::literals;

    const clazz instance{1};
    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.func("func"_hs));

    factory.func<&clazz::get_int>("func"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.func("func"_hs));
    ASSERT_TRUE(type.invoke("func"_hs, instance));
    ASSERT_EQ(type.invoke("func"_hs, instance).cast<int>(), instance.get_int());
    ASSERT_FALSE(type.invoke("func"_hs, {}));
}

TEST_F(MetaFactory, FuncFreeFunction) {
    using namespace entt::literals;

    const clazz instance{1};
    entt::meta_factory<clazz> factory{};
    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.func("func"_hs));

    factory.func<&clazz::get_double>("func"_hs);
    type = entt::resolve<clazz>();

    ASSERT_TRUE(type.func("func"_hs));
    ASSERT_TRUE(type.invoke("func"_hs, instance));
    ASSERT_EQ(type.invoke("func"_hs, instance).cast<double>(), clazz::get_double(instance));
    ASSERT_FALSE(type.invoke("func"_hs, {}));
}

TEST_F(MetaFactory, FuncOverload) {
    using namespace entt::literals;

    clazz instance{1};
    entt::meta_factory<clazz> factory{};
    const entt::meta_type type = entt::resolve<clazz>();

    ASSERT_FALSE(type.func("func"_hs));

    factory.func<&clazz::set_int>("func"_hs);

    ASSERT_TRUE(type.func("func"_hs));
    ASSERT_FALSE(type.func("func"_hs).next());

    factory.func<&clazz::set_boxed_int>("func"_hs);

    ASSERT_TRUE(type.func("func"_hs));
    ASSERT_TRUE(type.func("func"_hs).next());
    ASSERT_FALSE(type.func("func"_hs).next().next());

    ASSERT_TRUE(type.invoke("func"_hs, instance, 2));
    ASSERT_EQ(instance.get_int(), 2);

    ASSERT_TRUE(type.invoke("func"_hs, instance, test::boxed_int{3}));
    ASSERT_EQ(instance.get_int(), 3);
}

TEST_F(MetaFactory, FuncTypeNameClash) {
    using namespace entt::literals;

    const auto value = 2;
    entt::meta_factory<clazz> factory{};
    const auto member = entt::type_id<base>().hash();
    entt::meta_func func = entt::resolve<base>().func(member);

    ASSERT_FALSE(func);

    factory.func<&clazz::get_int>(member).custom<int>(value);
    func = entt::resolve<clazz>().func(member);

    ASSERT_TRUE(func);
    ASSERT_NE(static_cast<const int *>(func.custom()), nullptr);
    ASSERT_EQ(static_cast<int>(func.custom()), value);
}

TEST_F(MetaFactory, Traits) {
    using namespace entt::literals;

    entt::meta_factory<clazz>{}
        .data<&base::member>("member"_hs)
        .func<&clazz::set_int>("func"_hs)
        .func<&clazz::set_boxed_int>("func"_hs);

    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_EQ(type.traits<test::meta_traits>(), test::meta_traits::none);
    ASSERT_EQ(type.data("member"_hs).traits<test::meta_traits>(), test::meta_traits::none);
    ASSERT_EQ(type.func("func"_hs).traits<test::meta_traits>(), test::meta_traits::none);
    ASSERT_EQ(type.func("func"_hs).next().traits<test::meta_traits>(), test::meta_traits::none);

    entt::meta_factory<clazz>{}
        .traits(test::meta_traits::one | test::meta_traits::three)
        .data<&base::member>("member"_hs)
        .traits(test::meta_traits::one)
        .func<&clazz::set_int>("func"_hs)
        .traits(test::meta_traits::two)
        .func<&clazz::set_boxed_int>("func"_hs)
        .traits(test::meta_traits::three);

    // traits are copied and never refreshed
    type = entt::resolve<clazz>();

    ASSERT_EQ(type.traits<test::meta_traits>(), test::meta_traits::one | test::meta_traits::three);
    ASSERT_EQ(type.data("member"_hs).traits<test::meta_traits>(), test::meta_traits::one);
    ASSERT_EQ(type.func("func"_hs).traits<test::meta_traits>(), test::meta_traits::two);
    ASSERT_EQ(type.func("func"_hs).next().traits<test::meta_traits>(), test::meta_traits::three);

    entt::meta_factory<clazz>{}
        .traits(test::meta_traits::one, true)
        .data<&base::member>("member"_hs)
        .traits(test::meta_traits::one | test::meta_traits::three, true)
        .func<&clazz::set_int>("func"_hs)
        .traits(test::meta_traits::one | test::meta_traits::three, true)
        .func<&clazz::set_boxed_int>("func"_hs)
        .traits(test::meta_traits::all, true);

    // traits are copied and never refreshed
    type = entt::resolve<clazz>();

    ASSERT_EQ(type.traits<test::meta_traits>(), test::meta_traits::three);
    ASSERT_EQ(type.data("member"_hs).traits<test::meta_traits>(), test::meta_traits::none);
    ASSERT_EQ(type.func("func"_hs).traits<test::meta_traits>(), test::meta_traits::two);
    ASSERT_EQ(type.func("func"_hs).next().traits<test::meta_traits>(), test::meta_traits::none);
}

TEST_F(MetaFactory, Custom) {
    using namespace entt::literals;

    entt::meta_factory<clazz>{}
        .data<&base::member>("member"_hs)
        .func<&clazz::set_int>("func"_hs)
        .func<&clazz::set_boxed_int>("func"_hs);

    entt::meta_type type = entt::resolve<clazz>();

    ASSERT_EQ(static_cast<const int *>(type.custom()), nullptr);
    ASSERT_EQ(static_cast<const int *>(type.data("member"_hs).custom()), nullptr);
    ASSERT_EQ(static_cast<const int *>(type.func("func"_hs).custom()), nullptr);
    ASSERT_EQ(static_cast<const int *>(type.func("func"_hs).next().custom()), nullptr);

    entt::meta_factory<clazz>{}
        .custom<int>(0)
        .data<&base::member>("member"_hs)
        .custom<int>(1)
        .func<&clazz::set_int>("func"_hs)
        .custom<int>(2)
        .func<&clazz::set_boxed_int>("func"_hs)
        .custom<int>(3);

    // custom data pointers are copied and never refreshed
    type = entt::resolve<clazz>();

    ASSERT_EQ(static_cast<int>(type.custom()), 0);
    ASSERT_EQ(static_cast<int>(type.data("member"_hs).custom()), 1);
    ASSERT_EQ(static_cast<int>(type.func("func"_hs).custom()), 2);
    ASSERT_EQ(static_cast<int>(type.func("func"_hs).next().custom()), 3);
}

TEST_F(MetaFactory, MetaReset) {
    using namespace entt::literals;

    entt::meta_ctx ctx{};

    entt::meta_factory<int>{}.type("global"_hs);
    entt::meta_factory<int>{ctx}.type("local"_hs);

    ASSERT_TRUE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset();

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset(ctx);

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_FALSE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_factory<int>{}.type("global"_hs);
    entt::meta_factory<int>{ctx}.type("local"_hs);

    ASSERT_TRUE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset<int>();

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset<int>(ctx);

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_FALSE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_factory<int>{}.type("global"_hs);
    entt::meta_factory<int>{ctx}.type("local"_hs);

    ASSERT_TRUE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset(entt::type_hash<int>::value());

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset(ctx, entt::type_hash<int>::value());

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_FALSE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_factory<int>{}.type("global"_hs);
    entt::meta_factory<int>{ctx}.type("local"_hs);

    ASSERT_TRUE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset("global"_hs);

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_TRUE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_reset(ctx, "local"_hs);

    ASSERT_FALSE(entt::resolve(entt::type_id<int>()));
    ASSERT_FALSE(entt::resolve(ctx, entt::type_id<int>()));

    entt::meta_factory<int>{"global"_hs};
    entt::meta_factory<int>{ctx, "local"_hs};

    ASSERT_TRUE(entt::resolve("global"_hs));
    ASSERT_TRUE(entt::resolve(ctx, "local"_hs));

    entt::meta_reset("global"_hs);

    ASSERT_FALSE(entt::resolve("global"_hs));
    ASSERT_TRUE(entt::resolve(ctx, "local"_hs));

    entt::meta_reset(ctx, "local"_hs);

    ASSERT_FALSE(entt::resolve("global"_hs));
    ASSERT_FALSE(entt::resolve(ctx, "local"_hs));
}
