local floor, setmetatable = math.floor, setmetatable

local function siftup( self, from )
	local items, priorities, indices, higherpriority = self, self._priorities, self._indices, self._higherpriority
	local index = from
	local parent = floor( index / 2 )
	while index > 1 and higherpriority( priorities[index], priorities[parent] ) do
		priorities[index], priorities[parent] = priorities[parent], priorities[index]
		items[index], items[parent] = items[parent], items[index]
		indices[items[index]], indices[items[parent]] = index, parent
		index = parent
		parent = floor( index / 2 )
	end
	return index
end

local function siftdown( self, limit )
	local items, priorities, indices, higherpriority, size = self, self._priorities, self._indices, self._higherpriority, self._size
	for index = limit, 1, -1 do
		local left = index + index
		local right = left + 1
		while left <= size do
			local smaller = left
			if right <= size and higherpriority( priorities[right], priorities[left] ) then
				smaller = right
			end
			if higherpriority( priorities[smaller], priorities[index] ) then
				items[index], items[smaller] = items[smaller], items[index]
				priorities[index], priorities[smaller] = priorities[smaller], priorities[index]
				indices[items[index]], indices[items[smaller]] = index, smaller
			else
				break
			end
			index = smaller
			left = index + index
			right = left + 1
		end
	end
end

local PriorityQueueMt

local PriorityQueue = {}

local function minishigher( a, b )
	return a < b
end

local function maxishigher( a, b )
	return a > b
end

function PriorityQueue.new( priority_or_array )
	local t = type( priority_or_array )
	local higherpriority = minishigher
	
	if t == 'table' then
		higherpriority = priority_or_array.higherpriority or higherpriority
	elseif t == 'function' or t == 'string' then
		higherpriority = priority_or_array
	elseif t ~= 'nil' then
		local msg = 'Wrong argument type to PriorityQueue.new, it must be table or function or string, has: %q'
		error( msg:format( t ))
	end

	if type( higherpriority ) == 'string' then
		if higherpriority == 'min' then
			higherpriority = minishigher
		elseif higherpriority == 'max' then
			higherpriority = maxishigher
		else
			local msg = 'Wrong string argument to PriorityQueue.new, it must be "min" or "max", has: %q'
			error( msg:format( tostring( higherpriority )))
		end
	end

	local self = setmetatable( {
		_priorities = {},
		_indices = {},
		_size = 0,
		_higherpriority = higherpriority or minishigher
	}, PriorityQueueMt )

	if t == 'table' then
		self:batchenq( priority_or_array )
	end

	return self
end

function PriorityQueue:enqueue( item, priority )
	local items, priorities, indices = self, self._priorities, self._indices
	if indices[item] ~= nil then
		error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' )
	end
	local size = self._size + 1
	self._size = size	
	items[size], priorities[size], indices[item] = item, priority, size
	siftup( self, size ) 
	return self
end

function PriorityQueue:remove( item )
	local index = self._indices[item]
	if index ~= nil then
		local size = self._size
		local items, priorities, indices = self, self._priorities, self._indices
		indices[item] = nil
		if size == index then
			items[size], priorities[size] = nil, nil
			self._size = size - 1
		else
			local lastitem = items[size]
			items[index], priorities[index] = items[size], priorities[size]
			items[size], priorities[size] = nil, nil
			indices[lastitem] = index
			size = size - 1
			self._size = size
			if size > 1 then
				siftdown( self, siftup( self, index )) 
			end
		end
		return true
	else
		return false
	end
end

function PriorityQueue:contains( item )
	return self._indices[item] ~= nil
end

function PriorityQueue:update( item, priority )
	local ok = self:remove( item )
	if ok then
		self:enqueue( item, priority )
		return true
	else
		return false
	end
end

function PriorityQueue:dequeue()
	local size = self._size
	
	assert( size > 0, 'Heap is empty' )
	
	local items, priorities, indices = self, self._priorities, self._indices
	local item, priority = items[1], priorities[1]
	indices[item] = nil

	if size > 1 then
		local newitem = items[size]
		items[1], priorities[1] = newitem, priorities[size]
		items[size], priorities[size] = nil, nil
		indices[newitem] = 1
		size = size - 1
		self._size = size
		siftdown( self, 1 )
	else
		items[1], priorities[1] = nil, nil
		self._size = 0
	end

	return item, priority
end

function PriorityQueue:peek()
	return self[1], self._priorities[1]
end
	
function PriorityQueue:len()
	return self._size
end

function PriorityQueue:empty()
	return self._size <= 0
end

function PriorityQueue:batchenq( iparray )
	local items, priorities, indices = self, self._priorities, self._indices
	local size = self._size
	for i = 1, #iparray, 2 do
		local item, priority = iparray[i], iparray[i+1]
		if indices[item] ~= nil then
			error( 'Item ' .. tostring(indices[item]) .. ' is already in the heap' )
		end
		size = size + 1
		items[size], priorities[size] = item, priority
		indices[item] = size
	end
	self._size = size
	if size > 1 then
		siftdown( self, floor( size / 2 ))
	end
end

PriorityQueueMt = {
	__index = PriorityQueue,
	__len = PriorityQueue.len,
}
      
return setmetatable( PriorityQueue, {
	__call = function( _, ... )
		return PriorityQueue.new( ... )
	end
} )