//
// 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
//

#pragma once

#include <immer/config.hpp>
#include <immer/detail/rbts/node.hpp>
#include <immer/detail/rbts/position.hpp>
#include <immer/detail/rbts/operations.hpp>

#include <immer/detail/type_traits.hpp>

#include <cassert>
#include <memory>
#include <numeric>

namespace immer {
namespace detail {
namespace rbts {

template <typename T,
          typename MemoryPolicy,
          bits_t   B,
          bits_t   BL>
struct rrbtree_iterator;

template <typename T,
          typename MemoryPolicy,
          bits_t   B,
          bits_t   BL>
struct rrbtree
{
    using node_t  = node<T, MemoryPolicy, B, BL>;
    using edit_t  = typename node_t::edit_t;
    using owner_t = typename MemoryPolicy::transience_t::owner;

    size_t  size;
    shift_t shift;
    node_t* root;
    node_t* tail;

    static const rrbtree& empty()
    {
        static const rrbtree empty_ {
            0,
            BL,
            node_t::make_inner_n(0u),
            node_t::make_leaf_n(0u)
        };
        return empty_;
    }

    template <typename U>
    static auto from_initializer_list(std::initializer_list<U> values)
    {
        auto e = owner_t{};
        auto result = rrbtree{empty()};
        for (auto&& v : values)
            result.push_back_mut(e, v);
        return result;
    }

    template <typename Iter, typename Sent,
              std::enable_if_t
              <compatible_sentinel_v<Iter, Sent>, bool> = true>
    static auto from_range(Iter first, Sent last)
    {
        auto e = owner_t{};
        auto result = rrbtree{empty()};
        for (; first != last; ++first)
            result.push_back_mut(e, *first);
        return result;
    }

    static auto from_fill(size_t n, T v)
    {
        auto e = owner_t{};
        auto result = rrbtree{empty()};
        while (n --> 0)
            result.push_back_mut(e, v);
        return result;
    }

    rrbtree(size_t sz, shift_t sh, node_t* r, node_t* t)
        : size{sz}, shift{sh}, root{r}, tail{t}
    {
        assert(check_tree());
    }

    rrbtree(const rrbtree& other)
        : rrbtree{other.size, other.shift, other.root, other.tail}
    {
        inc();
    }

    rrbtree(rrbtree&& other)
        : rrbtree{empty()}
    {
        swap(*this, other);
    }

    rrbtree& operator=(const rrbtree& other)
    {
        auto next{other};
        swap(*this, next);
        return *this;
    }

    rrbtree& operator=(rrbtree&& other)
    {
        swap(*this, other);
        return *this;
    }

    friend void swap(rrbtree& x, rrbtree& y)
    {
        using std::swap;
        swap(x.size,  y.size);
        swap(x.shift, y.shift);
        swap(x.root,  y.root);
        swap(x.tail,  y.tail);
    }

    ~rrbtree()
    {
        dec();
    }

    void inc() const
    {
        root->inc();
        tail->inc();
    }

    void dec() const
    {
        traverse(dec_visitor());
    }

    auto tail_size() const
    {
        return size - tail_offset();
    }

    auto tail_offset() const
    {
        auto r = root->relaxed();
        assert(r == nullptr || r->d.count);
        return
            r               ? r->d.sizes[r->d.count - 1] :
            size            ? (size - 1) & ~mask<BL>
            /* otherwise */ : 0;
    }

    template <typename Visitor, typename... Args>
    void traverse(Visitor v, Args&&... args) const
    {
        auto tail_off  = tail_offset();
        auto tail_size = size - tail_off;

        if (tail_off) visit_maybe_relaxed_sub(root, shift, tail_off, v, args...);
        else make_empty_regular_pos(root).visit(v, args...);

        if (tail_size) make_leaf_sub_pos(tail, tail_size).visit(v, args...);
        else make_empty_leaf_pos(tail).visit(v, args...);
    }

    template <typename Visitor, typename... Args>
    void traverse(Visitor v, size_t first, size_t last, Args&&... args) const
    {
        auto tail_off  = tail_offset();
        auto tail_size = size - tail_off;

        if (first < tail_off)
            visit_maybe_relaxed_sub(root, shift, tail_off, v,
                                    first,
                                    last < tail_off ? last : tail_off,
                                    args...);
        if (last > tail_off)
            make_leaf_sub_pos(tail, tail_size).visit(
                v,
                first > tail_off ? first - tail_off : 0,
                last - tail_off,
                args...);
    }

