#ifndef TILE_BASE_BUFFER_H
#define TILE_BASE_BUFFER_H

#pragma once

#include "tile/base/buffer/builtin_buffer_block.h"
#include "tile/base/buffer/polymorphic_buffer.h"
#include "tile/base/internal/meta.h"
#include "tile/base/internal/singly_linked_list.h"
#include "tile/base/internal/utility.h"
#include "tile/base/slice.h"

#include <algorithm>
#include <limits>
#include <numeric>
#include <string>
#include <utility>
#include <vector>

namespace tile {
namespace detail {
template<typename T>
constexpr auto
data(const T &c) -> enable_if_t<std::is_convertible<decltype(c.data()), const char *>::value, const char *>
{
    return c.data();
}

template<std::size_t N>
constexpr auto
data(const char (&c)[N]) -> decltype(c)
{
    return c;
}

template<typename T>
constexpr std::size_t
size(const T &c)
{
    return c.size();
}

template<std::size_t N>
struct size_impl {
    static constexpr std::size_t size(const char (&c)[N]) { return N - (c[N - 1] == '\0'); }
};

template<>
struct size_impl<0> {
    static constexpr std::size_t size(const char (&c)[0]) { return 0; }
};

template<std::size_t N>
constexpr std::size_t
size(const char (&c)[N])
{
    return size_impl<N>::size(c);
}

template<typename A1>
constexpr std::size_t
total_size(const A1 &a1)
{
    return size(a1);
}

template<typename A1, typename... Args>
constexpr std::size_t
total_size(const A1 &a1, const Args &...args)
{
    return size(a1) + total_size(args...);
}

}// namespace detail

class NoncontiguousBuffer {
    // <buffer_t, buffer_t::chain>
    using LinkedBuffers = internal::SinglyLinkedList<PolymorphicBuffer, &PolymorphicBuffer::chain>;

public:
    using iterator                  = LinkedBuffers::iterator;
    using const_iterator            = LinkedBuffers::const_iterator;
    using buffer_t                  = PolymorphicBuffer;

    constexpr NoncontiguousBuffer() = default;

    // Copyable & moveable
    NoncontiguousBuffer(const NoncontiguousBuffer &other);
    NoncontiguousBuffer &operator=(const NoncontiguousBuffer &other);

    NoncontiguousBuffer(NoncontiguousBuffer &&other) noexcept
        : byte_size_(internal::Exchange(other.byte_size_, 0)),
          buffers_(std::move(other.buffers_))
    {}

    NoncontiguousBuffer &operator=(NoncontiguousBuffer &&other) noexcept
    {
        if (TILE_UNLIKELY(&other == this)) { return *this; }

        Clear();
        std::swap(byte_size_, other.byte_size_);
        buffers_ = std::move(other.buffers_);
        return *this;
    }

    ~NoncontiguousBuffer() { Clear(); }

    Slice FirstContiguous() const noexcept
    {
        TILE_DCHECK(!Empty());
        auto &&first = buffers_.front();
        return Slice(first.data(), first.size());
    }

    void Skip(size_t bytes) noexcept
    {
        TILE_DCHECK_LE(bytes, ByteSize());
        if (TILE_UNLIKELY(bytes == 0)) {
        } else if (bytes < buffers_.front().size()) {
            buffers_.front().Skip(bytes);
            byte_size_ -= bytes;
        } else {
            SkipSlow(bytes);
        }
    }

    NoncontiguousBuffer Cut(std::size_t bytes)
    {
        TILE_DCHECK_LE(bytes, ByteSize());
        TILE_DCHECK_GE(bytes, 0);

        NoncontiguousBuffer rc;
        auto left = bytes;
        while (left && left >= buffers_.front().size()) {
            left -= buffers_.front().size();
            rc.buffers_.push_back(buffers_.pop_front());
        }

        if (TILE_LIKELY(left > 0)) {
            auto ncb = object_pool::Get<PolymorphicBuffer>().Leak();
            *ncb     = buffers_.front();
            ncb->set_size(left);
            rc.buffers_.push_back(ncb);
            buffers_.front().Skip(left);
        }
        rc.byte_size_ = bytes;
        byte_size_ -= bytes;
        return rc;
    }

