/*******************************************************************************
 * This file is part of the "Enduro2D"
 * For conditions of distribution and use, see copyright notice in LICENSE.md
 * Copyright (C) 2018-2020, by Matvey Cherevko (blackmatov@gmail.com)
 ******************************************************************************/

#include "_utils.hpp"
using namespace e2d;

#include <random>

namespace
{
    struct ilist_tag1 {};
    struct ilist_tag2 {};

    class obj_t
        : public intrusive_list_hook<ilist_tag1>
        , public intrusive_list_hook<ilist_tag2>
    {
    public:
        int i{42};
        obj_t() = default;
        obj_t(int ni) : i(ni) {}
    };

    bool operator<(const obj_t& l, const obj_t& r) noexcept {
        return l.i < r.i;
    }
}

TEST_CASE("intrusive_list") {
    {
        intrusive_list<obj_t, ilist_tag1> l;
        REQUIRE(l.empty());
        REQUIRE_FALSE(l.size());
    }
    SECTION("push_back/push_front") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            auto o3 = std::make_unique<obj_t>(3);
            auto o4 = std::make_unique<obj_t>(4);

            intrusive_list<obj_t, ilist_tag1> l;

            l.push_back(*o1);
            REQUIRE(l.back().i == 1);
            REQUIRE(l.front().i == 1);

            l.push_back(*o2);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 1);

            l.push_front(*o3);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 3);

            l.push_front(*o4);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 4);
        }
    }
    SECTION("pop_back/pop_front") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            auto o3 = std::make_unique<obj_t>(3);
            auto o4 = std::make_unique<obj_t>(4);

            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);
            l.push_back(*o4);

            l.pop_front();
            REQUIRE(l.front().i == 2);
            REQUIRE(l.back().i == 4);

            l.pop_back();
            REQUIRE(l.front().i == 2);
            REQUIRE(l.back().i == 3);

            l.pop_front();
            REQUIRE(l.front().i == 3);
            REQUIRE(l.back().i == 3);

            l.pop_back();
            REQUIRE(l.empty());
        }
    }
    SECTION("pop_back_and_dispose/pop_front_and_dispose") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            auto o3 = std::make_unique<obj_t>(3);
            auto o4 = std::make_unique<obj_t>(4);

            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);
            l.push_back(*o4);

            std::size_t dispose_count = 0;
            l.pop_front_and_dispose([&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 1);
            });
            REQUIRE(l.front().i == 2);
            REQUIRE(l.back().i == 4);

            l.pop_back_and_dispose([&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 4);
            });
            REQUIRE(l.front().i == 2);
            REQUIRE(l.back().i == 3);

            l.pop_front_and_dispose([&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 2);
            });
            REQUIRE(l.front().i == 3);
            REQUIRE(l.back().i == 3);

            l.pop_front_and_dispose([&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 3);
            });
            REQUIRE(l.empty());
            REQUIRE(dispose_count == 4);
        }
    }
    SECTION("empty/size/clear/clear_and_dispose") {
        auto o1 = std::make_unique<obj_t>(1);
        auto o2 = std::make_unique<obj_t>(2);
        auto o3 = std::make_unique<obj_t>(3);
        {
            intrusive_list<obj_t, ilist_tag1> l;
            REQUIRE(l.empty());
            REQUIRE_FALSE(l.size());

            l.push_back(*o1);
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 1u);

            l.push_front(*o2);
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 2u);

            l.push_back(*o3);
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 3u);

            l.pop_front();
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 2u);

            l.pop_back();
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 1u);

            l.pop_front();
            REQUIRE(l.empty());
            REQUIRE_FALSE(l.size());
        }
        {
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);
            REQUIRE_FALSE(l.empty());
            REQUIRE(l.size() == 3u);
        }
        {
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);
            l.clear();
            REQUIRE(l.empty());
        }
        {
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);

            std::size_t dispose_count = 0;
            std::array<int, 3> obj_values{3,2,1};
            l.clear_and_dispose([&dispose_count, &obj_values](obj_t* o){
                REQUIRE(obj_values[dispose_count] == o->i);
                ++dispose_count;
            });
            REQUIRE(l.empty());
            REQUIRE(dispose_count == 3);
        }
    }
    SECTION("swap") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            auto o3 = std::make_unique<obj_t>(3);
            auto o4 = std::make_unique<obj_t>(4);

            intrusive_list<obj_t, ilist_tag1> l1;
            intrusive_list<obj_t, ilist_tag1> l2;

            l1.push_back(*o1);
            l1.push_back(*o2);

            l2.push_back(*o3);
            l2.push_back(*o4);

            l1.swap(l2);

            REQUIRE(l1.back().i == 4);
            REQUIRE(l1.front().i == 3);

            REQUIRE(l2.back().i == 2);
            REQUIRE(l2.front().i == 1);
        }
    }
    SECTION("insert/erase/erase_and_dispose") {
        auto o1 = std::make_unique<obj_t>(1);
        auto o2 = std::make_unique<obj_t>(2);
        auto o3 = std::make_unique<obj_t>(3);
        auto o4 = std::make_unique<obj_t>(4);
        {
            intrusive_list<obj_t, ilist_tag1> l;

            REQUIRE(l.insert(l.end(), *o1)->i == 1); // 1
            {
                auto i = l.insert(++l.begin(), *o2); // 1 2
                REQUIRE(std::distance(l.begin(), i) == 1);
            }
            {
                auto i = l.insert(l.begin(), *o3); // 3 1 2
                REQUIRE(std::distance(l.begin(), i) == 0);
            }
            {
                auto iter = l.begin();
                REQUIRE(iter->i == 3);
                REQUIRE((++iter)->i == 1);
                REQUIRE((++iter)->i == 2);
                REQUIRE(++iter == l.end());
            }

            // 3 1 2 4
            l.push_back(*o4);

            // 3 1 2
            REQUIRE(l.erase(--l.end()) == l.end());
            REQUIRE(l.size() == 3);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 3);

            // 3 2
            REQUIRE(l.erase(++l.begin())->i == 2);
            REQUIRE(l.size() == 2);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 3);

            // 2
            REQUIRE(l.erase(l.begin())->i == 2);
            REQUIRE(l.size() == 1);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 2);
        }
        {
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            l.push_back(*o3);
            l.push_back(*o4);
            std::size_t dispose_count = 0;
            l.erase_and_dispose(l.begin(), [&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 1);
            });
            l.erase_and_dispose(++l.begin(), [&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 3);
            });
            l.erase_and_dispose(++l.begin(), [&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 4);
            });
            l.erase_and_dispose(l.begin(), [&dispose_count](obj_t* o){
                ++dispose_count;
                REQUIRE(o->i == 2);
            });
            REQUIRE(dispose_count == 4);
        }
    }
    SECTION("iterator_to") {
        auto o1 = std::make_unique<obj_t>(1);
        auto o2 = std::make_unique<obj_t>(2);
        auto o3 = std::make_unique<obj_t>(3);
        auto o4 = std::make_unique<obj_t>(4);

        intrusive_list<obj_t, ilist_tag1> l;

        l.push_back(*o1);
        l.push_back(*o2);
        l.push_back(*o3);
        l.push_back(*o4);

        REQUIRE(l.iterator_to(*o1) == l.begin());
        REQUIRE(l.iterator_to(*o2) == ++l.begin());
        {
            const auto& co3 = o3;
            const auto& co4 = o4;
            REQUIRE(l.iterator_to(*co3) == --(--l.end()));
            REQUIRE(l.iterator_to(*co4) == --l.end());
        }
    }
    SECTION("auto_unlink") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);

            o1.reset();
            REQUIRE(l.size() == 1u);
            REQUIRE(l.back().i == 2);
            REQUIRE(l.front().i == 2);
        }
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);

            o2.reset();
            REQUIRE(l.size() == 1u);
            REQUIRE(l.back().i == 1);
            REQUIRE(l.front().i == 1);
        }
    }
    SECTION("multitag") {
        {
            auto o1 = std::make_unique<obj_t>();
            intrusive_list<obj_t, ilist_tag1> l1;
            intrusive_list<obj_t, ilist_tag2> l2;
            l1.push_back(*o1);
            l2.push_back(*o1);
            REQUIRE(l1.size() == 1);
            REQUIRE(l2.size() == 1);
            REQUIRE(l1.begin()->i == 42);
            REQUIRE(l2.begin()->i == 42);
            l2.pop_back();
            REQUIRE(l1.size() == 1);
            REQUIRE(l1.begin()->i == 42);
            REQUIRE(l2.empty());
        }
    }
    SECTION("iterator") {
        {
            auto o1 = std::make_unique<obj_t>(1);
            auto o2 = std::make_unique<obj_t>(2);
            intrusive_list<obj_t, ilist_tag1> l;
            l.push_back(*o1);
            l.push_back(*o2);
            std::transform(l.begin(), l.end(), l.begin(), [](obj_t& o){
                return o.i * 5;
            });
            {
                auto iter = l.begin();
                REQUIRE((*iter).i == 5);
                REQUIRE((++iter)->i == 10);
            }
            {
                auto iter = l.rbegin();
                REQUIRE((*iter).i == 10);
                REQUIRE((++iter)->i == 5);
            }
            REQUIRE(15 == std::accumulate(l.begin(), l.end(), 0, [](int acc, obj_t& o){
                return acc + o.i;
            }));
            REQUIRE(15 == std::accumulate(l.rbegin(), l.rend(), 0, [](int acc, obj_t& o){
                return acc + o.i;
            }));
            {
                const auto& cl = l;
                REQUIRE(15 == std::accumulate(cl.begin(), cl.end(), 0, [](int acc, const obj_t& o){
                    return acc + o.i;
                }));
                REQUIRE(15 == std::accumulate(cl.rbegin(), cl.rend(), 0, [](int acc, const obj_t& o){
                    return acc + o.i;
                }));
            }
            {
                REQUIRE(l.begin() == l.begin());
                REQUIRE(l.begin() == l.cbegin());
                REQUIRE(l.begin() != l.end());
                REQUIRE(l.begin() != l.cend());

                REQUIRE(l.cbegin() == l.begin());
                REQUIRE(l.cbegin() == l.cbegin());
                REQUIRE(l.cbegin() != l.end());
                REQUIRE(l.cbegin() != l.cend());

                REQUIRE(l.end() == l.end());
                REQUIRE(l.end() == l.cend());
                REQUIRE(l.end() != l.begin());
                REQUIRE(l.end() != l.cbegin());

                REQUIRE(l.cend() == l.end());
                REQUIRE(l.cend() == l.cend());
                REQUIRE(l.cend() != l.begin());
                REQUIRE(l.cend() != l.cbegin());
            }
            {
                REQUIRE(l.rbegin() == l.rbegin());
                REQUIRE(l.rbegin() == l.crbegin());
                REQUIRE(l.rbegin() != l.rend());
                REQUIRE(l.rbegin() != l.crend());

                REQUIRE(l.crbegin() == l.rbegin());
                REQUIRE(l.crbegin() == l.crbegin());
                REQUIRE(l.crbegin() != l.rend());
                REQUIRE(l.crbegin() != l.crend());

                REQUIRE(l.rend() == l.rend());
                REQUIRE(l.rend() == l.crend());
                REQUIRE(l.rend() != l.rbegin());
                REQUIRE(l.rend() != l.crbegin());

                REQUIRE(l.crend() == l.rend());
                REQUIRE(l.crend() == l.crend());
                REQUIRE(l.crend() != l.rbegin());
                REQUIRE(l.crend() != l.crbegin());
            }
        }
    }
    SECTION("sort") {
        {
            std::random_device rd;
            std::mt19937 rnd(rd());

            std::uniform_int_distribution<u32> uni_size(0, 1024);
            std::uniform_int_distribution<i32> uni_number(-65536, 65535);

            const auto pred = [](const obj_t& l, const obj_t& r) noexcept {
                return l.i < r.i;
            };

            const auto r_pred = [](const obj_t& l, const obj_t& r) noexcept {
                return l.i > r.i;
            };

            for ( std::size_t i = 0; i < 1024; ++i ) {
                vector<std::unique_ptr<obj_t>> objs;
                intrusive_list<obj_t, ilist_tag1> l;
                for ( u32 b = 0, e = uni_size(rnd); b < e; ++b ) {
                    objs.push_back(std::make_unique<obj_t>(uni_number(rnd)));
                    l.push_back(*objs.back());
                }
                l.sort();
                REQUIRE(std::is_sorted(l.cbegin(), l.cend()));
                l.sort(r_pred);
                REQUIRE(std::is_sorted(l.cbegin(), l.cend(), r_pred));
                l.sort(pred);
                REQUIRE(std::is_sorted(l.cbegin(), l.cend(), pred));
            }
        }
    }
}