    template <typename Visitor, typename... Args>
    bool traverse_p(Visitor v, Args&&... args) const
    {
        auto tail_off  = tail_offset();
        auto tail_size = size - tail_off;
        return (tail_off
                ? visit_maybe_relaxed_sub(root, shift, tail_off, v, args...)
                : make_empty_regular_pos(root).visit(v, args...))
            && (tail_size
                ? make_leaf_sub_pos(tail, tail_size).visit(v, args...)
                : make_empty_leaf_pos(tail).visit(v, args...));
    }

    template <typename Visitor, typename... Args>
    bool traverse_p(Visitor v, size_t first, size_t last, Args&&... args) const
    {
        auto tail_off  = tail_offset();
        auto tail_size = size - tail_off;
        return
            (first < tail_off
             ? visit_maybe_relaxed_sub(root, shift, tail_off, v,
                                       first,
                                       last < tail_off ? last : tail_off,
                                       args...)
             : true)
         && (last > tail_off
             ? make_leaf_sub_pos(tail, tail_size).visit(
                 v,
                 first > tail_off ? first - tail_off : 0,
                 last - tail_off,
                 args...)
             : true);
    }

    template <typename Visitor>
    decltype(auto) descend(Visitor v, size_t idx) const
    {
        auto tail_off  = tail_offset();
        return idx >= tail_off
            ? make_leaf_descent_pos(tail).visit(v, idx - tail_off)
            : visit_maybe_relaxed_descent(root, shift, v, idx);
    }

    template <typename Fn>
    void for_each_chunk(Fn&& fn) const
    {
        traverse(for_each_chunk_visitor{}, std::forward<Fn>(fn));
    }

    template <typename Fn>
    void for_each_chunk(size_t first, size_t last, Fn&& fn) const
    {
        traverse(for_each_chunk_i_visitor{}, first, last, std::forward<Fn>(fn));
    }

    template <typename Fn>
    bool for_each_chunk_p(Fn&& fn) const
    {
        return traverse_p(for_each_chunk_p_visitor{}, std::forward<Fn>(fn));
    }

    template <typename Fn>
    bool for_each_chunk_p(size_t first, size_t last, Fn&& fn) const
    {
        return traverse_p(for_each_chunk_p_i_visitor{}, first, last, std::forward<Fn>(fn));
    }

    bool equals(const rrbtree& other) const
    {
        using iter_t = rrbtree_iterator<T, MemoryPolicy, B, BL>;
        if (size != other.size) return false;
        if (size == 0) return true;
        auto tail_off = tail_offset();
        auto tail_off_other = other.tail_offset();
        // compare trees
        if (tail_off > 0 && tail_off_other > 0) {
            // other.shift != shift is a theoretical possibility for
            // relaxed trees that sadly we haven't managed to exercise
            // in tests yet...
            if (other.shift >= shift) {
                if (!visit_maybe_relaxed_sub(
                        other.root, other.shift, tail_off_other,
                        equals_visitor::rrb{}, iter_t{other},
                        root, shift, tail_off))
                    return false;
            } else {
                if (!visit_maybe_relaxed_sub(
                        root, shift, tail_off,
                        equals_visitor::rrb{}, iter_t{*this},
                        other.root, other.shift, tail_off_other))
                    return false;
            }
        }
        return
            tail_off == tail_off_other ? make_leaf_sub_pos(
                tail, tail_size()).visit(
                    equals_visitor{}, other.tail) :
            tail_off > tail_off_other  ? std::equal(
                tail->leaf(), tail->leaf() + (size - tail_off),
                other.tail->leaf() + (tail_off - tail_off_other))
            /* otherwise */            : std::equal(
                tail->leaf(), tail->leaf() + (size - tail_off),
                iter_t{other} + tail_off);
    }

    std::tuple<shift_t, node_t*>
    push_tail(node_t* root, shift_t shift, size_t size,
              node_t* tail, count_t tail_size) const
    {
        if (auto r = root->relaxed()) {
            auto new_root = make_relaxed_pos(root, shift, r)
                .visit(push_tail_visitor<node_t>{}, tail, tail_size);
            if (new_root)
                return std::make_tuple( shift, new_root );
            else {
                auto new_root = node_t::make_inner_r_n(2);
                try {
                    auto new_path = node_t::make_path(shift, tail);
                    new_root->inner() [0] = root->inc();
                    new_root->inner() [1] = new_path;
                    new_root->relaxed()->d.sizes [0] = size;
                    new_root->relaxed()->d.sizes [1] = size + tail_size;
                    new_root->relaxed()->d.count = 2u;
                } catch (...) {
                    node_t::delete_inner_r(new_root, 2);
                    throw;
                }
                return std::make_tuple( shift + B, new_root );
            }
        } else if (size == size_t{branches<B>} << shift) {
            auto new_root = node_t::make_inner_n(2);
            try {
                auto new_path = node_t::make_path(shift, tail);
                new_root->inner() [0] = root->inc();
                new_root->inner() [1] = new_path;
            } catch (...) {
                node_t::delete_inner(new_root, 2);
                throw;
            }
            return std::make_tuple( shift + B, new_root );
        } else if (size) {
            auto new_root = make_regular_sub_pos(root, shift, size)
                .visit(push_tail_visitor<node_t>{}, tail);
            return std::make_tuple( shift, new_root );
        } else {
            return std::make_tuple( shift, node_t::make_path(shift, tail) );
        }
    }

