#pragma once

#include <util/generic/yexception.h>

#include <algorithm>
#include <initializer_list>
#include <iterator>

/**
 * `TArrayRef` works pretty much like `std::span` with dynamic extent, presenting
 * an array-like interface into a contiguous sequence of objects.
 *
 * It can be used at interface boundaries instead of `TVector` or
 * pointer-size pairs, and is actually a preferred way to pass contiguous data
 * into functions.
 *
 * Note that `TArrayRef` can be auto-constructed from any contiguous container
 * (with `size` and `data` members), and thus you don't have to change client code
 * when switching over from passing `TVector` to `TArrayRef`.
 *
 * Note that `TArrayRef` has the same const-semantics as raw pointers:
 * - `TArrayRef<T>` is a non-const reference to non-const data (like `T*`);
 * - `TArrayRef<const T>` is a non-const reference to const data (like `const T*`);
 * - `const TArrayRef<T>` is a const reference to non-const data (like `T* const`);
 * - `const TArrayRef<const T>` is a const reference to const data (like `const T* const`).
 */
template <class T>
class TArrayRef {
public:
    using iterator = T*;
    using const_iterator = const T*;
    using reference = T&;
    using const_reference = const T&;
    using value_type = T;
    using reverse_iterator = std::reverse_iterator<iterator>;
    using const_reverse_iterator = std::reverse_iterator<const_iterator>;

    constexpr inline TArrayRef() noexcept
        : T_(nullptr)
        , S_(0)
    {
    }

    constexpr inline TArrayRef(T* data, size_t len) noexcept
        : T_(data)
        , S_(len)
    {
    }

    constexpr inline TArrayRef(T* begin, T* end) noexcept
        : T_(begin)
        , S_(end - begin)
    {
    }

    constexpr inline TArrayRef(std::initializer_list<T> list) noexcept
        : T_(list.begin())
        , S_(list.size())
    {
    }

    template <class Container>
    constexpr inline TArrayRef(Container&& container, decltype(std::declval<T*&>() = container.data(), nullptr) = nullptr) noexcept
        : T_(container.data())
        , S_(container.size())
    {
        static_assert(
            sizeof(decltype(*container.data())) == sizeof(T),
            "Attempt to create TArrayRef from a container of elements with a different size");
    }

    template <size_t N>
    constexpr inline TArrayRef(T (&array)[N]) noexcept
        : T_(array)
        , S_(N)
    {
    }

    template <class TT, typename = std::enable_if_t<std::is_same<std::remove_const_t<T>, std::remove_const_t<TT>>::value>>
    bool operator==(const TArrayRef<TT>& other) const noexcept {
        return (S_ == other.size()) && std::equal(begin(), end(), other.begin());
    }

    constexpr inline T* data() const noexcept {
        return T_;
    }

    constexpr inline size_t size() const noexcept {
        return S_;
    }

    constexpr size_t size_bytes() const noexcept {
        return (size() * sizeof(T));
    }

    constexpr inline bool empty() const noexcept {
        return (S_ == 0);
    }

    constexpr inline iterator begin() const noexcept {
        return T_;
    }

    constexpr inline iterator end() const noexcept {
        return (T_ + S_);
    }

    constexpr inline const_iterator cbegin() const noexcept {
        return T_;
    }

    constexpr inline const_iterator cend() const noexcept {
        return (T_ + S_);
    }

    constexpr inline reverse_iterator rbegin() const noexcept {
        return reverse_iterator(T_ + S_);
    }

    constexpr inline reverse_iterator rend() const noexcept {
        return reverse_iterator(T_);
    }

    constexpr inline const_reverse_iterator crbegin() const noexcept {
        return const_reverse_iterator(T_ + S_);
    }

    constexpr inline const_reverse_iterator crend() const noexcept {
        return const_reverse_iterator(T_);
    }

    constexpr inline reference front() const noexcept {
        return *T_;
    }

    inline reference back() const noexcept {
        Y_ASSERT(S_ > 0);

        return *(end() - 1);
    }

