local Logging = {}

local LogLevel = {
    TRACE = 0,
    DEBUG = 1,
    INFO = 2,
    WARNING = 3,
    CRITICAL = 4,
    ERROR = 5
}

for k,v in pairs(LogLevel) do
    LogLevel[v] = k
end

function LogLevel.isValid(value)
  return LogLevel[LogLevel[value]] ~= nil
end

function LogLevel.asNumber(value)
  return type(value) == "number" and value or LogLevel[value]
end

function LogLevel.isGreaterOrEqual(a, b)
  return a >= b
end

local Logger = {
  ignoreGlobals = true
}
Logger.__index = Logger

function Logger:new(o)
  local logger = {}
  local output = print
  if type(o) == "table" then
    if type(o.level) == "number" and LogLevel.isValid(o.level) then
      logger.level = o.level
    end
    if type(o.output) == "function" then
      output = o.output
    end
  end
  logger.output = output
  setmetatable(logger, self)
  logger.__index = logger

  return logger
end

function Logger:setIgnoreGlobals(ignoreGlobals)
  self.ignoreGlobals = ignoreGlobals
end

local LogPlain = {}
function LogPlain.of(value, deep)
  local obj = { value, deep = deep }
  setmetatable(obj, LogPlain)
  return obj
end

function LogPlain.is(value)
  return type(value) == "table" and getmetatable(value) == LogPlain
end

function LogPlain.isDeep(value)
  return LogPlain.is(value) and value.deep
end

function LogPlain.getValue(value)
  return LogPlain.is(value) and value[1] or value
end

local RecursionSentinel = {}
function RecursionSentinel.make(table, index)
  local obj = { table = table, index = index }
  setmetatable(obj, RecursionSentinel)
  return obj
end

function RecursionSentinel.isSentinel(value)
  return type(value) == "table" and getmetatable(value) == RecursionSentinel
end

function RecursionSentinel.isKnown(knownTables, value)
  return knownTables[value] ~= nil
end

function RecursionSentinel.getSentinel(knownTables, value)
    return knownTables[value] or RecursionSentinel.add(knownTables, value)
end

function RecursionSentinel.add(knownTables, value)
  local sentinel = RecursionSentinel.make(value, #knownTables)
  knownTables[value] = sentinel
  return sentinel
end

function RecursionSentinel.remove(knownTables, value)
  local sentinel = knownTables[value]
  if sentinel then
    sentinel.index = nil
  end
  knownTables[value] = nil
end

local function cloneNonRecursive(inValue, sentinels)
  if type(inValue) == "table" then
    local wrapLogPlain = LogPlain.is(inValue) and function(v) return LogPlain.of(v, inValue.deep) end or function(v) return v end
    local value = LogPlain.getValue(inValue)
    if RecursionSentinel.isKnown(sentinels, value) then
      return wrapLogPlain(RecursionSentinel.getSentinel(sentinels, value))
    end

    local sentinel = RecursionSentinel.getSentinel(sentinels, value)
    local clone = {}
    for i,v in ipairs(value) do
      clone[i] = cloneNonRecursive(v, sentinels)
    end
    for k,v in pairs(value) do
      clone[cloneNonRecursive(k, sentinels)] = cloneNonRecursive(v, sentinels)
    end
    RecursionSentinel.remove(sentinels, sentinel.table)

    sentinel.value = clone
    return wrapLogPlain(sentinel.value)
  else
    return inValue
  end
end

local G_ = {}
for k,v in pairs(_G) do
    G_[v] = k
end

local function _simpleStringify(inValue, builder, ignoreGlobals, skipFunctions, plain)
  local value = LogPlain.getValue(inValue)
  local isPlain = plain or LogPlain.is(inValue)
  local isDeep = plain or LogPlain.isDeep(inValue)
  if type(value) == "table" then
    if not isPlain then
        table.insert(builder, "<")
    end
    if ignoreGlobals and G_[value] ~= nil then
      table.insert(builder, "_G.")
      table.insert(builder, G_[value])
    elseif RecursionSentinel.isSentinel(value) then
      if isPlain then
        table.insert(builder, "<recurse_")
        table.insert(builder, tostring(value.index))
        table.insert(builder, ">")
      else
        table.insert(builder, "recurse ")
        table.insert(builder, tostring(value.value))
      end
    else
      if not isPlain then
        table.insert(builder, tostring(value))
        table.insert(builder, ">")
      end
      table.insert(builder, "{")
      local first = true
      for _,v in ipairs(value) do
        if not first then
          table.insert(builder, ",")
        else
          first = false
        end
        _simpleStringify(v, builder, ignoreGlobals, skipFunctions, isDeep)
      end
      first = #value == 0
      for k,v in pairs(value) do
        if not first then
          table.insert(builder, ",")
        else
          first = false
        end
        -- String key values are always plain-printed
        _simpleStringify(k, builder, ignoreGlobals, skipFunctions, isDeep or type(k) == "string")
        table.insert(builder, "=")
        _simpleStringify(v, builder, ignoreGlobals, skipFunctions, isDeep)
      end
      table.insert(builder, "}")
    end
    if not isPlain then
        table.insert(builder, ">")
    end
  elseif type(value) == "string" then
    if not isPlain then
      table.insert(builder, "\"")
    end
    table.insert(builder, tostring(value))
    if not isPlain then
      table.insert(builder, "\"")
    end
  elseif type(value) == "function" then
    if not skipFunctions then
        local info = debug.getinfo(value, "u")
        if not isPlain then
          table.insert(builder, tostring(value))
        end
        table.insert(builder, "(")
        table.insert(builder, tostring(info.nparams))
        if info.isvararg then
          table.insert(builder, "*")
        end
        table.insert(builder, ")")
    end
  elseif not skipFunctions or type(value) ~= "function" then
    table.insert(builder, tostring(value))
  end
end

local function deepStringify(value)
  local collect = {}
  _simpleStringify(cloneNonRecursive(value, {}), collect, false, false)
  return table.concat(collect)
end

function Logger:_doPrint(level, message, ...)
  if LogLevel.isGreaterOrEqual(level, self.level) then
    local result = { tostring(LogLevel[level]), deepStringify(message) }
    for _,v in ipairs({...}) do
      table.insert(result, deepStringify(v))
    end
    self.output(table.concat(result, " "))
  end
end

function Logger:trace(message, ...)
  self:_doPrint(LogLevel.TRACE, message, ...)
end

function Logger:debug(message, ...)
  self:_doPrint(LogLevel.DEBUG, message, ...)
end

function Logger:info(message, ...)
  self:_doPrint(LogLevel.INFO, message, ...)
end

function Logger:warn(message, ...)
  self:_doPrint(LogLevel.WARNING, message, ...)
end

function Logger:critical(message, ...)
  self:_doPrint(LogLevel.CRITICAL, message, ...)
end

function Logger:error(message, ...)
  self:_doPrint(LogLevel.ERROR, message, ...)
end

function Logger:setOutput(output)
  self.output = output
end

function Logger:setLevel(level)
  if LogLevel.isValid(level) then
    self.level = LogLevel.asNumber(level)
  end
end

function Logger.plain(value, deep)
  return LogPlain.of(value, deep)
end

Logging.Logger = Logger
Logging.LogLevel = LogLevel

function Logging.getGlobalLogger()
  if _G._GLOBAL_LOGGER == nil then
    _G._GLOBAL_LOGGER = Logger:new{ level = LogLevel.DEBUG, output = print }
  end
  return _G._GLOBAL_LOGGER
end

return Logging