#ifndef F20CF38F_7327_4608_8307_6AE058041CD5
#define F20CF38F_7327_4608_8307_6AE058041CD5

#include <algorithm>
#include <cstring>
#include <list>
#include <mesytec-mnode/mnode_cpp_types.h>
#include <mesytec-mnode/mnode_math.h>
#include <numeric>
#include <spdlog/fmt/fmt.h>
#include <vector>

namespace mesytec::mnode::mana
{

template <typename T> inline bool is_aligned(const T *ptr, size_t alignment = alignof(T))
{
    return (((uintptr_t)ptr) % alignment) == 0;
}

class Arena
{
  public:
    static constexpr size_t initial_segment_size = 1u << 20;
    static constexpr size_t default_align_to = 8;
    static constexpr size_t default_max_segments = 1;

    Arena() = default;
    ~Arena() = default;

    Arena(Arena &&other) = default;
    Arena &operator=(Arena &&other) = default;

    // Pushes at least the required amount of memory onto the arena. The memory is aligned to
    // 'default_align_to' and zeroed
    u8 *push_size(size_t required)
    {
        if (!segment_)
        {
            if (segment_count() < max_segments())
            {
                // could waste complete segments if required > default size of 1u << 20 but that's
                // fine for now
                segment_ = &segments_.emplace_back(
                    Arena::Segment(std::max(required, initial_segment_size)));
            }
            else
            {
                throw std::bad_alloc();
            }
        }

        assert(segment_ && segment_->free() >= required);

        void *ptr = segment_->cur();
        size_t space = segment_->free();
        if (!std::align(default_align_to, required, ptr, space))
            throw std::bad_alloc();

        assert(is_aligned(ptr, default_align_to));

        size_t pad_waste = segment_->free() - space;
        segment_->used += required + pad_waste;
        pad_waste_ += pad_waste;
        ++allocations_;

        std::memset(ptr, 0, required);
        return reinterpret_cast<u8 *>(ptr);
    }

    template <typename T> T *push_t(size_t count = 1)
    {
        static_assert(std::is_trivial<T>::value, "T must be a trivial type");
        return reinterpret_cast<T *>(push_size(sizeof(T) * count));
    }

    char *push_cstr(const std::string &str)
    {
        auto mem = push_size(str.size() + 1);
        std::memcpy(mem, str.c_str(), str.size());
        return reinterpret_cast<char *>(mem);
    }

    void reset()
    {
        std::for_each(std::begin(segments_), std::end(segments_),
                      [](Segment &seg) { seg.reset(); });
        segment_ = segments_.empty() ? nullptr : &segments_.front();
        allocations_ = 0;
        pad_waste_ = 0;
    }

    size_t max_segments() const { return max_segments_; }
    size_t allocations() const { return allocations_; }
    size_t pad_waste() const { return pad_waste_; }
    size_t segment_count() const { return segments_.size(); }
    size_t capacity() const
    {
        auto accu = [](size_t sum, const Segment &seg) { return sum + seg.data.size(); };
        return std::accumulate(std::begin(segments_), std::end(segments_), static_cast<size_t>(0u),
                               accu);
    }

    size_t used() const
    {
        auto accu = [](size_t sum, const Segment &seg) { return sum + seg.used; };
        return std::accumulate(std::begin(segments_), std::end(segments_), static_cast<size_t>(0u),
                               accu) -
               pad_waste_;
    }

    const u8 *cur_begin() const { return segment_ ? segment_->data.data() : nullptr; }
    const u8 *cur_end() const { return segment_ ? segment_->cur() : nullptr; }

  private:
    Arena(const Arena &) = delete;
    Arena &operator=(const Arena &) = delete;

    struct Segment
    {
        std::vector<u8> data;
        size_t used;

        explicit Segment(size_t size)
            : data(size)
            , used(0)
        {
        }
        size_t free() const { return data.size() - used; }
        u8 *cur() { return data.data() + used; }
        void reset() { used = 0; }
    };

    size_t max_segments_ = default_max_segments;
    std::list<Segment> segments_;
    Segment *segment_ = nullptr;
    size_t allocations_ = 0;
    size_t pad_waste_ = 0;
};

inline std::string arena_stats(const Arena &arena)
{
    return fmt::format("allocations={}, capacity={}, pad_waste={}, used_size={}",
                       arena.allocations(), arena.capacity(), arena.pad_waste(), arena.used());
}

} // namespace mesytec::mnode::mana

#endif /* F20CF38F_7327_4608_8307_6AE058041CD5 */