    void Append(PolymorphicBuffer buffer)
    {
        if (TILE_UNLIKELY(buffer.size() == 0)) { return; }

        auto block = object_pool::Get<PolymorphicBuffer>();
        *block     = std::move(buffer);
        byte_size_ += block->size();
        buffers_.push_back(block.Leak());
    }

    void Append(NoncontiguousBuffer buffer)
    {
        byte_size_ += internal::Exchange(buffer.byte_size_, 0);
        buffers_.splice(std::move(buffer.buffers_));
    }

    std::size_t ByteSize() const noexcept { return byte_size_; }

    bool Empty() const noexcept
    {
        TILE_CHECK_EQ(buffers_.empty(), !byte_size_);
        return !byte_size_;
    }

    void Clear() noexcept
    {
        if (!Empty()) { ClearSlow(); }
    }

    iterator begin() noexcept { return buffers_.begin(); }

    iterator end() noexcept { return buffers_.end(); }

    const_iterator begin() const noexcept { return buffers_.begin(); }

    const_iterator end() const noexcept { return buffers_.end(); }

private:
    void SkipSlow(size_t bytes) noexcept;
    void ClearSlow() noexcept;

private:
    std::size_t byte_size_{};
    LinkedBuffers buffers_;
};

class NoncontiguousBufferBuilder {
    static constexpr auto kAppendViaCopyThreshold = 128;

public:
    NoncontiguousBufferBuilder() { InitializeNextBlock(); }

    // current write ptr
    // It's size is available at `SizeAvailable()`
    char *data() const noexcept { return current_->mutable_data() + used_; }

    std::size_t SizeAvailable() const noexcept { return current_->size() - used_; }

    void MarkWritten(std::size_t bytes)
    {
        TILE_DCHECK_LE(bytes, SizeAvailable(), "You're overflowing the buffer.");
        used_ += bytes;
        // Is fulled ?
        if (TILE_UNLIKELY(!SizeAvailable())) {
            FlushCurrentBlock();
            InitializeNextBlock();
        }
    }

    // Allocate new block
    // return write ptr
    char *Reserve(std::size_t bytes)
    {
        static const auto kMaxBytes = 1024;
        TILE_CHECK_LE(bytes, kMaxBytes, "At most [{}] bytes may be reserved in single call.", kMaxBytes);
        if (SizeAvailable() < bytes) {
            FlushCurrentBlock();
            InitializeNextBlock();
        }

        auto ptr = data();
        MarkWritten(bytes);
        return ptr;
    }

    // Total number of bytes written
    std::size_t ByteSize() const noexcept { return nb_.ByteSize() + used_; }

    NoncontiguousBuffer DestructiveGet() noexcept
    {
        FlushCurrentBlock();
        return std::move(nb_);
    }

    void Append(const void *ptr, std::size_t length)
    {
        auto current = data();
        used_ += length;
        if (TILE_LIKELY(used_ < current_->size())) {
            memcpy(static_cast<void *>(current), ptr, length);
            return;
        }
        used_ -= length;
        AppendSlow(ptr, length);
    }

    void Append(Slice s) { return Append(s.data(), s.size()); }

    void Append(const std::string &s) { Append(s.data(), s.size()); }

    void Append(PolymorphicBuffer buffer)
    {
        if (buffer.size() < kAppendViaCopyThreshold && SizeAvailable() >= buffer.size()) {
            Append(buffer.data(), buffer.size());
            return;
        }

        if (used_) {
            FlushCurrentBlock();
            InitializeNextBlock();
        }

        nb_.Append(std::move(buffer));
    }

    void Append(NoncontiguousBuffer buffer)
    {
        if (buffer.ByteSize() < kAppendViaCopyThreshold && SizeAvailable()) {
            AppendCopy(buffer);
            return;
        }

        if (used_) {
            FlushCurrentBlock();
            InitializeNextBlock();
        }
        nb_.Append(std::move(buffer));
    }