    void push_tail_mut(edit_t e, size_t tail_off,
                       node_t* tail, count_t tail_size)
    {
        if (auto r = root->relaxed()) {
            auto new_root = make_relaxed_pos(root, shift, r)
                .visit(push_tail_mut_visitor<node_t>{}, e, tail, tail_size);
            if (new_root) {
                root = new_root;
            } else {
                auto new_root = node_t::make_inner_r_e(e);
                try {
                    auto new_path = node_t::make_path_e(e, shift, tail);
                    new_root->inner() [0] = root;
                    new_root->inner() [1] = new_path;
                    new_root->relaxed()->d.sizes [0] = tail_off;
                    new_root->relaxed()->d.sizes [1] = tail_off + tail_size;
                    new_root->relaxed()->d.count = 2u;
                    root = new_root;
                    shift += B;
                } catch (...) {
                    node_t::delete_inner_r_e(new_root);
                    throw;
                }
            }
        } else if (tail_off == size_t{branches<B>} << shift) {
            auto new_root = node_t::make_inner_e(e);
            try {
                auto new_path = node_t::make_path_e(e, shift, tail);
                new_root->inner() [0] = root;
                new_root->inner() [1] = new_path;
                root = new_root;
                shift += B;
            } catch (...) {
                node_t::delete_inner_e(new_root);
                throw;
            }
        } else if (tail_off) {
            auto new_root = make_regular_sub_pos(root, shift, tail_off)
                .visit(push_tail_mut_visitor<node_t>{}, e, tail);
            root = new_root;
        } else {
            auto new_root = node_t::make_path_e(e, shift, tail);
            dec_empty_regular(root);
            root = new_root;
        }
    }

    void ensure_mutable_tail(edit_t e, count_t n)
    {
        if (!tail->can_mutate(e)) {
            auto new_tail = node_t::copy_leaf_e(e, tail, n);
            dec_leaf(tail, n);
            tail = new_tail;
        }
    }

    void push_back_mut(edit_t e, T value)
    {
        auto ts = tail_size();
        if (ts < branches<BL>) {
            ensure_mutable_tail(e, ts);
            new (&tail->leaf()[ts]) T{std::move(value)};
        } else {
            using std::get;
            auto new_tail = node_t::make_leaf_e(e, std::move(value));
            auto tail_off = tail_offset();
            try {
                push_tail_mut(e, tail_off, tail, ts);
                tail = new_tail;
            } catch (...) {
                node_t::delete_leaf(new_tail, 1u);
                throw;
            }
        }
        ++size;
    }

    rrbtree push_back(T value) const
    {
        auto ts = tail_size();
        if (ts < branches<BL>) {
            auto new_tail = node_t::copy_leaf_emplace(tail, ts,
                                                      std::move(value));
            return { size + 1, shift, root->inc(), new_tail };
        } else {
            using std::get;
            auto new_tail = node_t::make_leaf_n(1u, std::move(value));
            auto tail_off = tail_offset();
            try {
                auto new_root = push_tail(root, shift, tail_off,
                                          tail, size - tail_off);
                tail->inc();
                return { size + 1, get<0>(new_root), get<1>(new_root), new_tail };
            } catch (...) {
                node_t::delete_leaf(new_tail, 1u);
                throw;
            }
        }
    }

    std::tuple<const T*, size_t, size_t>
    region_for(size_t idx) const
    {
        using std::get;
        auto tail_off = tail_offset();
        if (idx >= tail_off) {
            return std::make_tuple( tail->leaf(), tail_off, size );
        } else {
            auto subs = visit_maybe_relaxed_sub(
                root, shift, tail_off,
                region_for_visitor<T>(), idx);
            auto first = idx - get<1>(subs);
            auto end   = first + get<2>(subs);
            return std::make_tuple( get<0>(subs), first, end );
        }
    }

    T& get_mut(edit_t e, size_t idx)
    {
        auto tail_off = tail_offset();
        if (idx >= tail_off) {
            ensure_mutable_tail(e, size - tail_off);
            return tail->leaf() [(idx - tail_off) & mask<BL>];
        } else {
            return visit_maybe_relaxed_sub(
                root, shift, tail_off,
                get_mut_visitor<node_t>{}, idx, e, &root);
        }
    }

