local Config = require("neoconf.config")

local M = {}
local uv = vim.uv or vim.loop
M.islist = vim.islist or vim.tbl_islist

function M.merge(...)
  local function can_merge(v)
    return type(v) == "table" and (vim.tbl_isempty(v) or not M.islist(v))
  end

  local values = { ... }
  local ret = values[1]
  for i = 2, #values, 1 do
    local value = values[i]
    if can_merge(ret) and can_merge(value) then
      for k, v in pairs(value) do
        ret[k] = M.merge(ret[k], v)
      end
    else
      ret = value
    end
  end
  return ret
end

function M.root_pattern(...)
  return require("lspconfig.util").root_pattern(...)
end

function M.find_git_ancestor(...)
  return vim.fs.dirname(vim.fs.find(".git", { path = startpath, upward = true })[1])
end

function M.has_lspconfig(server)
  return vim.tbl_contains(require("lspconfig.util").available_servers(), server)
end

---@param opts { on_config: fun(config, root_dir:string, original_config), root_dir: fun(), name: string }
function M.on_config(opts)
  local lsputil = require("lspconfig.util")
  local hook = lsputil.add_hook_before

  lsputil.on_setup = hook(lsputil.on_setup, function(initial_config)
    if opts.on_config then
      initial_config.on_new_config = hook(
        initial_config.on_new_config,
        M.protect(function(config, root_dir)
          opts.on_config(config, root_dir, initial_config)
        end, "Failed to run client.before_init" .. (opts.name and (" for " .. opts.name) or ""))
      )
    end
    if opts.root_dir then
      local root_dir = initial_config.root_dir
      initial_config.root_dir = function(...)
        local b = root_dir and root_dir(...)
        if not b then
          return
        end

        local a = opts.root_dir(...)

        if a and b then
          return M.pick_root_dir(a, b)
        end
        return a or b
      end
    end
  end)
end

function M.pick_root_dir(a, b)
  -- prefer the longest path
  return #a > #b and a or b
end

