"""FIFO queue that preserves history until explicitly cleared.

.. warning::

    One must define the type "QueueItem" before including this file. This makes
    it possible to store different types as QueueItems.

This queue can be operated like a class. The structure stores the state of the
instance and the contained functions act as instance methods. The important
distinction compared to normal queues is that popping an element doesn't remove
it internally. Thus unless the queue's internal buffer is explicitly cleared
(see `queue_clear`) popped items can be restored at anytime using
`queue_restore`.

A possible application of this special functionality might be when actions
performed on popped items must be undone at a later stage.

Example
-------
::

    cdef QueueWithHistory queue
    cdef QueueItem item

    queue_init(&queue, 5)
    try:
        for i in range(10):
            item = <QueueItem>i
            queue_push(&queue, &item)

        while queue_pop(&queue, &item):
            print(item)  # Prints 0 to 9

        queue_restore(&queue)

        while queue_pop(&queue, &item):
            print(item)  # Prints 0 to 9 again
    finally:
        queue_exit(&queue)
"""

from libc.stdlib cimport malloc, realloc, free


# Store state of queue
cdef struct QueueWithHistory:
    QueueItem* _buffer_ptr
    Py_ssize_t _buffer_size  # Maximal number of elements the buffer can store
    Py_ssize_t _index_valid  # Index to most recently inserted item
    Py_ssize_t _index_consumed  # Index to most recently consumed item


cdef inline void queue_init(QueueWithHistory* self, Py_ssize_t buffer_size) noexcept nogil:
    """Initialize the queue and its buffer size.

    The size is defined as the number of queue items to fit into the initial
    buffer, thus its true memory size is `buffer_size * sizeof(QueueItem)`. Be
    sure to call `queue_exit` after calling this function to free the allocated
    memory!
    """
    self._buffer_ptr = <QueueItem*>malloc(buffer_size * sizeof(QueueItem))
    if not self._buffer_ptr:
        with gil:
            raise MemoryError("couldn't allocate buffer")
    self._buffer_size = buffer_size
    self._index_consumed = -1
    self._index_valid = -1


cdef inline void queue_push(QueueWithHistory* self, QueueItem* item_ptr) noexcept nogil:
    """Enqueue a new item."""
    self._index_valid += 1
    if self._buffer_size <= self._index_valid:
        _queue_grow_buffer(self)
    self._buffer_ptr[self._index_valid] = item_ptr[0]


cdef inline unsigned char queue_pop(QueueWithHistory* self,
                                    QueueItem* item_ptr) noexcept nogil:
    """If not empty pop an item and return 1 otherwise return 0.

    The item is still preserved in the internal buffer and can be restored with
    `queue_restore`. To truly clear the internal buffer use `queue_clear`.
    """
    if 0 <= self._index_consumed + 1 <= self._index_valid:
        self._index_consumed += 1
        item_ptr[0] = self._buffer_ptr[self._index_consumed]
        return 1
    return 0


cdef inline void queue_restore(QueueWithHistory* self) noexcept nogil:
    """Restore all consumed items to the queue.

    The order of the restored queue is the same as previous one, meaning older
    items are popped first.
    """
    self._index_consumed = -1


cdef inline void queue_clear(QueueWithHistory* self) noexcept nogil:#
    """Remove all consumable items.

    After this the old items can't be restored with `queue_restore`.
    """
    self._index_consumed = -1
    self._index_valid = -1


cdef inline void queue_exit(QueueWithHistory* self) noexcept nogil:
    """Free the buffer of the queue.

    Don't use the queue after this command unless `queue_init` is called again.
    """
    free(self._buffer_ptr)


cdef inline int _queue_grow_buffer(QueueWithHistory* self) except -1 nogil:
    """Double the memory used for the buffer."""
    cdef QueueItem* new_buffer
    self._buffer_size *= 2
    new_buffer_ptr = <QueueItem*>realloc(
        self._buffer_ptr,
        self._buffer_size * sizeof(QueueItem)
    )
    if not new_buffer_ptr:
        with gil:
            raise MemoryError("couldn't reallocate buffer")
    self._buffer_ptr = new_buffer_ptr

    return 0
