describe("dijkstras algorithm with binaryheap", function()
  -- See https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm

  it("calculates knight's shortest path", function()
    -- See http://stackoverflow.com/questions/2339101

    local binaryheap = require 'binaryheap'

    local ROWS = 8
    local COLS = 8

    local unvisited = binaryheap.minUnique()

    local function Cell(x, y)
      return x .. '_' .. y
    end

    local function Coordinates(cell)
      local x, y = cell:match('(%d+)_(%d+)')
      return x, y
    end

    local function neighbours(cell)
      local x, y = Coordinates(cell)
      local gen = coroutine.wrap(function()
        coroutine.yield(x - 1, y - 2)
        coroutine.yield(x - 1, y + 2)
        coroutine.yield(x + 1, y - 2)
        coroutine.yield(x + 1, y + 2)
        coroutine.yield(x - 2, y - 1)
        coroutine.yield(x - 2, y + 1)
        coroutine.yield(x + 2, y - 1)
        coroutine.yield(x + 2, y + 1)
      end)
      return coroutine.wrap(function()
        for xx, yy in gen do
          if 1 <= xx and xx <= COLS and
              1 <= yy and yy <= ROWS then
            coroutine.yield(Cell(xx, yy))
          end
        end
      end)
    end

    for y = 1, ROWS do
      for x = 1, COLS do
        local cell = Cell(x, y)
        unvisited:insert(math.huge, cell)
      end
    end
    unvisited:update('1_1', 0)

    local final_distance = {}

    while unvisited:peekValue() do
      local current, current_distance = unvisited:peek()
      assert(not final_distance[current])
      final_distance[current] = current_distance
      unvisited:remove(current)
      -- update neighbours
      local new_distance = current_distance + 1
      for neighbour in neighbours(current) do
        if unvisited.reverse[neighbour] then
          local pos = unvisited.reverse[neighbour]
          local distance = unvisited.values[pos]
          if distance > new_distance then
            unvisited:update(neighbour, new_distance)
          end
        end
      end
    end

    local rows = {}
    for y = 1, ROWS do
      local row = {}
      for x = 1, COLS do
        local cell = Cell(x, y)
        local distance = final_distance[cell]
        table.insert(row, distance)
      end
      table.insert(rows, table.concat(row, ' '))
    end

    assert.equal([[
0 3 2 3 2 3 4 5
3 4 1 2 3 4 3 4
2 1 4 3 2 3 4 5
3 2 3 2 3 4 3 4
2 3 2 3 4 3 4 5
3 4 3 4 3 4 5 4
4 3 4 3 4 5 4 5
5 4 5 4 5 4 5 6]],
    table.concat(rows, '\n'))
  end)
end)