    const T& get(size_t index) const
    {
        return descend(get_visitor<T>(), index);
    }

    const T& get_check(size_t index) const
    {
        if (index >= size)
            throw std::out_of_range{"out of range"};
        return descend(get_visitor<T>(), index);
    }

    const T& front() const
    {
        return get(0);
    }

    const T& back() const
    {
        return get(size - 1);
    }

    template <typename FnT>
    void update_mut(edit_t e, size_t idx, FnT&& fn)
    {
        auto& elem = get_mut(e, idx);
        elem = std::forward<FnT>(fn) (std::move(elem));
    }

    template <typename FnT>
    rrbtree update(size_t idx, FnT&& fn) const
    {
        auto tail_off  = tail_offset();
        if (idx >= tail_off) {
            auto tail_size = size - tail_off;
            auto new_tail  = make_leaf_sub_pos(tail, tail_size)
                .visit(update_visitor<node_t>{}, idx - tail_off, fn);
            return { size, shift, root->inc(), new_tail };
        } else {
            auto new_root  = visit_maybe_relaxed_sub(
                root, shift, tail_off,
                update_visitor<node_t>{}, idx, fn);
            return { size, shift, new_root, tail->inc() };
        }
    }

    void assoc_mut(edit_t e, size_t idx, T value)
    {
        update_mut(e, idx, [&] (auto&&) {
                return std::move(value);
            });
    }

    rrbtree assoc(size_t idx, T value) const
    {
        return update(idx, [&] (auto&&) {
                return std::move(value);
            });
    }

    void take_mut(edit_t e, size_t new_size)
    {
        auto tail_off = tail_offset();
        if (new_size == 0) {
            *this = empty();
        } else if (new_size >= size) {
            return;
        } else if (new_size > tail_off) {
            auto ts    = size - tail_off;
            auto newts = new_size - tail_off;
            if (tail->can_mutate(e)) {
                destroy_n(tail->leaf() + newts, ts - newts);
            } else {
                auto new_tail = node_t::copy_leaf_e(e, tail, newts);
                dec_leaf(tail, ts);
                tail = new_tail;
            }
            size = new_size;
            return;
        } else {
            using std::get;
            auto l = new_size - 1;
            auto v = slice_right_mut_visitor<node_t>();
            auto r = visit_maybe_relaxed_sub(root, shift, tail_off, v, l, e);
            auto new_shift = get<0>(r);
            auto new_root  = get<1>(r);
            auto new_tail  = get<3>(r);
            if (new_root) {
                root  = new_root;
                shift = new_shift;
            } else {
                root  = empty().root->inc();
                shift = BL;
            }
            dec_leaf(tail, size - tail_off);
            size  = new_size;
            tail  = new_tail;
            return;
        }
    }

    rrbtree take(size_t new_size) const
    {
        auto tail_off = tail_offset();
        if (new_size == 0) {
            return empty();
        } else if (new_size >= size) {
            return *this;
        } else if (new_size > tail_off) {
            auto new_tail = node_t::copy_leaf(tail, new_size - tail_off);
            return { new_size, shift, root->inc(), new_tail };
        } else {
            using std::get;
            auto l = new_size - 1;
            auto v = slice_right_visitor<node_t>();
            auto r = visit_maybe_relaxed_sub(root, shift, tail_off, v, l);
            auto new_shift = get<0>(r);
            auto new_root  = get<1>(r);
            auto new_tail  = get<3>(r);
            if (new_root) {
                IMMER_ASSERT_TAGGED(new_root->compute_shift() == get<0>(r));
                assert(new_root->check(new_shift, new_size - get<2>(r)));
                return { new_size, new_shift, new_root, new_tail };
            } else {
                return { new_size, BL, empty().root->inc(), new_tail };
            }
        }
    }

    void drop_mut(edit_t e, size_t elems)
    {
        using std::get;
        auto tail_off = tail_offset();
        if (elems == 0) {
            return;
        } else if (elems >= size) {
            *this = empty();
        } else if (elems == tail_off) {
            dec_inner(root, shift, tail_off);
            shift = BL;
            root  = empty().root->inc();
            size -= elems;
            return;
        } else if (elems > tail_off) {
            auto v = slice_left_mut_visitor<node_t>();
            tail = get<1>(make_leaf_sub_pos(tail, size - tail_off).visit(
                              v, elems - tail_off, e));
            if (root != empty().root) {
                dec_inner(root, shift, tail_off);
                shift = BL;
                root  = empty().root->inc();
            }
            size -= elems;
            return;
        } else {
            auto v = slice_left_mut_visitor<node_t>();
            auto r = visit_maybe_relaxed_sub(root, shift, tail_off, v, elems, e);
            shift = get<0>(r);
            root  = get<1>(r);
            size -= elems;
            return;
        }
    }

