local util = {}

local table_is_empty = function(t)
  return next(t) == nil
end

local is_in = function(value, list)
  for _,el in ipairs(list) do
    if value == el then
      return true
    end
  end
  return false
end

util.hash = function(v) -- TODO: maybe use Argon2 [https://luarocks.org/modules/thibaultcha/argon2]
  --xxhash:update(v)
  --return xxhash:digest()
  return v
end


--[[
  Uses 'schema' to verify:
    Existence of required value in 'req'
    They have proper types
    The DB references actually exist
  ]]
--[[ schema = {
  user_id = {'db_ref', to='user', required=true, load_as='user_record'} -- `load_as` so a DB search doesn't have to be done again after validation
  price = {'number', required=true}
  }
  ]]
util.validate = function(req, schema, db, sessions)
  local errors = {}
  local loaded_records = {}
  for property, rules in pairs(schema) do
    local expected_value_type = rules[1]
    local value = req[property]
    if rules.required == true then
      if value == nil then
        table.insert(errors, "Expected '"..property.."' to be provided.")
      end
    end
    if expected_value_type == 'number' or expected_value_type == 'string' or expected_value_type == 'table' then
      if type(value) ~= expected_value_type then
        table.insert(errors, "Expected '"..property.."' to be a "..expected_value_type..".")
      elseif rules.is_any_of ~= nil then
        if not is_in(value, rules.is_any_of) then
          table.insert(errors, "Expected '"..property.."' to be one of (".. table.concat(rules.is_any_of,',') ..").")
        end
      end
    elseif expected_value_type == 'db_ref' then
      local expected_record_type = rules.to
      local record = db:findOne(function(record) return record.id == value and record.type == expected_record_type end)
      if record == nil then
        table.insert(errors, "Expected '"..property.."' to point to a '"..expected_record_type.."' in the database.")
      else
        loaded_records[rules.load_as] = record
      end
    elseif expected_value_type == 'base64' then
      local _, start_of_base64_string_index = string.find(value, "data:.-/.-;base64,")
      start_of_base64_string_index = start_of_base64_string_index + 1
      local base64_adjusted = string.sub(value, start_of_base64_string_index)  -- must remove "data:*/*;base64," from the beginning of the string
      loaded_records[rules.load_as] = base64_adjusted
    elseif expected_value_type == 'session' then
      local session = sessions[value]
      if session == nil then
        table.insert(errors, "No such session.")
      else
        if rules.is_admin == true and db:getrecord(session.user_id).is_admin ~= true then
          table.insert(errors, "Current user is not admin.")
        else
          loaded_records.session = session
        end
      end
    end
  end
  return errors, loaded_records
end


util.run_every = function(interval_in_s, fn)
  local co = coroutine.create(function()
    local t1 = os.time()
    while true do
      if os.time()-t1 > interval_in_s*1000 then
        fn()
        t1 = os.time()
      end
      coroutine.yield()
      --posix.unistd.sleep(interval_in_s)
    end
    end)
  return co
end

util.setdefaults = function(t, defaults)
  for k,v in pairs(defaults) do
    if t[k] == nil then
      t[k] = v
    end
  end
end

util.merge = function(dest, source)
  for k,v in pairs(source) do
    dest[k] = v
  end
end

util.findOne = function(t, fn)
  for i,v in ipairs(t) do
    if fn(v, i) then return v end
  end
  return nil
end

util.removefromtable = function(t, v)
  local i = 1
  while t[i] ~= v do  i = i + 1  end
  table.remove(t, i)
  return i
end

return util