#include <gtest/gtest.h>
#include <type_traits>
#include <cassert>
#include <map>
#include <string>
#include <duktape.h>
#include <entt/entity/registry.hpp>

template<typename Type>
struct tag { using type = Type; };

struct position {
    double x;
    double y;
};

struct renderable {};

struct duktape_runtime {
    std::map<duk_uint_t, std::string> components;
};

template<typename Comp>
duk_ret_t set(duk_context *ctx, entt::registry &registry) {
    const auto entity = duk_require_uint(ctx, 0);

    if constexpr(std::is_same_v<Comp, position>) {
        const auto x = duk_require_number(ctx, 2);
        const auto y = duk_require_number(ctx, 3);
        registry.assign_or_replace<position>(entity, x, y);
    } else if constexpr(std::is_same_v<Comp, duktape_runtime>) {
        const auto type = duk_require_uint(ctx, 1);

        duk_dup(ctx, 2);

        if(!registry.has<duktape_runtime>(entity)) {
            registry.assign<duktape_runtime>(entity).components[type] = duk_json_encode(ctx, -1);
        } else {
            registry.get<duktape_runtime>(entity).components[type] = duk_json_encode(ctx, -1);
        }

        duk_pop(ctx);
    } else {
        registry.assign_or_replace<Comp>(entity);
    }

    return 0;
}

template<typename Comp>
duk_ret_t unset(duk_context *ctx, entt::registry &registry) {
    const auto entity = duk_require_uint(ctx, 0);

    if constexpr(std::is_same_v<Comp, duktape_runtime>) {
        const auto type = duk_require_uint(ctx, 1);

        auto &components = registry.get<duktape_runtime>(entity).components;
        assert(components.find(type) != components.cend());
        components.erase(type);

        if(components.empty()) {
            registry.remove<duktape_runtime>(entity);
        }
    } else {
        registry.remove<Comp>(entity);
    }

    return 0;
}

template<typename Comp>
duk_ret_t has(duk_context *ctx, entt::registry &registry) {
    const auto entity = duk_require_uint(ctx, 0);

    if constexpr(std::is_same_v<Comp, duktape_runtime>) {
        duk_push_boolean(ctx, registry.has<duktape_runtime>(entity));

        if(registry.has<duktape_runtime>(entity)) {
            const auto type = duk_require_uint(ctx, 1);
            const auto &components = registry.get<duktape_runtime>(entity).components;
            duk_push_boolean(ctx, components.find(type) != components.cend());
        } else {
            duk_push_false(ctx);
        }
    } else {
        duk_push_boolean(ctx, registry.has<Comp>(entity));
    }

    return 1;
}

template<typename Comp>
duk_ret_t get(duk_context *ctx, entt::registry &registry) {
    [[maybe_unused]] const auto entity = duk_require_uint(ctx, 0);

    if constexpr(std::is_same_v<Comp, position>) {
        const auto &pos = registry.get<position>(entity);

        const auto idx = duk_push_object(ctx);

        duk_push_string(ctx, "x");
        duk_push_number(ctx, pos.x);
        duk_def_prop(ctx, idx, DUK_DEFPROP_HAVE_VALUE);

        duk_push_string(ctx, "y");
        duk_push_number(ctx, pos.y);
        duk_def_prop(ctx, idx, DUK_DEFPROP_HAVE_VALUE);
    } if constexpr(std::is_same_v<Comp, duktape_runtime>) {
        const auto type = duk_require_uint(ctx, 1);

        auto &runtime = registry.get<duktape_runtime>(entity);
        assert(runtime.components.find(type) != runtime.components.cend());

        duk_push_string(ctx, runtime.components[type].c_str());
        duk_json_decode(ctx, -1);
    } else {
        assert(registry.has<Comp>(entity));
        duk_push_object(ctx);
    }

    return 1;
}

class duktape_registry {
    // I'm pretty sure I won't have more than 99 components in the example
    static constexpr entt::registry::component_type udef = 100;

    struct func_map {
        using func_type = duk_ret_t(*)(duk_context *, entt::registry &);

        func_type set;
        func_type unset;
        func_type has;
        func_type get;
    };

    template<typename... Comp>
    void reg() {
        ((func[registry.type<Comp>()] = {
                &::set<Comp>,
                &::unset<Comp>,
                &::has<Comp>,
                &::get<Comp>
        }), ...);
    }