    rrbtree drop(size_t elems) const
    {
        if (elems == 0) {
            return *this;
        } else if (elems >= size) {
            return empty();
        } else if (elems == tail_offset()) {
            return { size - elems, BL, empty().root->inc(), tail->inc() };
        } else if (elems > tail_offset()) {
            auto tail_off = tail_offset();
            auto new_tail = node_t::copy_leaf(tail, elems - tail_off,
                                              size - tail_off);
            return { size - elems, BL, empty().root->inc(), new_tail };
        } else {
            using std::get;
            auto v = slice_left_visitor<node_t>();
            auto r = visit_maybe_relaxed_sub(root, shift, tail_offset(), v, elems);
            auto new_root  = get<1>(r);
            auto new_shift = get<0>(r);
            return { size - elems, new_shift, new_root, tail->inc() };
        }
        return *this;
    }

    rrbtree concat(const rrbtree& r) const
    {
        assert(r.size < (std::numeric_limits<size_t>::max() - size));
        using std::get;
        if (size == 0)
            return r;
        else if (r.size == 0)
            return *this;
        else if (r.tail_offset() == 0) {
            // just concat the tail, similar to push_back
            auto tail_offst = tail_offset();
            auto tail_size  = size - tail_offst;
            if (tail_size == branches<BL>) {
                auto new_root = push_tail(root, shift, tail_offst,
                                          tail, tail_size);
                tail->inc();
                return { size + r.size, get<0>(new_root), get<1>(new_root),
                         r.tail->inc() };
            } else if (tail_size + r.size <= branches<BL>) {
                auto new_tail = node_t::copy_leaf(tail, tail_size,
                                                  r.tail, r.size);
                return { size + r.size, shift, root->inc(), new_tail };
            } else {
                auto remaining = branches<BL> - tail_size;
                auto add_tail  = node_t::copy_leaf(tail, tail_size,
                                                   r.tail, remaining);
                try {
                    auto new_tail = node_t::copy_leaf(r.tail, remaining, r.size);
                    try {
                        auto new_root  = push_tail(root, shift, tail_offst,
                                                   add_tail, branches<BL>);
                        return { size + r.size,
                                 get<0>(new_root), get<1>(new_root),
                                 new_tail };
                    } catch (...) {
                        node_t::delete_leaf(new_tail, r.size - remaining);
                        throw;
                    }
                } catch (...) {
                    node_t::delete_leaf(add_tail, branches<BL>);
                    throw;
                }
            }
        } else if (tail_offset() == 0) {
            auto tail_offst = tail_offset();
            auto tail_size  = size - tail_offst;
            auto concated   = concat_trees(tail, tail_size,
                                           r.root, r.shift, r.tail_offset());
            auto new_shift  = concated.shift();
            auto new_root   = concated.node();
            IMMER_ASSERT_TAGGED(new_shift == new_root->compute_shift());
            assert(new_root->check(new_shift, size + r.tail_offset()));
            return { size + r.size, new_shift, new_root, r.tail->inc() };
        } else {
            auto tail_offst = tail_offset();
            auto tail_size  = size - tail_offst;
            auto concated   = concat_trees(root, shift, tail_offst,
                                           tail, tail_size,
                                           r.root, r.shift, r.tail_offset());
            auto new_shift  = concated.shift();
            auto new_root   = concated.node();
            IMMER_ASSERT_TAGGED(new_shift == new_root->compute_shift());
            assert(new_root->check(new_shift, size + r.tail_offset()));
            return { size + r.size, new_shift, new_root, r.tail->inc() };
        }
    }

    constexpr static bool supports_transient_concat =
        !std::is_empty<edit_t>::value;

