/**
 * @file CircularQueue.h
 * @author apio (cloudapio.eu)
 * @brief Lock-free FIFO data structures.
 *
 * @copyright Copyright (c) 2022-2023, the Luna authors.
 *
 */

#pragma once
#include <luna/Atomic.h>
#include <luna/Heap.h>
#include <luna/Result.h>
#include <luna/Types.h>

/**
 * @brief An atomic lock-free circular FIFO queue.
 *
 * @tparam T The type of data to store in this queue.
 * @tparam Size The amount of elements to make space for.
 */
template <typename T, usize Size> class CircularQueue
{
    enum
    {
        Capacity = Size + 1
    };

  public:
    CircularQueue()
    {
    }

    /**
     * @brief Return whether the queue is empty.
     *
     * @return true The queue is empty.
     * @return false The queue is not empty.
     */
    bool is_empty() const
    {
        return m_tail.load() == m_head.load();
    }

    /**
     * @brief Push a value onto the queue.
     *
     * @param value The value to push.
     * @return true The operation succeeded.
     * @return false The queue was full or someone else was trying to push a value at the same time.
     */
    bool try_push(const T& value)
    {
        usize current_tail = m_tail.load(MemoryOrder::Relaxed);
        const usize new_tail = (current_tail + 1) % Capacity;
        if (new_tail == m_head.load(MemoryOrder::Acquire))
        {
            // Queue is full
            return false;
        }
        m_data[current_tail] = value;
        if (!m_tail.compare_exchange_strong(current_tail, new_tail, MemoryOrder::Release, MemoryOrder::Relaxed))
        {
            // Someone else updated the tail
            return false;
        }
        return true;
    }

    /**
     * @brief Pop a value from the queue.
     *
     * @param value The variable to store the value into.
     * @return true The operation succeeded.
     * @return false The queue was empty or someone else was trying to pop a value at the same time.
     */
    bool try_pop(T& value)
    {
        usize current_head = m_head.load(MemoryOrder::Relaxed);
        if (current_head == m_tail.load(MemoryOrder::Acquire))
        {
            // Queue is empty
            return false;
        }
        value = m_data[current_head];
        const usize new_head = (current_head + 1) % Capacity;
        if (!m_head.compare_exchange_strong(current_head, new_head, MemoryOrder::Release, MemoryOrder::Relaxed))
        {
            // Someone else updated the head
            return false;
        }
        return true;
    }

  private:
    T m_data[Capacity];
    Atomic<usize> m_head = 0;
    Atomic<usize> m_tail = 0;
};

/**
 * @brief An atomic lock-free circular FIFO queue.
 *
 * In this variant the size is set at runtime.
 *
 * @tparam T The type of data to store in this queue.
 */
template <typename T> class DynamicCircularQueue
{

  public:
    DynamicCircularQueue()
    {
    }

    ~DynamicCircularQueue()
    {
        if (m_data) free_impl(m_data);
    }

    /**
     * @brief Return whether the queue is empty.
     *
     * @return true The queue is empty.
     * @return false The queue is not empty.
     */
    bool is_empty() const
    {
        return m_tail.load() == m_head.load();
    }

    /**
     * @brief Set the size of the queue and allocate memory for it.
     *
     * This should not be used to grow the queue, as all existing data is lost. In most cases, this function will only
     * be called once to set the initial size of the queue and that's it.
     *
     * @param size The amount of elements to make space for.
     * @return Result<void> Whether the operation succeeded.
     */
    Result<void> set_size(usize size)
    {
        if (m_data) free_impl(m_data);
        m_data = (T*)TRY(calloc_impl(size + 1, sizeof(T), false));
        m_capacity = size + 1;
        return {};
    }

    /**
     * @brief Push a value onto the queue.
     *
     * @param value The value to push.
     * @return true The operation succeeded.
     * @return false The queue was full or someone else was trying to push a value at the same time.
     */
    bool try_push(const T& value)
    {
        check(m_capacity);
        usize current_tail = m_tail.load(MemoryOrder::Relaxed);
        const usize new_tail = (current_tail + 1) % m_capacity;
        if (new_tail == m_head.load(MemoryOrder::Acquire))
        {
            // Queue is full
            return false;
        }
        m_data[current_tail] = value;
        if (!m_tail.compare_exchange_strong(current_tail, new_tail, MemoryOrder::Release, MemoryOrder::Relaxed))
        {
            // Someone else updated the tail
            return false;
        }
        return true;
    }

    /**
     * @brief Pop a value from the queue.
     *
     * @param value The variable to store the value into.
     * @return true The operation succeeded.
     * @return false The queue was empty or someone else was trying to pop a value at the same time.
     */
    bool try_pop(T& value)
    {
        check(m_capacity);
        usize current_head = m_head.load(MemoryOrder::Relaxed);
        if (current_head == m_tail.load(MemoryOrder::Acquire))
        {
            // Queue is empty
            return false;
        }
        value = m_data[current_head];
        const usize new_head = (current_head + 1) % m_capacity;
        if (!m_head.compare_exchange_strong(current_head, new_head, MemoryOrder::Release, MemoryOrder::Relaxed))
        {
            // Someone else updated the head
            return false;
        }
        return true;
    }

  private:
    T* m_data = nullptr;
    usize m_capacity = 0;
    Atomic<usize> m_head = 0;
    Atomic<usize> m_tail = 0;
};