#pragma once

#include <cassert>
#include <cstddef>
#include <optional>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>

namespace uc::detail {

// A fixed-capacity circular leaf.  Empty slots are represented explicitly so
// the block also works for non-default-constructible and non-trivial values.
// Insert/erase shifts the shorter side and therefore moves at most size()/2
// elements.  offset_ is the physical slot containing logical element zero.
template <class T>
class RingBlock {
public:
    explicit RingBlock(std::size_t capacity = 256)
        : slots_(capacity) {
        if (capacity < 2) {
            throw std::invalid_argument("RingBlock capacity must be at least two");
        }
    }

    RingBlock(const RingBlock&) = default;
    RingBlock(RingBlock&&) noexcept = default;
    RingBlock& operator=(const RingBlock&) = default;
    RingBlock& operator=(RingBlock&&) noexcept = default;
    ~RingBlock() = default;

    [[nodiscard]] std::size_t size() const noexcept { return size_; }
    [[nodiscard]] std::size_t capacity() const noexcept { return slots_.size(); }
    [[nodiscard]] bool empty() const noexcept { return size_ == 0; }
    [[nodiscard]] bool full() const noexcept { return size_ == capacity(); }
    [[nodiscard]] std::size_t offset() const noexcept { return offset_; }

    T& operator[](std::size_t index) noexcept {
        assert(index < size_);
        return *slots_[physical(index)];
    }

    const T& operator[](std::size_t index) const noexcept {
        assert(index < size_);
        return *slots_[physical(index)];
    }

    T& at(std::size_t index) {
        if (index >= size_) {
            throw std::out_of_range("RingBlock index out of range");
        }
        return (*this)[index];
    }

    const T& at(std::size_t index) const {
        if (index >= size_) {
            throw std::out_of_range("RingBlock index out of range");
        }
        return (*this)[index];
    }

    template <class U>
    void insert(std::size_t index, U&& value) {
        if (index > size_) {
            throw std::out_of_range("RingBlock insertion index out of range");
        }
        if (full()) {
            throw std::length_error("RingBlock is full");
        }

        const auto old_offset = offset_;
        if (index < size_ / 2) {
            const auto new_offset = decrement(old_offset);
            for (std::size_t i = 0; i < index; ++i) {
                move_slot((new_offset + i) % capacity(), (old_offset + i) % capacity());
            }
            offset_ = new_offset;
        } else {
            for (std::size_t i = size_; i > index; --i) {
                move_slot((old_offset + i) % capacity(),
                          (old_offset + i - 1) % capacity());
            }
        }

        slots_[physical(index)].emplace(std::forward<U>(value));
        ++size_;
        assert_invariant();
    }

    template <class... Args>
    T& emplace(std::size_t index, Args&&... args) {
        T value(std::forward<Args>(args)...);
        insert(index, std::move(value));
        return (*this)[index];
    }

    template <class U>
    void push_back(U&& value) {
        insert(size_, std::forward<U>(value));
    }

    T erase(std::size_t index) {
        if (index >= size_) {
            throw std::out_of_range("RingBlock erase index out of range");
        }

        T removed(std::move((*this)[index]));
        const auto old_offset = offset_;
        slots_[physical(index)].reset();

        if (index < size_ / 2) {
            for (std::size_t i = index; i > 0; --i) {
                move_slot((old_offset + i) % capacity(),
                          (old_offset + i - 1) % capacity());
            }
            offset_ = increment(old_offset);
        } else {
            for (std::size_t i = index; i + 1 < size_; ++i) {
                move_slot((old_offset + i) % capacity(),
                          (old_offset + i + 1) % capacity());
            }
        }

        --size_;
        if (size_ == 0) {
            offset_ = 0;
        }
        assert_invariant();
        return removed;
    }

    void clear() noexcept {
        for (auto& slot : slots_) {
            slot.reset();
        }
        size_ = 0;
        offset_ = 0;
    }

    [[nodiscard]] std::size_t allocated_bytes() const noexcept {
        return slots_.capacity() * sizeof(typename decltype(slots_)::value_type);
    }

private:
    [[nodiscard]] std::size_t physical(std::size_t logical) const noexcept {
        return (offset_ + logical) % capacity();
    }

    [[nodiscard]] std::size_t increment(std::size_t slot) const noexcept {
        return slot + 1 == capacity() ? 0 : slot + 1;
    }

    [[nodiscard]] std::size_t decrement(std::size_t slot) const noexcept {
        return slot == 0 ? capacity() - 1 : slot - 1;
    }

    void move_slot(std::size_t destination, std::size_t source) {
        assert(!slots_[destination].has_value());
        assert(slots_[source].has_value());
        slots_[destination].emplace(std::move(*slots_[source]));
        slots_[source].reset();
    }

    void assert_invariant() const noexcept {
#ifndef NDEBUG
        std::size_t occupied = 0;
        for (const auto& slot : slots_) {
            occupied += slot.has_value() ? 1U : 0U;
        }
        assert(occupied == size_);
        for (std::size_t i = 0; i < size_; ++i) {
            assert(slots_[physical(i)].has_value());
        }
#endif
    }

    std::vector<std::optional<T>> slots_;
    std::size_t offset_ = 0;
    std::size_t size_ = 0;
};

} // namespace uc::detail