    friend void concat_mut_l(rrbtree& l, edit_t el, const rrbtree& r)
    {
        assert(&l != &r);
        assert(r.size < (std::numeric_limits<size_t>::max() - l.size));
        using std::get;
        if (l.size == 0)
            l = r;
        else if (r.size == 0)
            return;
        else if (r.tail_offset() == 0) {
            // just concat the tail, similar to push_back
            auto tail_offst = l.tail_offset();
            auto tail_size  = l.size - tail_offst;
            if (tail_size == branches<BL>) {
                l.push_tail_mut(el, tail_offst, l.tail, tail_size);
                l.tail = r.tail->inc();
                l.size += r.size;
                return;
            } else if (tail_size + r.size <= branches<BL>) {
                l.ensure_mutable_tail(el, tail_size);
                std::uninitialized_copy(r.tail->leaf(),
                                        r.tail->leaf() + r.size,
                                        l.tail->leaf() + tail_size);
                l.size += r.size;
                return;
            } else {
                auto remaining = branches<BL> - tail_size;
                l.ensure_mutable_tail(el, tail_size);
                std::uninitialized_copy(r.tail->leaf(),
                                        r.tail->leaf() + remaining,
                                        l.tail->leaf() + tail_size);
                try {
                    auto new_tail = node_t::copy_leaf_e(el, r.tail, remaining, r.size);
                    try {
                        l.push_tail_mut(el, tail_offst, l.tail, branches<BL>);
                        l.tail = new_tail;
                        l.size += r.size;
                        return;
                    } catch (...) {
                        node_t::delete_leaf(new_tail, r.size - remaining);
                        throw;
                    }
                } catch (...) {
                    destroy_n(r.tail->leaf() + tail_size, remaining);
                    throw;
                }
            }
        } else if (l.tail_offset() == 0) {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    el,
                    el, l.tail, tail_size,
                    MemoryPolicy::transience_t::noone,
                    r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                l.size += r.size;
                l.shift = concated.shift();
                l.root  = concated.node();
                l.tail  = r.tail;
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                l = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
                return;
            }
        } else {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    el,
                    el, l.root, l.shift, tail_offst, l.tail, tail_size,
                    MemoryPolicy::transience_t::noone,
                    r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                l.size += r.size;
                l.shift = concated.shift();
                l.root  = concated.node();
                l.tail  = r.tail;
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.root, l.shift, tail_offst,
                                               l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                l = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
            }
        }
    }