    static duktape_registry & instance(duk_context *ctx) {
        duk_push_this(ctx);

        duk_push_string(ctx, DUK_HIDDEN_SYMBOL("dreg"));
        duk_get_prop(ctx, -2);
        auto &dreg = *static_cast<duktape_registry *>(duk_require_pointer(ctx, -1));
        duk_pop_2(ctx);

        return dreg;
    }

    template<func_map::func_type func_map::*Op>
    static duk_ret_t invoke(duk_context *ctx) {
        auto &dreg = instance(ctx);
        auto &func = dreg.func;
        auto &registry = dreg.registry;
        auto type = duk_require_uint(ctx, 1);

        if(type >= udef) {
            type = registry.type<duktape_runtime>();
        }

        assert(func.find(type) != func.cend());

        return (func[type].*Op)(ctx, registry);
    }

public:
    duktape_registry(entt::registry &ref)
        : registry{ref}
    {
        reg<position, renderable, duktape_runtime>();
    }

    static duk_ret_t identifier(duk_context *ctx) {
        static auto next = udef;
        duk_push_uint(ctx, next++);
        return 1;
    }

    static duk_ret_t create(duk_context *ctx) {
        auto &dreg = instance(ctx);
        duk_push_uint(ctx, dreg.registry.create());
        return 1;
    }

    static duk_ret_t set(duk_context *ctx) {
        return invoke<&func_map::set>(ctx);
    }

    static duk_ret_t unset(duk_context *ctx) {
        return invoke<&func_map::unset>(ctx);
    }

    static duk_ret_t has(duk_context *ctx) {
        return invoke<&func_map::has>(ctx);
    }

    static duk_ret_t get(duk_context *ctx) {
        return invoke<&func_map::get>(ctx);
    }

    static duk_ret_t entities(duk_context *ctx) {
        const duk_idx_t nargs = duk_get_top(ctx);
        auto &dreg = instance(ctx);
        duk_uarridx_t pos = 0;

        duk_push_array(ctx);

        std::vector<typename entt::registry::component_type> components;
        std::vector<typename entt::registry::component_type> runtime;

        for(duk_idx_t arg = 0; arg < nargs; arg++) {
            auto type = duk_require_uint(ctx, arg);

            if(type < udef) {
                components.push_back(type);
            } else {
                if(runtime.empty()) {
                    components.push_back(dreg.registry.type<duktape_runtime>());
                }

                runtime.push_back(type);
            }
        }

        auto view = dreg.registry.runtime_view(components.cbegin(), components.cend());

        for(const auto entity: view) {
            if(runtime.empty()) {
                duk_push_uint(ctx, entity);
                duk_put_prop_index(ctx, -2, pos++);
            } else {
                const auto &others = dreg.registry.get<duktape_runtime>(entity).components;
                const auto match = std::all_of(runtime.cbegin(), runtime.cend(), [&others](const auto type) {
                    return others.find(type) != others.cend();
                });

                if(match) {
                    duk_push_uint(ctx, entity);
                    duk_put_prop_index(ctx, -2, pos++);
                }
            }
        }

        return 1;
    }

private:
    std::map<duk_uint_t, func_map> func;
    entt::registry &registry;
};

const duk_function_list_entry js_duktape_registry_methods[] = {
    { "identifier", &duktape_registry::identifier, 0 },
    { "create", &duktape_registry::create, 0 },
    { "set", &duktape_registry::set, DUK_VARARGS },
    { "unset", &duktape_registry::unset, 2 },
    { "has", &duktape_registry::has, 2 },
    { "get", &duktape_registry::get, 2 },
    { "entities", &duktape_registry::entities, DUK_VARARGS },
    { nullptr, nullptr, 0 }
};

void export_types(duk_context *context, entt::registry &registry) {
    auto export_type = [](auto *ctx, auto &reg, auto idx, auto type, const auto *name) {
        duk_push_string(ctx, name);
        duk_push_uint(ctx, reg.template type<typename decltype(type)::type>());
        duk_def_prop(ctx, idx, DUK_DEFPROP_HAVE_VALUE | DUK_DEFPROP_CLEAR_WRITABLE);
    };

    auto idx = duk_push_object(context);

    export_type(context, registry, idx, tag<position>{}, "position");
    export_type(context, registry, idx, tag<renderable>{}, "renderable");

    duk_put_global_string(context, "Types");
}

void export_duktape_registry(duk_context *ctx, duktape_registry &dreg) {
    auto idx = duk_push_object(ctx);

    duk_push_string(ctx, DUK_HIDDEN_SYMBOL("dreg"));
    duk_push_pointer(ctx, &dreg);
    duk_put_prop(ctx, idx);

    duk_put_function_list(ctx, idx, js_duktape_registry_methods);
    duk_put_global_string(ctx, "Registry");
}

