local M = {}

-- adds tokens to the current wait list, does not change order/unordered
M.wait = function(self, ...)
  local tlist = { ... }
  for _, token in ipairs(tlist) do
    if type(token) ~= "string" then
      error("Wait tokens must be strings. Got "..type(token), 2)
    end
    table.insert(self.tokens, token)
  end
end

-- set list as unordered, adds tokens to current wait list
M.wait_unordered = function(self, ...)
  self.ordered = false
  self:wait(...)
end

-- set list as ordered, adds tokens to current wait list
M.wait_ordered = function(self, ...)
  self.ordered = true
  self:wait(...)
end

-- generates a message listing tokens received/open
M.tokenlist = function(self)
  local list
  if #self.tokens_done == 0 then
    list = "No tokens received."
  else
    list = "Tokens received ("..tostring(#self.tokens_done)..")"
    local s = ": "
    for _,t in ipairs(self.tokens_done) do
      list = list .. s .. "'"..t.."'"
      s = ", "
    end
    list = list .. "."
  end
  if #self.tokens == 0 then
    list = list .. " No more tokens expected."
  else
    list = list .. " Tokens not received ("..tostring(#self.tokens)..")"
    local s = ": "
    for _, t in ipairs(self.tokens) do
      list = list .. s .. "'"..t.."'"
      s = ", "
    end
    list = list .. "."
  end
  return list
end

-- marks a token as completed, checks for ordered/unordered, checks for completeness
M.done = function(self, ...) self:_done(...) end  -- extra wrapper for same error level constant as __call method
M._done = function(self, token)
  if token then
    if type(token) ~= "string" then
      error("Wait tokens must be strings. Got "..type(token), 3)
    end
    if self.ordered then
      if self.tokens[1] == token then
        table.remove(self.tokens, 1)
        table.insert(self.tokens_done, token)
      else
        if self.tokens[1] then
          error(("Bad token, expected '%s' got '%s'. %s"):format(self.tokens[1], token, self:tokenlist()), 3)
        else
          error(("Bad token (no more tokens expected) got '%s'. %s"):format(token, self:tokenlist()), 3)
        end
      end
    else
      -- unordered
      for i, t in ipairs(self.tokens) do
        if t == token then
          table.remove(self.tokens, i)
          table.insert(self.tokens_done, token)
          token = nil
          break
        end
      end
      if token then
        error(("Unknown token '%s'. %s"):format(token, self:tokenlist()), 3)
      end
    end
  end
  if not next(self.tokens) then
    -- no more tokens, so we're really done...
    self.done_cb()
  end
end


-- wraps a done callback into a done-object supporting tokens to sign-off
M.new = function(done_callback)
  local obj = {
    tokens = {},
    tokens_done = {},
    done_cb = done_callback,
    ordered = true,  -- default for sign off of tokens
  }
  return setmetatable( obj, {
      __call = function(self, ...)
        self:_done(...)
      end,
      __index = M,
    })
end

return M