    friend void concat_mut_r(const rrbtree& l, rrbtree& r, edit_t er)
    {
        assert(&l != &r);
        assert(r.size < (std::numeric_limits<size_t>::max() - l.size));
        using std::get;
        if (r.size == 0)
            r = std::move(l);
        else if (l.size == 0)
            return;
        else if (r.tail_offset() == 0) {
            // just concat the tail, similar to push_back
            auto tail_offst = l.tail_offset();
            auto tail_size  = l.size - tail_offst;
            if (tail_size == branches<BL>) {
                // this could be improved by making sure that the
                // newly created nodes as part of the `push_tail()`
                // are tagged with `er`
                auto res = l.push_tail(l.root, l.shift, tail_offst,
                                       l.tail, tail_size);
                l.tail->inc(); // note: leak if mutably concatenated
                               // with itself, but this is forbidden
                               // by the interface
                r = { l.size + r.size, get<0>(res), get<1>(res),
                      r.tail->inc() };
                return;
            } else if (tail_size + r.size <= branches<BL>) {
                // doing this in a exception way mutating way is very
                // tricky while potential performance gains are
                // minimal (we need to move every element of the right
                // tail anyways to make space for the left tail)
                //
                // we could however improve this by at least moving the
                // elements of the right tail...
                auto new_tail = node_t::copy_leaf(l.tail, tail_size,
                                                  r.tail, r.size);
                r = { l.size + r.size, l.shift, l.root->inc(), new_tail };
                return;
            } else {
                // like the immutable version
                auto remaining = branches<BL> - tail_size;
                auto add_tail  = node_t::copy_leaf_e(er,
                                                     l.tail, tail_size,
                                                     r.tail, remaining);
                try {
                    auto new_tail = node_t::copy_leaf_e(er, r.tail, remaining, r.size);
                    try {
                        // this could be improved by making sure that the
                        // newly created nodes as part of the `push_tail()`
                        // are tagged with `er`
                        auto new_root = l.push_tail(l.root, l.shift, tail_offst,
                                                    add_tail, branches<BL>);
                        r = { l.size + r.size,
                              get<0>(new_root), get<1>(new_root),
                              new_tail };
                        return;
                    } catch (...) {
                        node_t::delete_leaf(new_tail, r.size - remaining);
                        throw;
                    }
                } catch (...) {
                    node_t::delete_leaf(add_tail, branches<BL>);
                    throw;
                }
                return;
            }
        } else if (l.tail_offset() == 0) {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    er,
                    MemoryPolicy::transience_t::noone, l.tail, tail_size,
                    er,r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                r.size += l.size;
                r.shift = concated.shift();
                r.root  = concated.node();
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                r = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
                return;
            }
        } else {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    er,
                    MemoryPolicy::transience_t::noone,
                    l.root, l.shift, tail_offst, l.tail, tail_size,
                    er, r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                r.size += l.size;
                r.shift = concated.shift();
                r.root  = concated.node();
                return;
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.root, l.shift, tail_offst,
                                               l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                r = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
                return;
            }
        }
    }

    friend void concat_mut_lr_l(rrbtree& l, edit_t el, rrbtree& r, edit_t er)
    {
        assert(&l != &r);
        assert(r.size < (std::numeric_limits<size_t>::max() - l.size));
        using std::get;
        if (l.size == 0)
            l = r;
        else if (r.size == 0)
            return;
        else if (r.tail_offset() == 0) {
            // just concat the tail, similar to push_back
            auto tail_offst = l.tail_offset();
            auto tail_size  = l.size - tail_offst;
            if (tail_size == branches<BL>) {
                l.push_tail_mut(el, tail_offst, l.tail, tail_size);
                l.tail = r.tail->inc();
                l.size += r.size;
                return;
            } else if (tail_size + r.size <= branches<BL>) {
                l.ensure_mutable_tail(el, tail_size);
                if (r.tail->can_mutate(er))
                    detail::uninitialized_move(r.tail->leaf(),
                                              r.tail->leaf() + r.size,
                                              l.tail->leaf() + tail_size);
                else
                    std::uninitialized_copy(r.tail->leaf(),
                                            r.tail->leaf() + r.size,
                                            l.tail->leaf() + tail_size);
                l.size += r.size;
                return;
            } else {
                auto remaining = branches<BL> - tail_size;
                l.ensure_mutable_tail(el, tail_size);
                if (r.tail->can_mutate(er))
                    detail::uninitialized_move(r.tail->leaf(),
                                              r.tail->leaf() + remaining,
                                              l.tail->leaf() + tail_size);
                else
                    std::uninitialized_copy(r.tail->leaf(),
                                            r.tail->leaf() + remaining,
                                            l.tail->leaf() + tail_size);
                try {
                    auto new_tail = node_t::copy_leaf_e(el, r.tail, remaining, r.size);
                    try {
                        l.push_tail_mut(el, tail_offst, l.tail, branches<BL>);
                        l.tail = new_tail;
                        l.size += r.size;
                        return;
                    } catch (...) {
                        node_t::delete_leaf(new_tail, r.size - remaining);
                        throw;
                    }
                } catch (...) {
                    destroy_n(r.tail->leaf() + tail_size, remaining);
                    throw;
                }
            }
        } else if (l.tail_offset() == 0) {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    el,
                    el, l.tail, tail_size,
                    er, r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                l.size += r.size;
                l.shift = concated.shift();
                l.root  = concated.node();
                l.tail  = r.tail;
                r.hard_reset();
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                l = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
                return;
            }
        } else {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    el,
                    el, l.root, l.shift, tail_offst, l.tail, tail_size,
                    er, r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                l.size += r.size;
                l.shift = concated.shift();
                l.root  = concated.node();
                l.tail  = r.tail;
                r.hard_reset();
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.root, l.shift, tail_offst,
                                               l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                l = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
            }
        }
    }

    friend void concat_mut_lr_r(rrbtree& l, edit_t el, rrbtree& r, edit_t er)
    {
        assert(&l != &r);
        assert(r.size < (std::numeric_limits<size_t>::max() - l.size));
        using std::get;
        if (r.size == 0)
            r = l;
        else if (l.size == 0)
            return;
        else if (r.tail_offset() == 0) {
            // just concat the tail, similar to push_back
            auto tail_offst = l.tail_offset();
            auto tail_size  = l.size - tail_offst;
            if (tail_size == branches<BL>) {
                // this could be improved by making sure that the
                // newly created nodes as part of the `push_tail()`
                // are tagged with `er`
                auto res = l.push_tail(l.root, l.shift, tail_offst,
                                       l.tail, tail_size);
                r = { l.size + r.size, get<0>(res), get<1>(res),
                      r.tail->inc() };
                return;
            } else if (tail_size + r.size <= branches<BL>) {
                // doing this in a exception way mutating way is very
                // tricky while potential performance gains are
                // minimal (we need to move every element of the right
                // tail anyways to make space for the left tail)
                //
                // we could however improve this by at least moving the
                // elements of the mutable tails...
                auto new_tail = node_t::copy_leaf(l.tail, tail_size,
                                                  r.tail, r.size);
                r = { l.size + r.size, l.shift, l.root->inc(), new_tail };
                return;
            } else {
                // like the immutable version.
                // we could improve this also by moving elements
                // instead of just copying them
                auto remaining = branches<BL> - tail_size;
                auto add_tail  = node_t::copy_leaf_e(er,
                                                     l.tail, tail_size,
                                                     r.tail, remaining);
                try {
                    auto new_tail = node_t::copy_leaf_e(er, r.tail, remaining, r.size);
                    try {
                        // this could be improved by making sure that the
                        // newly created nodes as part of the `push_tail()`
                        // are tagged with `er`
                        auto new_root = l.push_tail(l.root, l.shift, tail_offst,
                                                    add_tail, branches<BL>);
                        r = { l.size + r.size,
                              get<0>(new_root), get<1>(new_root),
                              new_tail };
                        return;
                    } catch (...) {
                        node_t::delete_leaf(new_tail, r.size - remaining);
                        throw;
                    }
                } catch (...) {
                    node_t::delete_leaf(add_tail, branches<BL>);
                    throw;
                }
                return;
            }
        } else if (l.tail_offset() == 0) {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    er,
                    el, l.tail, tail_size,
                    er,r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                r.size += l.size;
                r.shift = concated.shift();
                r.root  = concated.node();
                l.hard_reset();
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                r = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
                return;
            }
        } else {
            if (supports_transient_concat) {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees_mut(
                    er,
                    el, l.root, l.shift, tail_offst, l.tail, tail_size,
                    er, r.root, r.shift, r.tail_offset());
                IMMER_ASSERT_TAGGED(concated.shift() == concated.node()->compute_shift());
                assert(concated.node()->check(concated.shift(), l.size + r.tail_offset()));
                r.size += l.size;
                r.shift = concated.shift();
                r.root  = concated.node();
                l.hard_reset();
            } else {
                auto tail_offst = l.tail_offset();
                auto tail_size  = l.size - tail_offst;
                auto concated   = concat_trees(l.root, l.shift, tail_offst,
                                               l.tail, tail_size,
                                               r.root, r.shift, r.tail_offset());
                r = { l.size + r.size, concated.shift(),
                      concated.node(), r.tail->inc() };
            }
        }
    }

    void hard_reset()
    {
        assert(supports_transient_concat);
        auto&& empty_ = empty();
        size = empty_.size;
        shift = empty_.shift;
        root = empty_.root;
        tail = empty_.tail;
    }

    bool check_tree() const
    {
        assert(shift <= sizeof(size_t) * 8 - BL);
        assert(shift >= BL);
        assert(tail_offset() <= size);
        assert(tail_size() <= branches<BL>);
#if IMMER_DEBUG_DEEP_CHECK
        assert(check_root());
        assert(check_tail());
#endif
        return true;
    }

    bool check_tail() const
    {
#if IMMER_DEBUG_DEEP_CHECK
        if (tail_size() > 0)
            assert(tail->check(endshift<B, BL>, tail_size()));
#endif
        return true;
    }

    bool check_root() const
    {
#if IMMER_DEBUG_DEEP_CHECK
        if (tail_offset() > 0)
            assert(root->check(shift, tail_offset()));
        else {
            IMMER_ASSERT_TAGGED(root->kind() == node_t::kind_t::inner);
            assert(shift == BL);
        }
#endif
        return true;
    }

