/*
 * ringbuffer.h
 *
 *  Created on: May 31, 2025
 *      Author: Attila Body
 */

#pragma once

#include <cstddef>
#include <cstdint>
#include <cstring>

namespace f4ll {

class iringbuffer
{
public:
    using size_type = size_t;

    /// @brief Copies data to the ring buffer (without committing it)
    /// @param data Pointer to the data to copy
    /// @param len Length of the data to copy
    /// @retval Length of the data copied (differs when not all data can fit)
    virtual size_type put(uint8_t const *data, size_type len) = 0;

    /// @brief Commits the data already placed into the buffer and notifies the
    /// consumer about it's availability
    virtual void commit() = 0;

    /// @brief Waits until all the data from the ring buffer gets consumed.
    // void flush();

    /// @brief Gets a pointer to the next chunk of committed data in the buffer
    /// without registering the consumption.
    ///        The caller should also call report_consumption using the returned
    ///        chunk length after it finished processing the data.
    /// @param[in]  len_requested Length of the data requested from the buffer.
    /// The length of the actual data provided
    ///             might be actually smaller (because either reaching the end of
    ///             the buffer or not enough data in the buffer).
    /// @param[out] data Receives a pointer to the first byte of the available
    /// data in the buffer
    /// @param[out] len Receives the length of the chunk available in the buffer.
    /// Will not exceed len_requested.
    /// @retval true if the buffer has more available data, false otherwise.
    virtual bool get_chunk(size_type len_requested, uint8_t const *&data, size_type &len) const = 0;

    /// @brief Marks the chunk returned by ringbuffer_GetChunk as available.
    /// @param consumed The length of the chunk as returned by
    /// ringbuffer_GetChunk(..., len)
    virtual void consumed(size_type len) = 0;

    /// @brief Returns the number of uncommited bytes in the ring buffer.
    virtual size_type uncommited() const = 0;

    /// @brief Returns the number of commited bytes in the ring buffer.
    virtual size_type commited() const = 0;

    /// @brief Discards the uncommited data in the ring buffer.
    virtual void discard() = 0;

    /// @brief Returns the size of the internal buffer.
    ///        One byte in the buffer is always reserved as separator
    virtual size_type size() const = 0;

    /// @brief Returns the size of the used place in the buffer
    //        (including non-commited data)
    virtual size_type used() const = 0;

    /// @brief Returns the free storage capacity of the buffer
    virtual size_type unused() const = 0;
};

template <iringbuffer::size_type SZ> class ringbuffer : public iringbuffer
{
public:
    ringbuffer() = default;

    size_type put(uint8_t const *data, size_type len) override;
    void commit() override;
    bool get_chunk(size_type len_requested, uint8_t const *&data, size_type &len) const override;
    void consumed(size_type len) override;
    size_type uncommited() const override;
    size_type commited() const override;
    void discard() override;
    size_type size() const override;
    size_type used() const override;
    size_type unused() const override;

private:
    uint8_t m_buffer[SZ];        //!< Data bufer
    size_type m_head = 0;        //!< Write position
    size_type m_head_shadow = 0; //!< Shadowed write position for collecting data
                                 //!< before committing it
    size_type m_tail = 0;        //!< Read position

    size_type marker_diff(size_type m1, size_type m2) const;
};

//
template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::put(uint8_t const *data, size_type len)
{
    size_type chunk1 = 0;
    size_type chunk2 = 0;

    if (!data || !len) {
        return 0;
    }

    size_type max_len = unused();
    len = len < max_len ? len : max_len;

    chunk1 = sizeof(m_buffer) - m_head_shadow;
    if (chunk1 >= len) {
        chunk1 = len;
    } else {
        chunk2 = len - chunk1;
    }

    std::memcpy(m_buffer + m_head_shadow, data, chunk1);
    m_head_shadow += chunk1;
    if (m_head_shadow == sizeof(m_buffer)) {
        m_head_shadow = 0;
    }

    if (chunk2) {
        std::memcpy(m_buffer, data + chunk1, chunk2);
        m_head_shadow += chunk2;
        if (m_head_shadow == sizeof(m_buffer)) {
            m_head_shadow = 0;
        }
    }

    return len;
}

template <iringbuffer::size_type SZ> void ringbuffer<SZ>::commit()
{
    m_head = m_head_shadow;
};

template <iringbuffer::size_type SZ> bool ringbuffer<SZ>::get_chunk(size_type len_requested, uint8_t const *&data, size_type &len) const
{
    if (!len_requested) {
        return false;
    }

    size_type head = m_head;
    size_type tail = m_tail;
    size_type chunk_size = head >= tail ? head - tail : sizeof(m_buffer) - tail;

    if (!chunk_size) {
        len = 0;
        return false;
    }

    if (chunk_size > len_requested) {
        chunk_size = len_requested;
    }
    data = m_buffer + tail;
    len = chunk_size;

    tail += chunk_size;
    if (tail == sizeof(m_buffer)) {
        tail = 0;
    }

    return tail != head;
}

template <iringbuffer::size_type SZ> void ringbuffer<SZ>::consumed(size_type len)
{
    if (!len) {
        return;
    }
    m_tail += len;
    if (m_tail == sizeof(m_buffer)) {
        m_tail = 0;
    }
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::uncommited() const
{
    return marker_diff(m_head_shadow, m_head);
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::commited() const
{
    return marker_diff(m_head, m_tail);
}

template <iringbuffer::size_type SZ> void ringbuffer<SZ>::discard()
{
    m_head_shadow = m_head;
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::size() const
{
    return SZ;
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::used() const
{
    return marker_diff(m_head_shadow, m_tail);
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::unused() const
{
    return SZ - used() - 1;
}

template <iringbuffer::size_type SZ> iringbuffer::size_type ringbuffer<SZ>::marker_diff(size_type m1, size_type m2) const
{
    return (m1 >= m2) ? (m1 - m2) : (SZ - m2 + m1);
}

} // namespace f1ll