    void Append(char c) { Append(static_cast<unsigned char>(c)); }

    void Append(unsigned char c)
    {
        TILE_DCHECK(SizeAvailable());
        *reinterpret_cast<unsigned char *>(data()) = c;
        MarkWritten(1);
    }

    template<typename... Ts,
             typename = internal::void_t<decltype(detail::data(std::declval<Ts &>()))...>,
             typename = internal::void_t<decltype(detail::size(std::declval<Ts &>()))...>>
    void Append(const Ts &...buffers)
    {
        auto current = data();
        auto total   = detail::total_size(buffers...);
        used_ += total;
        if (TILE_LIKELY(used_ < current_->size())) {
            UncheckedAppend(current, buffers...);
            return;
        }
        used_ -= total;
        int dummy[] = {(Append(buffers), 0)...};
    }

    template<typename T>
    auto Append(T) -> enable_if_t<std::is_same<T, int>::value>
    {
        static_assert(sizeof(T) == 0, "Please use Append(char) instead.");
    }

private:
    // Allocate a new buffer
    void InitializeNextBlock();

    void FlushCurrentBlock();

    void AppendSlow(const void *ptr, std::size_t length);
    void AppendCopy(const NoncontiguousBuffer &buffer);

    template<typename T, typename...>
    inline void UncheckedAppend(char *ptr, const T &slice)
    {
        memcpy(ptr, detail::data(slice), detail::size(slice));
    }

    template<typename T, typename... Ts>
    inline void UncheckedAppend(char *ptr, const T &slice, const Ts &...slices)
    {
        memcpy(ptr, detail::data(slice), detail::size(slice));
        UncheckedAppend(ptr + detail::size(slice), slices...);
    }

private:
    NoncontiguousBuffer nb_;
    std::size_t used_;
    RefPtr<NativeBufferBlock> current_;
};

namespace detail {
void FlattenToSlowSlow(const NoncontiguousBuffer &ncb, void *buffer, std::size_t size);
}

NoncontiguousBuffer CreateBufferSlow(Slice s);
NoncontiguousBuffer CreateBufferSlow(const void *ptr, std::size_t size);

std::string FlattenSlow(const NoncontiguousBuffer &nb, std::size_t max_bytes = std::numeric_limits<std::size_t>::max());
std::string FlattenSlowUntil(const NoncontiguousBuffer &nb,
                             Slice delim,
                             std::size_t max_bytes = std::numeric_limits<std::size_t>::max());

inline void
FlattenToSlow(const NoncontiguousBuffer &nb, void *buffer, std::size_t max_bytes)
{
    if (TILE_LIKELY(max_bytes <= nb.FirstContiguous().size())) {
        std::memcpy(buffer, nb.FirstContiguous().data(), max_bytes);
    }
    return detail::FlattenToSlowSlow(nb, buffer, max_bytes);
}

PolymorphicBuffer MakeReferencingBuffer(const void *ptr, std::size_t size);

template<typename F>
PolymorphicBuffer
MakeReferencingBuffer(const void *ptr, std::size_t size, F &&completion_cb)
{
    using BufferBlock = ReferencingBufferBlock<typename std::remove_reference<F>::type>;
    return PolymorphicBuffer(
        MakeRefCounted<BufferBlock>(ptr, size, std::forward<typename std::remove_reference<F>::type>(completion_cb)), 0,
        size);
}

PolymorphicBuffer MakeForeignBuffer(std::string buffer);

// `T` in (`std::byte`, integral types, floating types)
template<typename T>
PolymorphicBuffer MakeForeignBuffer(std::vector<T> buffer);

bool StartsWith(NoncontiguousBuffer buffer, Slice prefix);
bool EndsWith(NoncontiguousBuffer buffer, Slice suffix);
bool StartsWithIgnoreCase(NoncontiguousBuffer buffer, Slice prefix);
bool EndsWithIgnoreCase(NoncontiguousBuffer buffer, Slice suffix);
}// namespace tile

#endif// TILE_BASE_BUFFER_H