---@param t table
---@param ret? table
function M.flatten(t, ret)
  ret = ret or {}
  for _, v in pairs(t) do
    if type(v) == "table" then
      M.flatten(v, ret)
    else
      ret[#ret + 1] = v
    end
  end
  return ret
end

---@param opts? { local: boolean, global: boolean, autocmd: boolean }
---@return string[]
function M.file_patterns(opts)
  opts = M.merge({ ["local"] = true, ["global"] = true }, opts)
  local ret = {}

  if opts["local"] then
    for _, p in ipairs(Config.local_patterns) do
      table.insert(ret, p.pattern)
    end
  end

  if opts["global"] then
    for _, p in ipairs(Config.global_patterns) do
      table.insert(ret, p.pattern)
    end
  end

  if opts.autocmd then
    for i, v in ipairs(ret) do
      if v:find("/") then
        ret[i] = "*/" .. v
      end
    end
  end

  return vim.tbl_map(vim.fs.normalize, ret)
end

---@return { pattern: table<string, string>, filename: table<string, string> }
function M.filetype_patterns()
  local pattern = {}
  local filename = {}

  for _, p in ipairs(M.file_patterns({ autocmd = true })) do
    if p:find("/") ~= nil then
      pattern[".*/" .. p:gsub("%.", "%."):gsub("%*", ".*")] = "jsonc"
    else
      filename[p] = "jsonc"
    end
  end

  return {
    pattern = pattern,
    filename = filename,
  }
end

function M.path(str)
  local f = debug.getinfo(1, "S").source:sub(2)
  return M.fqn(vim.fn.fnamemodify(f, ":h:h:h") .. "/" .. (str or ""))
end

function M.read_file(file)
  local fd = io.open(file, "r")
  if not fd then
    error(("Could not open file %s for reading"):format(file))
  end
  local data = fd:read("*a")
  fd:close()
  return data
end

function M.write_file(file, data)
  local fd = io.open(file, "w+")
  if not fd then
    error(("Could not open file %s for writing"):format(file))
  end
  fd:write(data)
  fd:close()
end

function M.json_decode(json)
  json = vim.trim(json)
  if json == "" then
    json = "{}"
  end
  return require("neoconf.json.jsonc").decode_jsonc(json)
end

function M.fqn(fname)
  fname = vim.fn.fnamemodify(fname, ":p")
  return uv.fs_realpath(fname) or fname
end

---@param root_dir string
---@param file string
function M.has_file(root_dir, file)
  root_dir = M.fqn(root_dir)
  file = M.fqn(file)
  return M.exists(file) and file:find(root_dir, 1, true) == 1
end

function M.protect(fn, msg)
  return function(...)
    local args = { ... }

    return xpcall(function()
      return fn(unpack(args))
    end, function(err)
      local lines = {}
      if msg then
        table.insert(lines, msg)
      end
      table.insert(lines, err)
      table.insert(lines, debug.traceback("", 3))

      M.error(table.concat(lines, "\n"))
      return err
    end)
  end
end

function M.try(fn, msg)
  M.protect(fn, msg)()
end

function M.config_path()
  return uv.fs_realpath(vim.fn.stdpath("config"))
end

function M.is_nvim_config(path)
  return M.has_file(M.fqn(path), M.config_path())
end

---@param patterns SettingsPattern[]
---@param fn fun(file: string, key:string|nil, pattern:string)
---@param root_dir string
function M.for_each(patterns, fn, root_dir)
  for _, p in ipairs(patterns) do
    local file = root_dir .. "/" .. p.pattern
    ---@diagnostic disable-next-line: param-type-mismatch
    for _, f in pairs(vim.fn.expand(file, false, true)) do
      ---@diagnostic disable-next-line: param-type-mismatch
      fn(f, type(p.key) == "function" and p.key(f) or p.key, p.pattern)
    end
  end
end

---@param patterns string|(string|SettingsPattern)[]
---@return SettingsPattern[]
function M.expand(patterns)
  return vim.tbl_map(function(p)
    return type(p) == "string" and { pattern = p } or p
  end, type(patterns) == "table" and patterns or { patterns })
end

---@param root_dir string
---@param fn fun(file: string, key:string|nil, pattern:string)
function M.for_each_local(fn, root_dir)
  M.for_each(Config.local_patterns, fn, root_dir)
end

---@param fn fun(file: string, key:string|nil, pattern:string)
function M.for_each_global(fn)
  M.for_each(Config.global_patterns, fn, vim.fn.stdpath("config"))
end

function M.is_global(file)
  local ret = false
  M.for_each_global(function(f)
    if file == f then
      ret = true
    end
  end)
  return ret
end

function M.fetch(url)
  local fd = io.popen(string.format("curl -s -k %q", url))
  if not fd then
    error(("Could not download %s"):format(url))
  end
  local ret = fd:read("*a")
  fd:close()
  return ret
end

function M.json_format(obj)
  local tmp = os.tmpname()
  M.write_file(tmp, vim.json.encode(obj))
  local fd = io.popen("jq -S < " .. tmp)
  if not fd then
    error("Could not format json")
  end
  local ret = fd:read("*a")
  if ret == "" then
    error("Could not format json")
  end
  fd:close()
  return ret
end

function M.mtime(fname)
  local stat = uv.fs_stat(fname)
  return (stat and stat.type) and stat.mtime.sec or 0
end

function M.exists(fname)
  local stat = uv.fs_stat(fname)
  return (stat and stat.type) or false
end

function M.notify(msg, level)
  vim.notify(msg, level, {
    title = "settings.nvim",
    on_open = function(win)
      vim.api.nvim_set_option_value("conceallevel", 3, {
        win = win,
        scope = "local",
      })
      local buf = vim.api.nvim_win_get_buf(win)
      vim.api.nvim_set_option_value("filetype", "markdown", { buf = buf, scope = "local" })
      vim.api.nvim_set_option_value("spell", false, { buf = buf, scope = "local" })
    end,
  })
end

function M.warn(msg)
  M.notify(msg, vim.log.levels.WARN)
end

function M.error(msg)
  M.notify(msg, vim.log.levels.ERROR)
end

function M.info(msg)
  M.notify(msg, vim.log.levels.INFO)
end

return M