    inline reference operator[](size_t n) const noexcept {
        Y_ASSERT(n < S_);

        return *(T_ + n);
    }

    inline reference at(size_t n) const {
        if (n >= S_) {
            throw std::out_of_range("array ref range error");
        }

        return (*this)[n];
    }

    constexpr inline explicit operator bool() const noexcept {
        return (S_ > 0);
    }

    /**
     * Obtains a ref that is a view over the first `count` elements of this TArrayRef.
     *
     * The behavior is undefined if count > size().
     */
    TArrayRef first(size_t count) const {
        Y_ASSERT(count <= size());
        return TArrayRef(data(), count);
    }

    /**
     * Obtains a ref that is a view over the last `count` elements of this TArrayRef.
     *
     * The behavior is undefined if count > size().
     */
    TArrayRef last(size_t count) const {
        Y_ASSERT(count <= size());
        return TArrayRef(end() - count, end());
    }

    /**
     * Obtains a ref that is a view over the `count` elements of this TArrayRef starting at `offset`.
     *
     * The behavior is undefined in either offset or count is out of range.
     */
    TArrayRef subspan(size_t offset) const {
        Y_ASSERT(offset <= size());
        return TArrayRef(data() + offset, size() - offset);
    }

    TArrayRef subspan(size_t offset, size_t count) const {
        Y_ASSERT(offset + count <= size());
        return TArrayRef(data() + offset, count);
    }

    TArrayRef Slice(size_t offset) const {
        return subspan(offset);
    }

    TArrayRef Slice(size_t offset, size_t size) const {
        return subspan(offset, size);
    }

    /* FIXME:
     * This method is placed here for backward compatibility only and should be removed.
     * Keep in mind that it's behavior is different from Slice():
     *      SubRegion() never throws. It returns empty TArrayRef in case of invalid input.
     *
     * DEPRECATED. DO NOT USE.
     */
    TArrayRef SubRegion(size_t offset, size_t size) const {
        if (size == 0 || offset >= S_) {
            return TArrayRef();
        }

        if (size > S_ - offset) {
            size = S_ - offset;
        }

        return TArrayRef(T_ + offset, size);
    }

    constexpr inline yssize_t ysize() const noexcept {
        return static_cast<yssize_t>(this->size());
    }

private:
    T* T_;
    size_t S_;
};

/**
 * Obtains a view to the object representation of the elements of the TArrayRef arrayRef.
 *
 * Named as its std counterparts, std::as_bytes.
 */
template <typename T>
TArrayRef<const char> as_bytes(TArrayRef<T> arrayRef) noexcept {
    return TArrayRef<const char>(
        reinterpret_cast<const char*>(arrayRef.data()),
        arrayRef.size_bytes());
}

/**
 * Obtains a view to the writable object representation of the elements of the TArrayRef arrayRef.
 *
 * Named as its std counterparts, std::as_writable_bytes.
 */
template <typename T>
TArrayRef<char> as_writable_bytes(TArrayRef<T> arrayRef) noexcept {
    return TArrayRef<char>(
        reinterpret_cast<char*>(arrayRef.data()),
        arrayRef.size_bytes());
}

template <class Range>
constexpr TArrayRef<const typename Range::value_type> MakeArrayRef(const Range& range) {
    return TArrayRef<const typename Range::value_type>(range);
}

template <class Range>
constexpr TArrayRef<typename Range::value_type> MakeArrayRef(Range& range) {
    return TArrayRef<typename Range::value_type>(range);
}

template <class Range>
constexpr TArrayRef<const typename Range::value_type> MakeConstArrayRef(const Range& range) {
    return TArrayRef<const typename Range::value_type>(range);
}

template <class Range>
constexpr TArrayRef<const typename Range::value_type> MakeConstArrayRef(Range& range) {
    return TArrayRef<const typename Range::value_type>(range);
}

template <class T>
constexpr TArrayRef<T> MakeArrayRef(T* data, size_t size) {
    return TArrayRef<T>(data, size);
}

template <class T>
constexpr TArrayRef<T> MakeArrayRef(T* begin, T* end) {
    return TArrayRef<T>(begin, end);
}