#pragma once

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>

namespace uc::detail {

// A compact directory over leaf sizes.  Level zero describes leaves, every
// next level groups `fanout` children, and the requested maximum number of
// levels is respected.  The top level uses binary search; descent only scans a
// single fanout-sized group.  A Fenwick side index supplies O(log L) prefix
// sums needed by stable-ID -> logical-index resolution.
class HierarchicalSizeIndex {
public:
    struct Location {
        std::size_t leaf = 0;
        std::size_t local = 0;
    };

    HierarchicalSizeIndex(std::size_t fanout = 16, std::size_t max_levels = 3)
        : fanout_(std::max<std::size_t>(2, fanout)),
          max_levels_(std::clamp<std::size_t>(max_levels, 1, 8)) {}

    void configure(std::size_t fanout, std::size_t max_levels) {
        const auto leaf_sizes = levels_.empty()
            ? std::vector<std::size_t>{} : levels_.front();
        HierarchicalSizeIndex replacement(fanout, max_levels);
        replacement.rebuild(leaf_sizes);
        swap(replacement);
    }

    void rebuild(const std::vector<std::size_t>& leaf_sizes) {
        // Build all allocation-owning state off to the side.  A failed
        // allocation must not leave a directory that no longer describes its
        // storage.
        std::vector<std::vector<std::size_t>> new_levels;
        std::vector<std::size_t> new_top_prefix;
        std::vector<std::size_t> new_fenwick(leaf_sizes.size() + 1, 0);
        std::size_t new_total = 0;
        if (leaf_sizes.empty()) {
            levels_.swap(new_levels);
            top_prefix_.swap(new_top_prefix);
            fenwick_.swap(new_fenwick);
            total_ = 0;
            return;
        }

        new_levels.push_back(leaf_sizes);
        // Keep the top directory at most fanout-sized.  Binary search handles
        // that top level, so building extra single-parent levels only adds a
        // lookup and cannot improve asymptotic complexity.
        while (new_levels.back().size() > fanout_
               && new_levels.size() < max_levels_) {
            const auto& children = new_levels.back();
            std::vector<std::size_t> parents;
            parents.reserve((children.size() + fanout_ - 1) / fanout_);
            for (std::size_t begin = 0; begin < children.size(); begin += fanout_) {
                const auto end = std::min(children.size(), begin + fanout_);
                std::size_t weight = 0;
                for (auto i = begin; i < end; ++i) {
                    weight += children[i];
                }
                parents.push_back(weight);
            }
            new_levels.push_back(std::move(parents));
        }

        for (const auto weight : new_levels.back()) {
            new_total += weight;
            new_top_prefix.push_back(new_total);
        }
        for (std::size_t i = 0; i < leaf_sizes.size(); ++i) {
            for (auto node = i + 1; node < new_fenwick.size();
                 node += node & (~node + 1)) {
                new_fenwick[node] += leaf_sizes[i];
            }
        }

        levels_.swap(new_levels);
        top_prefix_.swap(new_top_prefix);
        fenwick_.swap(new_fenwick);
        total_ = new_total;
    }

    void swap(HierarchicalSizeIndex& other) noexcept {
        using std::swap;
        swap(fanout_, other.fanout_);
        swap(max_levels_, other.max_levels_);
        swap(total_, other.total_);
        levels_.swap(other.levels_);
        top_prefix_.swap(other.top_prefix_);
        fenwick_.swap(other.fenwick_);
    }

    void update(std::size_t leaf, std::ptrdiff_t delta) {
        if (levels_.empty() || leaf >= levels_.front().size()) {
            throw std::out_of_range("directory leaf index out of range");
        }
        apply_delta(levels_[0][leaf], delta);
        std::size_t node = leaf;
        for (std::size_t level = 1; level < levels_.size(); ++level) {
            node /= fanout_;
            apply_delta(levels_[level][node], delta);
        }
        total_ = apply_delta_copy(total_, delta);
        fenwick_add_signed(leaf, delta);
        rebuild_top_prefix();
    }

    [[nodiscard]] Location locate(std::size_t logical_index) const {
        if (logical_index >= total_ || levels_.empty()) {
            throw std::out_of_range("logical index out of range");
        }

        const auto top_it = std::upper_bound(top_prefix_.begin(), top_prefix_.end(), logical_index);
        std::size_t node = static_cast<std::size_t>(top_it - top_prefix_.begin());
        std::size_t remaining = logical_index - (node == 0 ? 0 : top_prefix_[node - 1]);

        for (std::size_t level = levels_.size() - 1; level > 0; --level) {
            const auto& children = levels_[level - 1];
            const auto begin = node * fanout_;
            const auto end = std::min(children.size(), begin + fanout_);
            auto child = begin;
            for (; child < end; ++child) {
                if (remaining < children[child]) {
                    break;
                }
                remaining -= children[child];
            }
            assert(child < end);
            node = child;
        }
        return {node, remaining};
    }

    [[nodiscard]] std::size_t prefix_before(std::size_t leaf) const noexcept {
        std::size_t sum = 0;
        for (std::size_t i = leaf; i > 0; i -= i & (~i + 1)) {
            sum += fenwick_[i];
        }
        return sum;
    }

    [[nodiscard]] std::size_t total() const noexcept { return total_; }
    [[nodiscard]] std::size_t actual_levels() const noexcept { return levels_.size(); }
    [[nodiscard]] std::size_t fanout() const noexcept { return fanout_; }

    [[nodiscard]] std::size_t allocated_bytes() const noexcept {
        std::size_t bytes = fenwick_.capacity() * sizeof(std::size_t)
                          + top_prefix_.capacity() * sizeof(std::size_t);
        for (const auto& level : levels_) {
            bytes += level.capacity() * sizeof(std::size_t);
        }
        return bytes;
    }

private:
    static void apply_delta(std::size_t& value, std::ptrdiff_t delta) {
        value = apply_delta_copy(value, delta);
    }

    static std::size_t apply_delta_copy(std::size_t value, std::ptrdiff_t delta) {
        if (delta < 0) {
            const auto magnitude = static_cast<std::size_t>(-delta);
            assert(value >= magnitude);
            return value - magnitude;
        }
        return value + static_cast<std::size_t>(delta);
    }

    void rebuild_top_prefix() {
        top_prefix_.clear();
        top_prefix_.reserve(levels_.back().size());
        std::size_t prefix = 0;
        for (const auto weight : levels_.back()) {
            prefix += weight;
            top_prefix_.push_back(prefix);
        }
    }

    void fenwick_add(std::size_t leaf, std::size_t delta) noexcept {
        for (auto i = leaf + 1; i < fenwick_.size(); i += i & (~i + 1)) {
            fenwick_[i] += delta;
        }
    }

    void fenwick_add_signed(std::size_t leaf, std::ptrdiff_t delta) noexcept {
        for (auto i = leaf + 1; i < fenwick_.size(); i += i & (~i + 1)) {
            fenwick_[i] = apply_delta_copy(fenwick_[i], delta);
        }
    }

    std::size_t fanout_;
    std::size_t max_levels_;
    std::size_t total_ = 0;
    std::vector<std::vector<std::size_t>> levels_;
    std::vector<std::size_t> top_prefix_;
    std::vector<std::size_t> fenwick_;
};

} // namespace uc::detail