#if IMMER_DEBUG_PRINT
    void debug_print(std::ostream& out) const
    {
        out
            << "--" << std::endl
            << "{" << std::endl
            << "  size  = " << size << std::endl
            << "  shift = " << shift << std::endl
            << "  root  = " << std::endl;
        debug_print_node(out, root, shift, tail_offset());
        out << "  tail  = " << std::endl;
        debug_print_node(out, tail, endshift<B, BL>, tail_size());
        out << "}" << std::endl;
    }

    void debug_print_indent(std::ostream& out, unsigned indent) const
    {
        while (indent --> 0)
            out << ' ';
    }

    void debug_print_node(std::ostream& out,
                          node_t* node,
                          shift_t shift,
                          size_t size,
                          unsigned indent = 8) const
    {
        const auto indent_step = 4;

        if (shift == endshift<B, BL>) {
            debug_print_indent(out, indent);
            out << "- {" << size << "} "
                << pretty_print_array(node->leaf(), size)
                << std::endl;
        } else if (auto r = node->relaxed()) {
            auto count = r->d.count;
            debug_print_indent(out, indent);
            out << "# {" << size << "} "
                << pretty_print_array(r->d.sizes, r->d.count)
                << std::endl;
            auto last_size = size_t{};
            for (auto i = count_t{}; i < count; ++i) {
                debug_print_node(out,
                                 node->inner()[i],
                                 shift - B,
                                 r->d.sizes[i] - last_size,
                                 indent + indent_step);
                last_size = r->d.sizes[i];
            }
        } else {
            debug_print_indent(out, indent);
            out << "+ {" << size << "}" << std::endl;
            auto count = (size >> shift)
                + (size - ((size >> shift) << shift) > 0);
            if (count) {
                for (auto i = count_t{}; i < count - 1; ++i)
                    debug_print_node(out,
                                     node->inner()[i],
                                     shift - B,
                                     1 << shift,
                                     indent + indent_step);
                debug_print_node(out,
                                 node->inner()[count - 1],
                                 shift - B,
                                 size - ((count - 1) << shift),
                                 indent + indent_step);
            }
        }
    }
#endif // IMMER_DEBUG_PRINT
};

} // namespace rbts
} // namespace detail
} // namespace immer
