--- A double queue.
-- Taken from ***Programming in Lua*** [Queues and Double Queues](http://www.lua.org/pil/11.4.html)
-- and modified to not allow nil values, and returns nil if @{pop_first} or @{pop_last} is used when the queue is empty.
-- @module Misc.Queue
-- @usage local Queue = require('__stdlib__/stdlib/misc/queue')
-- local q = Queue() -- create a new empty queue
-- q('my value') -- push a value onto the queue
-- q() -- pop the last value off the queue
-- game.print(#q) -- print the number of items in the queue

local Queue = {
    __class = 'Queue',
    __index = require('__stdlib__/stdlib/core')
}
setmetatable(Queue, Queue)

local table = require('__stdlib__/stdlib/utils/table')
local t_size = table_size

local Inspect = require('__stdlib__/stdlib/vendor/inspect')

local meta = {}

function Queue.__call(_, ...)
    local queue = { first = 1, last = 0, objects = {} }
    setmetatable(queue, meta)
    for _, push in pairs { ... } do
        queue(push)
    end
    return queue
end

--- Constructs a new Queue object.
-- @param ... mixed, values to push into the queue
-- @treturn @{Queue} a new queue
function Queue.new(...)
    return Queue.__call(nil, ...)
end

--- Load global.queue or queues during on_load, as metatables are not persisted.
-- <p>This is only needed if you are using the queue as an object and storing it in global.
-- @tparam table queue (<span class="types">@{Queue}</span>,...)
-- @usage global.myqueue1 = Queue.new()
-- script.on_load(function() Queue.load(global.myqueue))
function Queue.load(queue)
    if type(queue) == 'table' and queue.first then
        return setmetatable(queue, meta)
    end
end

--- Push a new element to the front of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam Mixed value the element to push
function Queue.push_first(queue, ...)
    for _, value in pairs { ... } do
        queue.first = queue.first - 1
        queue.objects[queue.first] = value
    end
    return queue
end

--- Push a new element to the back of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam Mixed ... the element(s) to push
function Queue.push_last(queue, ...)
    for _, value in pairs { ... } do
        queue.last = queue.last + 1
        queue.objects[queue.last] = value
    end
    return queue
end

--- Shortcut for @{Queue.push_last}
-- @function Queue.push
Queue.push = Queue.push_last

--- Push a new element to a specific location of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam number index the index to push to.
-- @tparam Mixed value the element to push.
function Queue.push_at(queue, index, value)
    if index < queue.first then
        return Queue.push_first(queue, value)
    elseif index > queue.last then
        return Queue.push_last(queue, value)
    else
        table.insert(queue.objects, index, value)
        queue.last = queue.last + 1
    end
    return queue
end

function Queue.wrapper(self, func_name, ...)
    if Queue[func_name] then
        Queue[func_name](self, ...)
    end
    return self
end

--- Retrieve the element at the front of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed value the element at the front of the queue
function Queue.pop_first(queue)
    if Queue.is_empty(queue) then
        return nil
    end
    local first = queue.first
    local value = queue.objects[first]
    queue.objects[first] = nil -- to allow garbage collection
    queue.first = first + 1
    return value
end

--- Shortcut for @{Queue.pop_first}
-- @function Queue.pop
Queue.pop = Queue.pop_first

local function remove(queue, index)
    local ret = queue.objects[index]
    if ret ~= nil then
        for i = index + 1, queue.last do
            queue.objects[i - 1] = queue.objects[i]
        end
        queue.objects[queue.last] = nil
        queue.last = queue.last - 1
    end
    return ret
end

--- Pop an element at a specific location of the queue.
-- @tparam Queue queue the queue to push an element to
-- @tparam number index the index to push to.
-- @treturn Mixed value the popped element.
function Queue.pop_at(queue, index)
    return remove(queue, index)
end

--- Peek at an element in the queue without disturbing the queue.
-- @tparam Queue queue the queue to peek at
-- @tparam number index the index in the queue to peek at
-- @treturn Mixed the value of the peeked element
function Queue.peek_at(queue, index)
    return queue.objects[index]
end

--- Return the element at the front of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the front of the queue
function Queue.peek_first(queue)
    return queue.objects[queue.first]
end

--- Shortcut for @{Queue.peek_first}
-- @function Queue.peek
Queue.peek = Queue.peek_first

--- Retrieve the element at the back of the queue and remove it from the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the back of the queue
function Queue.pop_last(queue)
    if queue.is_empty(queue) then
        return nil
    end

    local last = queue.last
    local value = queue.objects[last]
    queue.objects[last] = nil -- to allow garbage collection
    queue.last = last - 1
    return value
end

--- Return the element at the back of the queue.
-- @tparam Queue queue the queue to retrieve the element from
-- @treturn Mixed the element at the back of the queue
function Queue.peek_last(queue)
    return queue.objects[queue.last]
end

--- Returns the popped value and pushes back into the queue.
-- @tparam Queue queue the queue
-- @return Mixed the value that was popped.
function Queue.pop_and_push(queue)
    local ret = queue.pop(queue)
    queue.push(queue, ret)
    return ret
end

--- Returns the queue after popping the last element and pushing it to the top.
-- @tparam Queue queue the queue
-- @treturn @{Queue} the queue
function Queue.cycle(queue)
    return queue.push(queue, queue.pop(queue))
end

--- Gets the first index which matches the stored data. does not compare inside tables.
function Queue.find(queue, find)
    for i, v in pairs(queue) do
        if v == find then
            return i
        end
    end
end

local function _sort_func(a, b)
    local lhs = type(a) == 'table' and '{' or tostring(a)
    local rhs = type(b) == 'table' and '{' or tostring(b)
    return lhs < rhs
end

--- sort and reorder the queue
function Queue.sort(queue, func)
    local sorted = {}
    for _, v in pairs(queue) do
        if v ~= nil then
            sorted[#sorted + 1] = v
        end
    end
    table.sort(sorted, func or _sort_func)
    queue.objects = sorted
    queue.first, queue.last = 1, #queue.objects
    return queue
end

--- Returns true if the given queue is empty.
-- @tparam Queue queue the queue to check
-- @treturn boolean true if empty, false otherwise
function Queue.is_empty(queue)
    return queue.first > queue.last
end

--- Returns the number of items in the queue.
-- @tparam Queue queue the queue to check
-- @treturn number the number of items in the queue
function Queue.size(queue)
    return t_size(queue.objects)
end

--- Shortcut for @{Queue.size}
-- @function Queue.count
Queue.count = Queue.size

--- Return the next element in the queue
-- @tparam Queue queue the queue to check
-- @tparam number|nil index if nil return the first value, else return the next index value
-- @tparam boolean pop pop the value off the queue
-- @treturn number|nil the index
-- @treturn Mixed|nil the value at queue index
function Queue.next(queue, index, pop)
    index = not index and queue.first or index + (pop and 0 or 1)
    for i = index, queue.last do
        local v = queue.objects[i]
        if v ~= nil then
            return i, pop and Queue.pop_at(queue, i) or v
        end
    end
    return nil, nil
end

--- Return the previous element in the queue
-- @tparam Queue queue the queue to check
-- @tparam number|nil index if nil return the last value, else return the previous index value
-- @tparam boolean pop pop the value off the queue
-- @treturn number|nil the index
-- @treturn Mixed|nil the value at queue index
function Queue.rnext(queue, index, pop)
    -- next returns index of next or nil and data,
    index = not index and queue.last or (index < queue.first and queue.first or index) - 1
    for i = index, queue.first, -1 do
        local v = queue.objects[i]
        if v ~= nil then
            return i, pop and Queue.pop_at(queue, i) or v
        end
    end
    return nil, nil
end

local function next_pop(queue, index)
    return Queue.next(queue, index, true)
end

local function rnext_pop(queue, index)
    return Queue.rnext(queue, index, true)
end

--- Iterate the queue forward
function Queue.pairs(queue, pop)
    return pop and next_pop or Queue.next, queue, nil
end

--- Iterate the queue backwards
function Queue.rpairs(queue, pop)
    return pop and rnext_pop or Queue.rnext, queue, nil
end

do
    meta.__class = 'queue'
    meta.__len = Queue.size
    meta.__unm = Queue.pop
    meta.__parent = Queue
    meta.__debugline = [[<Queue>{[}first={first},last={last}{]}]]

    -- Allows queue[3] to return the item at queue.objects[3]
    meta.__index = function(self, k)
        if type(k) == 'number' then
            return self:peek_at(k)
        else
            local v = rawget(self, k)
            if v == nil then
                return Queue[k]
            end
            return v
        end
    end

    meta.__newindex = function(self, k, v)
        if type(k) == 'number' then
            if v ~= nil then
                self:push_at(k, v)
            else
                error('Attempt to modify Queue structure')
            end
        else
            rawset(self, k, v)
        end
    end

    -- Allows queue() to pop_first and queue(data) to push_last
    meta.__call = function(self, ...)
        if ... then
            return self:push(...)
        else
            return self:pop()
        end
    end

    meta.__tostring = function(self)
        return Inspect({ first = self.first, last = self.last, objects = self.objects }, { arraykeys = true })
    end

    meta.__add = function(queue1, queue2)
        local new = Queue.new()
        local lhs = getmetatable(queue1) == meta and true
        local rhs = getmetatable(queue2) == meta and true
        if lhs then
            for _, v in pairs(queue1.objects) do
                new:push(v)
            end
        else
            new:push(queue1)
        end
        if rhs then
            for _, v in pairs(queue2.objects) do
                new:push(v)
            end
        else
            new:push(queue2)
        end
        return new
    end
end

return Queue
