-- log.lua
-- Does only support logging source files.
--
-- Inspired by rxi/log.lua
-- Modified by tjdevries and can be found at github.com/tjdevries/vlog.nvim
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.

local Path = require "plenary.path"

local p_debug = vim.fn.getenv "DEBUG_PLENARY"
if p_debug == vim.NIL then
  p_debug = false
end

-- User configuration section
local default_config = {
  -- Name of the plugin. Prepended to log messages.
  plugin = "plenary",

  -- Should print the output to neovim while running.
  -- values: 'sync','async',false
  use_console = "async",

  -- Should highlighting be used in console (using echohl).
  highlights = true,

  -- Should write to a file.
  -- Default output for logging file is `stdpath("log")/plugin.log`.
  use_file = true,

  -- Output file has precedence over plugin, if not nil.
  -- Used for the logging file, if not nil and use_file == true.
  outfile = nil,

  -- Should write to the quickfix list.
  use_quickfix = false,

  -- Any messages above this level will be logged.
  level = p_debug and "debug" or "info",

  -- Level configuration.
  modes = {
    { name = "trace", hl = "Comment" },
    { name = "debug", hl = "Comment" },
    { name = "info", hl = "None" },
    { name = "warn", hl = "WarningMsg" },
    { name = "error", hl = "ErrorMsg" },
    { name = "fatal", hl = "ErrorMsg" },
  },

  -- Can limit the number of decimals displayed for floats.
  float_precision = 0.01,

  -- Adjust content as needed, but must keep function parameters to be filled
  -- by library code.
  ---@param is_console boolean If output is for console or log file.
  ---@param mode_name string Level configuration 'modes' field 'name'
  ---@param src_path string Path to source file given by debug.info.source
  ---@param src_line integer Line into source file given by debug.info.currentline
  ---@param msg string Message, which is later on escaped, if needed.
  fmt_msg = function(is_console, mode_name, src_path, src_line, msg)
    local nameupper = mode_name:upper()
    local lineinfo = src_path .. ":" .. src_line
    if is_console then
      return string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg)
    else
      return string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg)
    end
  end,
}

-- {{{ NO NEED TO CHANGE
local log = {}

local unpack = unpack or table.unpack

log.new = function(config, standalone)
  config = vim.tbl_deep_extend("force", default_config, config)

  local outfile = vim.F.if_nil(
    config.outfile,
    Path:new(vim.api.nvim_call_function("stdpath", { "log" }), config.plugin .. ".log").filename
  )

  local obj
  if standalone then
    obj = log
  else
    obj = config
  end

  local levels = {}
  for i, v in ipairs(config.modes) do
    levels[v.name] = i
  end

  local round = function(x, increment)
    if x == 0 then
      return x
    end
    increment = increment or 1
    x = x / increment
    return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment
  end

  local make_string = function(...)
    local t = {}
    for i = 1, select("#", ...) do
      local x = select(i, ...)

      if type(x) == "number" and config.float_precision then
        x = tostring(round(x, config.float_precision))
      elseif type(x) == "table" then
        x = vim.inspect(x)
      else
        x = tostring(x)
      end

      t[#t + 1] = x
    end
    return table.concat(t, " ")
  end

  local log_at_level = function(level, level_config, message_maker, ...)
    -- Return early if we're below the config.level
    if level < levels[config.level] then
      return
    end
    local msg = message_maker(...)
    local info = debug.getinfo(config.info_level or 2, "Sl")
    local src_path = info.source:sub(2)
    local src_line = info.currentline
    -- Output to console
    if config.use_console then
      local log_to_console = function()
        local console_string = config.fmt_msg(true, level_config.name, src_path, src_line, msg)

        if config.highlights and level_config.hl then
          vim.cmd(string.format("echohl %s", level_config.hl))
        end

        local split_console = vim.split(console_string, "\n")
        for _, v in ipairs(split_console) do
          local formatted_msg = string.format("[%s] %s", config.plugin, vim.fn.escape(v, [["\]]))

          local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg))
          if not ok then
            vim.api.nvim_out_write(msg .. "\n")
          end
        end

        if config.highlights and level_config.hl then
          vim.cmd "echohl NONE"
        end
      end
      if config.use_console == "sync" and not vim.in_fast_event() then
        log_to_console()
      else
        vim.schedule(log_to_console)
      end
    end

    -- Output to log file
    if config.use_file then
      local outfile_parent_path = Path:new(outfile):parent()
      if not outfile_parent_path:exists() then
        outfile_parent_path:mkdir { parents = true }
      end
      local fp = assert(io.open(outfile, "a"))
      local str = config.fmt_msg(false, level_config.name, src_path, src_line, msg)
      fp:write(str)
      fp:close()
    end

    -- Output to quickfix
    if config.use_quickfix then
      local nameupper = level_config.name:upper()
      local formatted_msg = string.format("[%s] %s", nameupper, msg)
      local qf_entry = {
        -- remove the @ getinfo adds to the file path
        filename = info.source:sub(2),
        lnum = info.currentline,
        col = 1,
        text = formatted_msg,
      }
      vim.fn.setqflist({ qf_entry }, "a")
    end
  end

  for i, x in ipairs(config.modes) do
    -- log.info("these", "are", "separated")
    obj[x.name] = function(...)
      return log_at_level(i, x, make_string, ...)
    end

    -- log.fmt_info("These are %s strings", "formatted")
    obj[("fmt_%s"):format(x.name)] = function(...)
      return log_at_level(i, x, function(...)
        local passed = { ... }
        local fmt = table.remove(passed, 1)
        local inspected = {}
        for _, v in ipairs(passed) do
          table.insert(inspected, vim.inspect(v))
        end
        return string.format(fmt, unpack(inspected))
      end, ...)
    end

    -- log.lazy_info(expensive_to_calculate)
    obj[("lazy_%s"):format(x.name)] = function()
      return log_at_level(i, x, function(f)
        return f()
      end)
    end

    -- log.file_info("do not print")
    obj[("file_%s"):format(x.name)] = function(vals, override)
      local original_console = config.use_console
      config.use_console = false
      config.info_level = override.info_level
      log_at_level(i, x, make_string, unpack(vals))
      config.use_console = original_console
      config.info_level = nil
    end
  end

  return obj
end

log.new(default_config, true)
-- }}}

return log
