//
// immer: immutable data structures for C++
// Copyright (C) 2016, 2017, 2018 Juan Pedro Bolivar Puente
//
// This software is distributed under the Boost Software License, Version 1.0.
// See accompanying file LICENSE or copy at http://boost.org/LICENSE_1_0.txt
//

#include "test/dada.hpp"
#include "test/transient_tester.hpp"
#include "test/util.hpp"

#include <immer/algorithm.hpp>

#include <boost/range/adaptors.hpp>
#include <boost/range/irange.hpp>
#include <catch2/catch.hpp>

#include <algorithm>
#include <array>
#include <numeric>
#include <vector>

#ifndef FLEX_VECTOR_T
#error "define the vector template to use in FLEX_VECTOR_T"
#endif

#ifndef FLEX_VECTOR_TRANSIENT_T
#error "define the vector template to use in FLEX_VECTOR_TRANSIENT_T"
#endif

#ifndef VECTOR_T
#error "define the vector template to use in VECTOR_T"
#endif

IMMER_RANGES_CHECK(
    std::ranges::random_access_range<FLEX_VECTOR_T<std::string>>);
IMMER_RANGES_CHECK(
    std::ranges::random_access_range<FLEX_VECTOR_TRANSIENT_T<std::string>>);

template <typename V = VECTOR_T<unsigned>>
auto make_test_flex_vector(unsigned min, unsigned max)
{
    auto v = V{};
    for (auto i = min; i < max; ++i)
        v = v.push_back({i});
    return v;
}

template <typename V = FLEX_VECTOR_T<unsigned>>
auto make_test_flex_vector_front(unsigned min, unsigned max)
{
    auto v = V{};
    for (auto i = max; i > min;)
        v = v.push_front({--i});
    return v;
}

TEST_CASE("from flex_vector and to flex_vector")
{
    constexpr auto n = 100u;

    auto v = make_test_flex_vector(0, n).transient();
    CHECK_VECTOR_EQUALS(v, boost::irange(0u, n));

    auto p = v.persistent();
    CHECK_VECTOR_EQUALS(p, boost::irange(0u, n));
}

TEST_CASE("adopt regular vector contents")
{
    const auto n = 666u;
    auto v       = VECTOR_T<unsigned>{};
    for (auto i = 0u; i < n; ++i) {
        v       = v.push_back(i);
        auto fv = FLEX_VECTOR_TRANSIENT_T<unsigned>{v.transient()};
        CHECK_VECTOR_EQUALS_AUX(v, fv, [](auto&& v) { return &v; });
    }
}

TEST_CASE("drop move")
{
    using vector_t = FLEX_VECTOR_T<unsigned>;

    auto v = vector_t{};

    auto check_move = [&](vector_t&& x) -> vector_t&& {
        if (vector_t::memory_policy::use_transient_rvalues)
            CHECK(&x == &v);
        else
            CHECK(&x != &v);
        return std::move(x);
    };

    v = v.push_back(0).push_back(1);

    auto addr_before = &v[0];
    v                = check_move(std::move(v).drop(1));
    auto addr_after  = &v[0];

    if (vector_t::bits_leaf > 0 &&
        vector_t::memory_policy::use_transient_rvalues)
        CHECK(addr_before == addr_after);
    else
        CHECK(addr_before != addr_after);

    CHECK_VECTOR_EQUALS(v, boost::irange(1u, 2u));
}

