#include <catch2/catch.hpp>
#include <xs/function.h>
#include <thread>

using namespace xs;
using namespace panda;

struct RefOnly {
    string s;
    RefOnly (string s) : s(s) {}
};

struct RefAny {
    string s;
    RefAny (string s) : s(s) {}
};

namespace xs {
    template<> struct Typemap<RefOnly&> : TypemapBase<RefOnly&> {
        static RefOnly in (const Simple& arg) {
            return RefOnly(arg.as_string() + "_in");
        }
        static Sv out (RefOnly& v, const Sv& = {}) { return Simple(v.s + "_out"); }
    };

    template<> struct Typemap<RefAny> : TypemapBase<RefAny> {
        static RefAny in (const Simple& arg) {
            return RefAny(arg.as_string() + "_inV");
        }
        static Sv out (const RefAny& v, const Sv& = {}) { return Simple(v.s + "_outV"); }
    };

    template<> struct Typemap<RefAny&> : TypemapBase<RefAny&> {
        static RefAny& in (const Simple& arg) {
            static RefAny r("");
            r.s = arg.as_string() + "_inR";
            return r;
        }
        static Sv out (RefAny& v, const Sv& = {}) { return Simple(v.s + "_outR"); }
    };
}

using vv_fn  = function<void()>;
using vi_fn  = function<void(int)>;
using iid_fn = function<int(int, panda::string_view)>;
using iis_fn = function<int(int, panda::string_view)>;

struct Data {
    int i;
    Data (int i) : i(i) {}
};

TEST_CASE("sub->function", "[function]") {
    eval("$MyTest::_marker = undef");
    auto marker = Stash("MyTest")["_marker"].scalar();

    SECTION("void()") {
        auto fn = xs::in<vv_fn>(Sub::create("$MyTest::_marker = 1"));
        fn();
        CHECK(marker.is_true());
    }
    SECTION("void(int)") {
        auto fn = xs::in<vi_fn>(Sub::create("$MyTest::_marker = shift"));
        fn(42);
        CHECK(Simple(marker) == 42);
    }
    SECTION("void(int) custom") {
        auto sub = Sub::create("$MyTest::_marker = shift");
        auto fn = xs::sub2function<vi_fn>(sub, [](int val) { return Simple(val + 100); });
        fn(42);
        CHECK(Simple(marker) == 142);
    }
    SECTION("int(int,string_view)") {
        auto sub = Sub::create("$MyTest::_marker = [@_]; return 10");
        auto fn = xs::in<iis_fn>(sub);
        auto res = fn(42, "the string");
        CHECK(res == 10);
        CHECK(Simple(Array(marker)[0]) == 42);
        CHECK(Simple(Array(marker)[1]) == "the string");
    }
    SECTION("int(int,string_view) custom") {
        auto sub = Sub::create("$MyTest::_marker = [@_]; return 10");

        auto fn = xs::sub2function<iis_fn>(
            sub,
            [=](const Sv& sv) { return SvIV(sv) + 10; },
            [](int val)       { return Simple(val + 100); }
        );

        auto res = fn(42, "a string");
        CHECK(res == 20);
        CHECK(Simple(Array(marker)[0]) == 142);
        CHECK(Simple(Array(marker)[1]) == "a string");
    }
    SECTION("sub->function->sub") {
        auto src = Sub::create("$MyTest::_marker = shift");
        auto fn = xs::in<vi_fn>(src);
        Sub sub = xs::out(fn);
        CHECK(sub == src);
        sub.call(Simple(43));
        CHECK(Simple(marker) == 43);
    }
    SECTION("custom when no typemap") {
        auto sub = Sub::create("$MyTest::_marker = $_[0]; return $_[0] + 100");

        auto fn = sub2function<function<Data(const Data&)>>(
            sub,
            [=](const Sv& sv)     { return Data((int)Simple(sv) + 1); },
            [] (const Data& data) { return Simple(data.i + 2); }
        );

        auto ret = fn(Data(100));
        CHECK(ret.i == 203);
        CHECK(Simple(marker) == 102);
    }
    #ifdef USE_ITHREADS
    SECTION("with threads") {
        eval("require threads");
        Stash threads("threads");
        int tid = threads.call<Simple>("tid");
        auto sub = Sub::create("$MyTest::_marker = threads->tid(); return $_[0] + 10");
        auto fn = xs::in<function<int(int)>>(sub);

        auto res = fn(1);
        CHECK(res == 11);
        CHECK(Simple(marker) == tid);

        function<void()> thr_fn = [&] { res = fn(2); };

        // fn() must call correct sub (from correct interpreter thread)
        Stash("threads").call<Object>("create", xs::out(thr_fn)).call("join");
        CHECK(res == 12);
        CHECK(Simple(marker) == tid); // marker in master thread should not be changed

        // check that fn() is not corrupted by svt_dup or svt_free
        res = fn(3);
        CHECK(res == 13);
        CHECK(Simple(marker) == tid);
    }
    #endif
}

