//          Copyright Ferdinand Majerech 2011-2014.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

module dyaml.queue;


import std.traits : hasMember, hasIndirections;

package:

/// Simple queue implemented as a singly linked list with a tail pointer.
///
/// Needed in some D:YAML code that needs a queue-like structure without too much
/// reallocation that goes with an array.
///
/// Allocations are non-GC and are damped by a free-list based on the nodes
/// that are removed. Note that elements lifetime must be managed
/// outside.
struct Queue(T)
if (!hasMember!(T, "__xdtor"))
{

private:

    // Linked list node containing one element and pointer to the next node.
    struct Node
    {
        T payload_;
        Node* next_;
    }

    // Start of the linked list - first element added in time (end of the queue).
    Node* first_;
    // Last element of the linked list - last element added in time (start of the queue).
    Node* last_;
    // free-list
    Node* stock;

    // Length of the queue.
    size_t length_;

    // allocate a new node or recycle one from the stock.
    Node* makeNewNode(T thePayload, Node* theNext = null) @trusted nothrow @nogc
    {
        import std.experimental.allocator : make;
        import std.experimental.allocator.mallocator : Mallocator;

        Node* result;
        if (stock !is null)
        {
            result = stock;
            stock = result.next_;
            result.payload_ = thePayload;
            result.next_ = theNext;
        }
        else
        {
            result = Mallocator.instance.make!(Node)(thePayload, theNext);
            // GC can dispose T managed member if it thinks they are no used...
            static if (hasIndirections!T)
            {
                import core.memory : GC;
                GC.addRange(result, Node.sizeof);
            }
        }
        return result;
    }

    // free the stock of available free nodes.
    void freeStock() @trusted @nogc nothrow
    {
        import std.experimental.allocator.mallocator : Mallocator;

        while (stock !is null)
        {
            Node* toFree = stock;
            stock = stock.next_;
            static if (hasIndirections!T)
            {
                import core.memory : GC;
                GC.removeRange(toFree);
            }
            Mallocator.instance.deallocate((cast(ubyte*) toFree)[0 .. Node.sizeof]);
        }
    }

public:

    @disable void opAssign(ref Queue);
    @disable bool opEquals(ref Queue);
    @disable int opCmp(ref Queue);

    this(this) @safe nothrow @nogc
    {
        auto node = first_;
        first_ = null;
        last_ = null;
        while (node !is null)
        {
            Node* newLast = makeNewNode(node.payload_);
            if (last_ !is null)
                last_.next_ = newLast;
            if (first_ is null)
                first_      = newLast;
            last_ = newLast;
            node = node.next_;
        }
    }

    ~this() @safe nothrow @nogc
    {
        freeStock();
        stock = first_;
        freeStock();
    }

    /// Returns a forward range iterating over this queue.
    auto range() @safe pure nothrow @nogc
    {
        static struct Result
        {
            private Node* cursor;

            void popFront() @safe pure nothrow @nogc
            {
                cursor = cursor.next_;
            }
            ref T front() @safe pure nothrow @nogc
            in(cursor !is null)
            {
                return cursor.payload_;
            }
            bool empty() @safe pure nothrow @nogc const
            {
                return cursor is null;
            }
        }
        return Result(first_);
    }

    /// Push a new item to the queue.
    void push(T item) @nogc @safe nothrow
    {
        Node* newLast = makeNewNode(item);
        if (last_ !is null)
            last_.next_ = newLast;
        if (first_ is null)
            first_      = newLast;
        last_ = newLast;
        ++length_;
    }

    /// Insert a new item putting it to specified index in the linked list.
    void insert(T item, const size_t idx) @safe nothrow
    in
    {
        assert(idx <= length_);
    }
    do
    {
        if (idx == 0)
        {
            first_ = makeNewNode(item, first_);
            ++length_;
        }
        // Adding before last added element, so we can just push.
        else if (idx == length_)
        {
            push(item);
        }
        else
        {
            // Get the element before one we're inserting.
            Node* current = first_;
            foreach (i; 1 .. idx)
                current = current.next_;

            assert(current);
            // Insert a new node after current, and put current.next_ behind it.
            current.next_ = makeNewNode(item, current.next_);
            ++length_;
        }
    }

    /// Returns: The next element in the queue and remove it.
    T pop() @safe nothrow
    in
    {
        assert(!empty, "Trying to pop an element from an empty queue");
    }
    do
    {
        T result = peek();

        Node* oldStock = stock;
        Node* old = first_;
        first_ = first_.next_;

        // start the stock from the popped element
        stock = old;
        old.next_ = null;
        // add the existing "old" stock to the new first stock element
        if (oldStock !is null)
            stock.next_ = oldStock;

        if (--length_ == 0)
        {
            assert(first_ is null);
            last_ = null;
        }

        return result;
    }

    /// Returns: The next element in the queue.
    ref inout(T) peek() @safe pure nothrow inout @nogc
    in
    {
        assert(!empty, "Trying to peek at an element in an empty queue");
    }
    do
    {
        return first_.payload_;
    }

    /// Returns: true of the queue empty, false otherwise.
    bool empty() @safe pure nothrow const @nogc
    {
        return first_ is null;
    }

    /// Returns: The number of elements in the queue.
    size_t length() @safe pure nothrow const @nogc
    {
        return length_;
    }
}

@safe nothrow unittest
{
    auto queue = Queue!int();
    assert(queue.empty);
    foreach (i; 0 .. 65)
    {
        queue.push(5);
        assert(queue.pop() == 5);
        assert(queue.empty);
        assert(queue.length_ == 0);
    }

    int[] array = [1, -1, 2, -2, 3, -3, 4, -4, 5, -5];
    foreach (i; array)
    {
        queue.push(i);
    }

    array = 42 ~ array[0 .. 3] ~ 42 ~ array[3 .. $] ~ 42;
    queue.insert(42, 3);
    queue.insert(42, 0);
    queue.insert(42, queue.length);

    int[] array2;
    while (!queue.empty)
    {
        array2 ~= queue.pop();
    }

    assert(array == array2);
}