-- module will return the list of matchers, and registers matchers with the main assert engine

-- matchers take 1 parameters;
-- 1) state
-- 2) arguments list. The list has a member 'n' with the argument count to check for trailing nils
-- 3) level The level of the error position relative to the called function
-- returns; function (or callable object); a function that, given an argument, returns a boolean

local assert = require('luassert.assert')
local astate = require('luassert.state')
local util = require('luassert.util')
local s = require('say')

local function format(val)
  return astate.format_argument(val) or tostring(val)
end

local function unique(state, arguments, level)
  local deep = arguments[1]
  return function(value)
    local list = value
    for k,v in pairs(list) do
      for k2, v2 in pairs(list) do
        if k ~= k2 then
          if deep and util.deepcompare(v, v2, true) then
            return false
          else
            if v == v2 then
              return false
            end
          end
        end
      end
    end
    return true
  end
end

local function near(state, arguments, level)
  local level = (level or 1) + 1
  local argcnt = arguments.n
  assert(argcnt > 1, s("assertion.internal.argtolittle", { "near", 2, tostring(argcnt) }), level)
  local expected = tonumber(arguments[1])
  local tolerance = tonumber(arguments[2])
  local numbertype = "number or object convertible to a number"
  assert(expected, s("assertion.internal.badargtype", { 1, "near", numbertype, format(arguments[1]) }), level)
  assert(tolerance, s("assertion.internal.badargtype", { 2, "near", numbertype, format(arguments[2]) }), level)

  return function(value)
    local actual = tonumber(value)
    if not actual then return false end
    return (actual >= expected - tolerance and actual <= expected + tolerance)
  end
end

local function matches(state, arguments, level)
  local level = (level or 1) + 1
  local argcnt = arguments.n
  assert(argcnt > 0, s("assertion.internal.argtolittle", { "matches", 1, tostring(argcnt) }), level)
  local pattern = arguments[1]
  local init = arguments[2]
  local plain = arguments[3]
  assert(type(pattern) == "string", s("assertion.internal.badargtype", { 1, "matches", "string", type(arguments[1]) }), level)
  assert(init == nil or tonumber(init), s("assertion.internal.badargtype", { 2, "matches", "number", type(arguments[2]) }), level)

  return function(value)
    local actualtype = type(value)
    local actual = nil
    if actualtype == "string" or actualtype == "number" or
      actualtype == "table" and (getmetatable(value) or {}).__tostring then
      actual = tostring(value)
    end
    if not actual then return false end
    return (actual:find(pattern, init, plain) ~= nil)
  end
end

local function equals(state, arguments, level)
  local level = (level or 1) + 1
  local argcnt = arguments.n
  assert(argcnt > 0, s("assertion.internal.argtolittle", { "equals", 1, tostring(argcnt) }), level)
  return function(value)
    return value == arguments[1]
  end
end

local function same(state, arguments, level)
  local level = (level or 1) + 1
  local argcnt = arguments.n
  assert(argcnt > 0, s("assertion.internal.argtolittle", { "same", 1, tostring(argcnt) }), level)
  return function(value)
    if type(value) == 'table' and type(arguments[1]) == 'table' then
      local result = util.deepcompare(value, arguments[1], true)
      return result
    end
    return value == arguments[1]
  end
end

local function ref(state, arguments, level)
  local level = (level or 1) + 1
  local argcnt = arguments.n
  local argtype = type(arguments[1])
  local isobject = (argtype == "table" or argtype == "function" or argtype == "thread" or argtype == "userdata")
  assert(argcnt > 0, s("assertion.internal.argtolittle", { "ref", 1, tostring(argcnt) }), level)
  assert(isobject, s("assertion.internal.badargtype", { 1, "ref", "object", argtype }), level)
  return function(value)
    return value == arguments[1]
  end
end

local function is_true(state, arguments, level)
  return function(value)
    return value == true
  end
end

local function is_false(state, arguments, level)
  return function(value)
    return value == false
  end
end

local function truthy(state, arguments, level)
  return function(value)
    return value ~= false and value ~= nil
  end
end

local function falsy(state, arguments, level)
  local is_truthy = truthy(state, arguments, level)
  return function(value)
    return not is_truthy(value)
  end
end

local function is_type(state, arguments, level, etype)
  return function(value)
    return type(value) == etype
  end
end

local function is_nil(state, arguments, level)      return is_type(state, arguments, level, "nil")      end
local function is_boolean(state, arguments, level)  return is_type(state, arguments, level, "boolean")  end
local function is_number(state, arguments, level)   return is_type(state, arguments, level, "number")   end
local function is_string(state, arguments, level)   return is_type(state, arguments, level, "string")   end
local function is_table(state, arguments, level)    return is_type(state, arguments, level, "table")    end
local function is_function(state, arguments, level) return is_type(state, arguments, level, "function") end
local function is_userdata(state, arguments, level) return is_type(state, arguments, level, "userdata") end
local function is_thread(state, arguments, level)   return is_type(state, arguments, level, "thread")   end

assert:register("matcher", "true", is_true)
assert:register("matcher", "false", is_false)

assert:register("matcher", "nil", is_nil)
assert:register("matcher", "boolean", is_boolean)
assert:register("matcher", "number", is_number)
assert:register("matcher", "string", is_string)
assert:register("matcher", "table", is_table)
assert:register("matcher", "function", is_function)
assert:register("matcher", "userdata", is_userdata)
assert:register("matcher", "thread", is_thread)

assert:register("matcher", "ref", ref)
assert:register("matcher", "same", same)
assert:register("matcher", "matches", matches)
assert:register("matcher", "match", matches)
assert:register("matcher", "near", near)
assert:register("matcher", "equals", equals)
assert:register("matcher", "equal", equals)
assert:register("matcher", "unique", unique)
assert:register("matcher", "truthy", truthy)
assert:register("matcher", "falsy", falsy)