TEST(Mod, Duktape) {
    entt::registry registry;
    duktape_registry dreg{registry};
    duk_context *ctx = duk_create_heap_default();

    if(!ctx) {
        FAIL();
    }

    export_types(ctx, registry);
    export_duktape_registry(ctx, dreg);

    const char *s0 = ""
            "Types[\"PLAYING_CHARACTER\"] = Registry.identifier();"
            "Types[\"VELOCITY\"] = Registry.identifier();"
            "";

    if(duk_peval_string(ctx, s0)) {
        FAIL();
    }

    const auto e0 = registry.create();
    registry.assign<position>(e0, 0., 0.);
    registry.assign<renderable>(e0);

    const auto e1 = registry.create();
    registry.assign<position>(e1, 0., 0.);

    const char *s1 = ""
            "Registry.entities(Types.position, Types.renderable).forEach(function(entity) {"
                "Registry.set(entity, Types.position, 100., 100.);"
            "});"
            "var entity = Registry.create();"
            "Registry.set(entity, Types.position, 100., 100.);"
            "Registry.set(entity, Types.renderable);"
            "";

    if(duk_peval_string(ctx, s1)) {
        FAIL();
    }

    ASSERT_EQ(registry.view<duktape_runtime>().size(), 0u);
    ASSERT_EQ(registry.view<position>().size(), 3u);
    ASSERT_EQ(registry.view<renderable>().size(), 2u);

    registry.view<position>().each([&registry](auto entity, const auto &position) {
        ASSERT_FALSE(registry.has<duktape_runtime>(entity));

        if(registry.has<renderable>(entity)) {
            ASSERT_EQ(position.x, 100.);
            ASSERT_EQ(position.y, 100.);
        } else {
            ASSERT_EQ(position.x, 0.);
            ASSERT_EQ(position.y, 0.);
        }
    });

    const char *s2 = ""
            "Registry.entities(Types.position).forEach(function(entity) {"
                "if(!Registry.has(entity, Types.renderable)) {"
                    "Registry.set(entity, Types.VELOCITY, { \"dx\": -100., \"dy\": -100. });"
                    "Registry.set(entity, Types.PLAYING_CHARACTER, {});"
                "}"
            "});"
            "";

    if(duk_peval_string(ctx, s2)) {
        FAIL();
    }

    ASSERT_EQ(registry.view<duktape_runtime>().size(), 1u);
    ASSERT_EQ(registry.view<position>().size(), 3u);
    ASSERT_EQ(registry.view<renderable>().size(), 2u);

    registry.view<duktape_runtime>().each([](const duktape_runtime &runtime) {
        ASSERT_EQ(runtime.components.size(), 2u);
    });

    const char *s3 = ""
            "Registry.entities(Types.position, Types.renderable, Types.VELOCITY, Types.PLAYING_CHARACTER).forEach(function(entity) {"
                "var velocity = Registry.get(entity, Types.VELOCITY);"
                "Registry.set(entity, Types.position, velocity.dx, velocity.dy)"
            "});"
            "";

    if(duk_peval_string(ctx, s3)) {
        FAIL();
    }

    ASSERT_EQ(registry.view<duktape_runtime>().size(), 1u);
    ASSERT_EQ(registry.view<position>().size(), 3u);
    ASSERT_EQ(registry.view<renderable>().size(), 2u);

    registry.view<position, renderable, duktape_runtime>().each([](const position &position, auto &&...) {
        ASSERT_EQ(position.x, -100.);
        ASSERT_EQ(position.y, -100.);
    });

    const char *s4 = ""
            "Registry.entities(Types.VELOCITY, Types.PLAYING_CHARACTER).forEach(function(entity) {"
                "Registry.unset(entity, Types.VELOCITY);"
                "Registry.unset(entity, Types.PLAYING_CHARACTER);"
            "});"
            "Registry.entities(Types.position).forEach(function(entity) {"
                "Registry.unset(entity, Types.position);"
            "});"
            "";

    if(duk_peval_string(ctx, s4)) {
        FAIL();
    }

    ASSERT_EQ(registry.view<duktape_runtime>().size(), 0u);
    ASSERT_EQ(registry.view<position>().size(), 0u);
    ASSERT_EQ(registry.view<renderable>().size(), 2u);

    duk_destroy_heap(ctx);
}