TEST_CASE("exception safety relaxed")
{
    using dadaist_vector_t =
        typename dadaist_wrapper<FLEX_VECTOR_T<unsigned>>::type;
    constexpr auto n = 667u;

    SECTION("push back")
    {
        auto half = n / 2;
        auto t    = as_transient_tester(
            make_test_flex_vector_front<dadaist_vector_t>(0, half));
        auto d = dadaism{};
        for (auto li = half, i = half; i < n;) {
            auto s = d.next();
            try {
                if (t.transient)
                    t.vt.push_back({i});
                else
                    t.vp = t.vp.push_back({i});
                ++i;
            } catch (dada_error) {}
            if (t.step())
                li = i;
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, li));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, li));
            }
        }
        CHECK(d.happenings > 0);
        CHECK(t.d.happenings > 0);
        IMMER_TRACE_E(d.happenings);
        IMMER_TRACE_E(t.d.happenings);
    }

    SECTION("update")
    {
        using boost::irange;
        using boost::join;

        auto t = as_transient_tester(
            make_test_flex_vector_front<dadaist_vector_t>(0, n));
        auto d = dadaism{};
        for (auto li = 0u, i = 0u; i < n;) {
            auto s = d.next();
            try {
                if (t.transient) {
                    t.vt.update(i, [](auto x) { return dada(), x + 1; });
                } else {
                    t.vp = t.vp.update(i, [](auto x) { return dada(), x + 1; });
                }
                ++i;
            } catch (dada_error) {}
            if (t.step())
                li = i;
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt,
                                    join(irange(1u, 1u + i), irange(i, n)));
                CHECK_VECTOR_EQUALS(t.vp,
                                    join(irange(1u, 1u + li), irange(li, n)));
            } else {
                CHECK_VECTOR_EQUALS(t.vp,
                                    join(irange(1u, 1u + i), irange(i, n)));
                CHECK_VECTOR_EQUALS(t.vt,
                                    join(irange(1u, 1u + li), irange(li, n)));
            }
        }
        CHECK(d.happenings > 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("take")
    {
        auto t = as_transient_tester(
            make_test_flex_vector_front<dadaist_vector_t>(0, n));
        auto d      = dadaism{};
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = n, li = i;;) {
            auto s = d.next();
            auto r = dadaist_vector_t{};
            try {
                if (t.transient)
                    t.vt.take(i);
                else
                    t.vp = t.vp.take(i);
                if (t.step())
                    li = i;
                delta = deltas.next();
                if (i < delta)
                    break;
                i -= delta;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, i + delta));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, li));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, i + delta));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, li));
            }
        }
        CHECK(d.happenings > 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("drop")
    {
        auto t =
            as_transient_tester(make_test_flex_vector<dadaist_vector_t>(0, n));
        auto d      = dadaism{};
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = delta, li = 0u; i < n;) {
            auto s = d.next();
            auto r = dadaist_vector_t{};
            try {
                if (t.transient)
                    t.vt.drop(delta);
                else
                    t.vp = t.vp.drop(delta);
                if (t.step()) {
                    li = i;
                }
                delta = deltas.next();
                i += delta;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(i - delta, n));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(li, n));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(i - delta, n));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(li, n));
            }
        }
        CHECK(d.happenings > 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("append")
    {
        auto make_ = [](auto i, auto delta) {
            auto d = dadaism::disable();
            return make_test_flex_vector<dadaist_vector_t>(i, i + delta);
        };
        auto t      = as_transient_tester(dadaist_vector_t{});
        auto d      = dadaism();
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = 0u, li = 0u; i < n;) {
            try {
                if (t.transient) {
                    auto data  = make_(i, delta);
                    auto datat = data.transient();
                    t.vt.append(datat);
                } else {
                    auto data = make_(i, delta);
                    t.vp      = t.vp + data;
                }
                i += delta;
                if (t.step()) {
                    li = i;
                }
                delta = deltas.next() * 3;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, li));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, li));
            }
        }
        CHECK(d.happenings == 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("append mut")
    {
        auto make_ = [](auto i, auto delta) {
            auto d = dadaism::disable();
            return make_test_flex_vector<dadaist_vector_t>(i, i + delta);
        };
        auto t      = as_transient_tester(dadaist_vector_t{});
        auto d      = dadaism();
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = 0u, li = 0u; i < n;) {
            try {
                if (t.transient) {
                    auto data  = make_(i, delta);
                    auto datat = data.transient();
                    t.vt.append(std::move(datat));
                } else {
                    auto data = make_(i, delta);
                    t.vp      = t.vp + data;
                }
                i += delta;
                if (t.step()) {
                    li = i;
                }
                delta = deltas.next() * 3;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, li));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(0u, i));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(0u, li));
            }
        }
        CHECK(d.happenings == 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("prepend")
    {
        auto make_ = [](auto i, auto delta) {
            auto d = dadaism::disable();
            return make_test_flex_vector<dadaist_vector_t>(i, i + delta);
        };
        auto t      = as_transient_tester(dadaist_vector_t{});
        auto d      = dadaism();
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = n, li = n; i > 0;) {
            delta = std::min(i, delta);
            try {
                if (t.transient) {
                    auto data  = make_(i - delta, delta);
                    auto datat = data.transient();
                    t.vt.prepend(datat);
                } else {
                    auto data = make_(i - delta, delta);
                    t.vp      = data + t.vp;
                }
                i -= delta;
                if (t.step()) {
                    li = i;
                }
                delta = deltas.next() * 3;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(i, n));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(li, n));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(i, n));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(li, n));
            }
        }
        CHECK(d.happenings == 0);
        CHECK(t.d.happenings > 0);
    }

    SECTION("prepend mut")
    {
        auto make_ = [](auto i, auto delta) {
            auto d = dadaism::disable();
            return make_test_flex_vector<dadaist_vector_t>(i, i + delta);
        };
        auto t      = as_transient_tester(dadaist_vector_t{});
        auto d      = dadaism();
        auto deltas = magic_rotator();
        auto delta  = deltas.next();
        for (auto i = n, li = n; i > 0;) {
            delta = std::min(i, delta);
            try {
                if (t.transient) {
                    auto data  = make_(i - delta, delta);
                    auto datat = data.transient();
                    t.vt.prepend(std::move(datat));
                } else {
                    auto data = make_(i - delta, delta);
                    t.vp      = data + t.vp;
                }
                i -= delta;
                if (t.step()) {
                    li = i;
                }
                delta = deltas.next() * 3;
            } catch (dada_error) {}
            if (t.transient) {
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(i, n));
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(li, n));
            } else {
                CHECK_VECTOR_EQUALS(t.vp, boost::irange(i, n));
                CHECK_VECTOR_EQUALS(t.vt, boost::irange(li, n));
            }
        }
        CHECK(d.happenings == 0);
        CHECK(t.d.happenings > 0);
    }
}