TEST_CASE("function->sub", "[function]") {
    int ecnt = 0;
    int cnt = 0;
    SECTION("void()") {
        ecnt = 1;
        vv_fn fn = [&](){ cnt++; };
        auto sub = function2sub(fn);
        sub.call<void>();
    }
    SECTION("void(int)") {
        ecnt = 1;
        vi_fn fn = [&](int val) {
            CHECK(val == 42);
            cnt++;
        };
        auto sub = function2sub(fn);
        sub.call<void>(Simple(42));
    }
    SECTION("void(int) custom") {
        ecnt = 1;
        vi_fn fn = [&](int val) {
            CHECK(val == 942);
            cnt++;
        };
        auto sub = function2sub(fn, [](const Sv& sv){ return (int)Simple(sv) + 900; });
        sub.call<void>(Simple(42));
    }
    SECTION("int(int, double)") {
        ecnt = 1;
        iid_fn fn = [&](int i, panda::string_view d) {
            CHECK(i == 42);
            CHECK(d == "hello");
            cnt++;
            return 255;
        };
        Sub sub = xs::out(fn);
        Scalar ret = sub.call(Simple(42), Simple("hello"));
        CHECK(Simple(ret) == 255);
    }
    SECTION("int(int, double) custom") {
        ecnt = 1;
        iid_fn fn = [&](int i, panda::string_view d) {
            CHECK(i == 842);
            CHECK(d == "hi");
            cnt++;
            return 255;
        };
        auto sub = function2sub(fn,
            [](int r) { return Simple(r - 200); },
            [](const Sv& sv) { return (int)Simple(sv) + 800; }
        );
        Scalar ret = sub.call(Simple(42), Simple("hi"));
        CHECK(Simple(ret) == 55);
    }
    SECTION("function->sub->function") {
        ecnt = 2;
        vi_fn fn = [&](int val) {
            CHECK(val == 42);
            cnt++;
        };
        auto sub = function2sub(fn);
        sub.call<void>(Simple(42));
        auto fn2 = xs::in<vi_fn>(sub);
        fn2(42);
        CHECK(fn == fn2);
    }
    SECTION("removing references") {
        ecnt = 1;
        function<void(int&)> fn = [&](int& val) {
            CHECK(val == 42);
            cnt++;
        };
        auto sub = function2sub(fn);
        sub.call<void>(Simple(42));
    }
    SECTION("custom when no typemap") {
        function<Data(const Data&)> fn = [&](const Data& data) { return data.i + 100; };
        auto sub = function2sub(fn,
            [](const Data& data) { return Simple(data.i + 1000); },
            [](const Sv& sv)     { return Data((int)Simple(sv) + 10); }
        );
        Scalar ret = sub.call(Simple(1));
        CHECK(Simple(ret) == 1111);
    }
    SECTION("use ref typemap when it exists") {
        SECTION("exists only ref typemap") {
            using ref_fn  = function<RefOnly&(RefOnly&)>;
            ref_fn fn = [](RefOnly& v) -> RefOnly& { v.s += "_call"; return v; };
            auto sub = function2sub(fn);
            Scalar ret = sub.call(Simple("hello"));
            CHECK(Simple(ret) == "hello_in_call_out");
        }
        SECTION("prefer ref when both exists") {
            using val_fn = function<RefAny(RefAny)>;
            val_fn vfn = [](const RefAny& v) -> RefAny { return RefAny(v.s + "_call"); };
            auto sub = function2sub(vfn);
            Scalar ret = sub.call(Simple("hello"));
            CHECK(Simple(ret) == "hello_inV_call_outV");

            using ref_fn = function<RefAny&(RefAny&)>;
            ref_fn rfn = [](RefAny& v) -> RefAny& { v.s += "_call"; return v; };
            sub = function2sub(rfn);
            ret = sub.call(Simple("hello"));
            CHECK(Simple(ret) == "hello_inR_call_outR");
        }
    }
    #ifdef USE_ITHREADS
    SECTION("with threads") {
        std::thread::id id;
        eval("require threads");
        function<int()> fn = [&]{ id = std::this_thread::get_id(); return 55; };
        Sub sub = xs::out(fn);
        // sub must clone it's magic ptr on svt_dup
        Object thr = Stash("threads").call("create", Ref::create(sub));
        Simple res = thr.call("join");
        CHECK(res == 55);
        CHECK(id != std::this_thread::get_id());
        //check that sub isn't broken by svt_dup
        res = sub.call();
        CHECK(res == 55);
        CHECK(id == std::this_thread::get_id());
    }
    #endif

    CHECK(cnt == ecnt);
}

//uint64_t bench_vv (Sub sub, int cnt) {
//    RETVAL = 0;
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)sub2function<vv_fn>(sub).func.get();
//    }
//}
//
//uint64_t bench_vi (Sub sub, int cnt) {
//    RETVAL = 0;
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)sub2function<vi_fn>(sub).func.get();
//    }
//}
//
//uint64_t bench_iis (Sub sub, int cnt) {
//    RETVAL = 0;
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)sub2function<iis_fn>(sub).func.get();
//    }
//}
//
//uint64_t bench_vvR (int cnt) {
//    RETVAL = 0;
//    vv_fn fn = [](){};
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)function2sub(fn).get();
//    }
//}
//
//uint64_t bench_viR (int cnt) {
//    RETVAL = 0;
//    vi_fn fn = [](int){};
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)function2sub(fn).get();
//    }
//}
//
//uint64_t bench_iisR (int cnt) {
//    RETVAL = 0;
//    iis_fn fn = [](int a, string_view d) -> int { return a + d.length(); };
//    for (int i = 0; i < cnt; ++i) {
//        RETVAL += (uint64_t)function2sub(fn).get();
//    }
//}
