---@brief lsp-diagnostic
---
---@class Diagnostic
---@field range Range
---@field message string
---@field severity DiagnosticSeverity|nil
---@field code number | string
---@field source string
---@field tags DiagnosticTag[]
---@field relatedInformation DiagnosticRelatedInformation[]
local M = {}
local DEFAULT_CLIENT_ID = -1
---@private
local function get_client_id(client_id)
  if client_id == nil then
    client_id = DEFAULT_CLIENT_ID
  end
  return client_id
end
---@private
local function get_bufnr(bufnr)
  if not bufnr then
    return vim.api.nvim_get_current_buf()
  elseif bufnr == 0 then
    return vim.api.nvim_get_current_buf()
  end
  return bufnr
end
---@private
local function severity_lsp_to_vim(severity)
  if type(severity) == 'string' then
    severity = vim.lsp.protocol.DiagnosticSeverity[severity]
  end
  return severity
end
---@private
local function severity_vim_to_lsp(severity)
  if type(severity) == 'string' then
    severity = vim.diagnostic.severity[severity]
  end
  return severity
end
---@private
local function line_byte_from_position(lines, lnum, col, offset_encoding)
  if not lines or offset_encoding == "utf-8" then
    return col
  end
  local line = lines[lnum + 1]
  local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16")
  if ok then
    return result
  end
  return col
end
---@private
local function get_buf_lines(bufnr)
  if vim.api.nvim_buf_is_loaded(bufnr) then
    return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  end
  local filename = vim.api.nvim_buf_get_name(bufnr)
  local f = io.open(filename)
  if not f then
    return
  end
  local content = f:read("*a")
  if not content then
    -- Some LSP servers report diagnostics at a directory level, in which case
    -- io.read() returns nil
    f:close()
    return
  end
  local lines = vim.split(content, "\n")
  f:close()
  return lines
end
---@private
local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
  local buf_lines = get_buf_lines(bufnr)
  local client = vim.lsp.get_client_by_id(client_id)
  local offset_encoding = client and client.offset_encoding or "utf-16"
  return vim.tbl_map(function(diagnostic)
    local start = diagnostic.range.start
    local _end = diagnostic.range["end"]
    return {
      lnum = start.line,
      col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding),
      end_lnum = _end.line,
      end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding),
      severity = severity_lsp_to_vim(diagnostic.severity),
      message = diagnostic.message,
      source = diagnostic.source,
      user_data = {
        lsp = {
          code = diagnostic.code,
          codeDescription = diagnostic.codeDescription,
          tags = diagnostic.tags,
          relatedInformation = diagnostic.relatedInformation,
          data = diagnostic.data,
        },
      },
    }
  end, diagnostics)
end
---@private
local function diagnostic_vim_to_lsp(diagnostics)
  return vim.tbl_map(function(diagnostic)
    return vim.tbl_extend("error", {
      range = {
        start = {
          line = diagnostic.lnum,
          character = diagnostic.col,
        },
        ["end"] = {
          line = diagnostic.end_lnum,
          character = diagnostic.end_col,
        },
      },
      severity = severity_vim_to_lsp(diagnostic.severity),
      message = diagnostic.message,
      source = diagnostic.source,
    }, diagnostic.user_data and (diagnostic.user_data.lsp or {}) or {})
  end, diagnostics)
end
local _client_namespaces = {}
--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|.
---
---@param client_id number The id of the LSP client
function M.get_namespace(client_id)
  vim.validate { client_id = { client_id, 'n' } }
  if not _client_namespaces[client_id] then
    local name = string.format("vim.lsp.client-%d", client_id)
    _client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
  end
  return _client_namespaces[client_id]
end
--- Save diagnostics to the current buffer.
---
--- Handles saving diagnostics from multiple clients in the same buffer.
---@param diagnostics Diagnostic[]
---@param bufnr number
---@param client_id number
---@private
function M.save(diagnostics, bufnr, client_id)
  local namespace = M.get_namespace(client_id)
  vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
end
-- }}}
--- |lsp-handler| for the method "textDocument/publishDiagnostics"
---
--- See |vim.diagnostic.config()| for configuration options. Handler-specific
--- configuration can be set using |vim.lsp.with()|:
--- 
--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
---   vim.lsp.diagnostic.on_publish_diagnostics, {
---     -- Enable underline, use default values
---     underline = true,
---     -- Enable virtual text, override spacing to 4
---     virtual_text = {
---       spacing = 4,
---     },
---     -- Use a function to dynamically turn signs off
---     -- and on, using buffer local variables
---     signs = function(bufnr, client_id)
---       return vim.bo[bufnr].show_signs == false
---     end,
---     -- Disable a feature
---     update_in_insert = false,
---   }
--- )
--- 
---
---@param config table Configuration table (see |vim.diagnostic.config()|).
function M.on_publish_diagnostics(_, result, ctx, config)
  local client_id = ctx.client_id
  local uri = result.uri
  local bufnr = vim.uri_to_bufnr(uri)
  if not bufnr then
    return
  end
  client_id = get_client_id(client_id)
  local namespace = M.get_namespace(client_id)
  local diagnostics = result.diagnostics
  if config then
    for _, opt in pairs(config) do
      if type(opt) == 'table' then
        if not opt.severity and opt.severity_limit then
          opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)}
        end
      end
    end
  end
  vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), config)
  -- Keep old autocmd for back compat. This should eventually be removed.
  vim.api.nvim_command("doautocmd  User LspDiagnosticsChanged")
end
--- Clear diagnotics and diagnostic cache.
---
--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
--- this method signature is still used internally in some parts of the LSP
--- implementation so it's simply marked @private rather than @deprecated.
---
---@param client_id number
---@param buffer_client_map table map of buffers to active clients
---@private
function M.reset(client_id, buffer_client_map)
  buffer_client_map = vim.deepcopy(buffer_client_map)
  vim.schedule(function()
    for bufnr, client_ids in pairs(buffer_client_map) do
      if client_ids[client_id] then
        local namespace = M.get_namespace(client_id)
        vim.diagnostic.reset(namespace, bufnr)
      end
    end
  end)
end
-- Deprecated Functions {{{
--- Get all diagnostics for clients
---
---@deprecated Prefer |vim.diagnostic.get()|
---
---@param client_id number Restrict included diagnostics to the client
---                        If nil, diagnostics of all clients are included.
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
function M.get_all(client_id)
  local result = {}
  local namespace
  if client_id then
    namespace = M.get_namespace(client_id)
  end
  for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
    local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace}))
    result[bufnr] = diagnostics
  end
  return result
end
--- Return associated diagnostics for bufnr
---
---@deprecated Prefer |vim.diagnostic.get()|
---
---@param bufnr number
---@param client_id number|nil If nil, then return all of the diagnostics.
---                            Else, return just the diagnostics associated with the client_id.
---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
  predicate = predicate or function() return true end
  if client_id == nil then
    local all_diagnostics = {}
    vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
      local iter_diagnostics = vim.tbl_filter(predicate, M.get(bufnr, iter_client_id))
      for _, diagnostic in ipairs(iter_diagnostics) do
        table.insert(all_diagnostics, diagnostic)
      end
    end)
    return all_diagnostics
  end
  local namespace = M.get_namespace(client_id)
  return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace})))
end
--- Get the diagnostics by line
---
--- Marked private as this is used internally by the LSP subsystem, but
--- most users should instead prefer |vim.diagnostic.get()|.
---
---@param bufnr number|nil The buffer number
---@param line_nr number|nil The line number
---@param opts table|nil Configuration keys
---         - severity: (DiagnosticSeverity, default nil)
---             - Only return diagnostics with this severity. Overrides severity_limit
---         - severity_limit: (DiagnosticSeverity, default nil)
---             - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---@param client_id|nil number the client id
---@return table Table with map of line number to list of diagnostics.
---              Structured: { [1] = {...}, [5] = {.... } }
---@private
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
  opts = opts or {}
  if opts.severity then
    opts.severity = severity_lsp_to_vim(opts.severity)
  elseif opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  if client_id then
    opts.namespace = M.get_namespace(client_id)
  end
  if not line_nr then
    line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1
  end
  opts.lnum = line_nr
  return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts))
end
--- Get the counts for a particular severity
---
---@deprecated Prefer |vim.diagnostic.get_count()|
---
---@param bufnr number The buffer number
---@param severity DiagnosticSeverity
---@param client_id number the client id
function M.get_count(bufnr, severity, client_id)
  severity = severity_lsp_to_vim(severity)
  local opts = { severity = severity }
  if client_id ~= nil then
    opts.namespace = M.get_namespace(client_id)
  end
  return #vim.diagnostic.get(bufnr, opts)
end
--- Get the previous diagnostic closest to the cursor_position
---
---@deprecated Prefer |vim.diagnostic.get_prev()|
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic
function M.get_prev(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1]
end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
---
---@deprecated Prefer |vim.diagnostic.get_prev_pos()|
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return vim.diagnostic.get_prev_pos(opts)
end
--- Move to the previous diagnostic
---
---@deprecated Prefer |vim.diagnostic.goto_prev()|
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return vim.diagnostic.goto_prev(opts)
end
--- Get the next diagnostic closest to the cursor_position
---
---@deprecated Prefer |vim.diagnostic.get_next()|
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic
function M.get_next(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1]
end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
---
---@deprecated Prefer |vim.diagnostic.get_next_pos()|
---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return vim.diagnostic.get_next_pos(opts)
end
--- Move to the next diagnostic
---
---@deprecated Prefer |vim.diagnostic.goto_next()|
---
---@param opts table|nil Configuration table. Keys:
---         - {client_id}: (number)
---             - If nil, will consider all clients attached to buffer.
---         - {cursor_position}: (Position, default current position)
---             - See |nvim_win_get_cursor()|
---         - {wrap}: (boolean, default true)
---             - Whether to loop around file or not. Similar to 'wrapscan'
---         - {severity}: (DiagnosticSeverity)
---             - Exclusive severity to consider. Overrides {severity_limit}
---         - {severity_limit}: (DiagnosticSeverity)
---             - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---         - {enable_popup}: (boolean, default true)
---             - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
---         - {popup_opts}: (table)
---             - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
---         - {win_id}: (number, default 0)
---             - Window ID
function M.goto_next(opts)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return vim.diagnostic.goto_next(opts)
end
--- Set signs for given diagnostics
---
---@deprecated Prefer |vim.diagnostic._set_signs()|
---
---@param diagnostics Diagnostic[]
---@param bufnr number The buffer number
---@param client_id number the client id
---@param sign_ns number|nil
---@param opts table Configuration for signs. Keys:
---             - priority: Set the priority of the signs.
---             - severity_limit (DiagnosticSeverity):
---                 - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_signs(diagnostics, bufnr, client_id, _, opts)
  local namespace = M.get_namespace(client_id)
  if opts and not opts.severity and opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Set underline for given diagnostics
---
---@deprecated Prefer |vim.diagnostic._set_underline()|
---
---@param diagnostics Diagnostic[]
---@param bufnr number: The buffer number
---@param client_id number: The client id
---@param diagnostic_ns number|nil: The namespace
---@param opts table: Configuration table:
---             - severity_limit (DiagnosticSeverity):
---                 - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_underline(diagnostics, bufnr, client_id, _, opts)
  local namespace = M.get_namespace(client_id)
  if opts and not opts.severity and opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Set virtual text given diagnostics
---
---@deprecated Prefer |vim.diagnostic._set_virtual_text()|
---
---@param diagnostics Diagnostic[]
---@param bufnr number
---@param client_id number
---@param diagnostic_ns number
---@param opts table Options on how to display virtual text. Keys:
---             - prefix (string): Prefix to display before virtual text on line
---             - spacing (number): Number of spaces to insert before virtual text
---             - severity_limit (DiagnosticSeverity):
---                 - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts)
  local namespace = M.get_namespace(client_id)
  if opts and not opts.severity and opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Default function to get text chunks to display using |nvim_buf_set_extmark()|.
---
---@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()|
---
---@param bufnr number The buffer to display the virtual text in
---@param line number The line number to display the virtual text on
---@param line_diags Diagnostic[] The diagnostics associated with the line
---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()|
---@return an array of [text, hl_group] arrays. This can be passed directly to
---        the {virt_text} option of |nvim_buf_set_extmark()|.
function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts)
  return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
end
--- Open a floating window with the diagnostics from {position}
---
---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()|
---
---@param opts table|nil Configuration keys
---         - severity: (DiagnosticSeverity, default nil)
---             - Only return diagnostics with this severity. Overrides severity_limit
---         - severity_limit: (DiagnosticSeverity, default nil)
---             - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---         - all opts for |show_diagnostics()| can be used here
---@param buf_nr number|nil The buffer number
---@param position table|nil The (0,0)-indexed position
---@return table {popup_bufnr, win_id}
function M.show_position_diagnostics(opts, buf_nr, position)
  if opts then
    if opts.severity then
      opts.severity = severity_lsp_to_vim(opts.severity)
    elseif opts.severity_limit then
      opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
    end
  end
  return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position)
end
--- Open a floating window with the diagnostics from {line_nr}
---
---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
---
---@param opts table Configuration table
---     - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
---          |show_diagnostics()| can be used here
---@param buf_nr number|nil The buffer number
---@param line_nr number|nil The line number
---@param client_id number|nil the client id
---@return table {popup_bufnr, win_id}
function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
  if client_id then
    opts = opts or {}
    opts.namespace = M.get_namespace(client_id)
  end
  return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr)
end
--- Redraw diagnostics for the given buffer and client
---
---@deprecated Prefer |vim.diagnostic.redraw()|
---
--- This calls the "textDocument/publishDiagnostics" handler manually using
--- the cached diagnostics already received from the server. This can be useful
--- for redrawing diagnostics after making changes in diagnostics
--- configuration. |lsp-handler-configuration|
---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Redraw diagnostics for the given
---       client. The default is to redraw diagnostics for all attached
---       clients.
function M.redraw(bufnr, client_id)
  bufnr = get_bufnr(bufnr)
  if not client_id then
    return vim.lsp.for_each_buffer_client(bufnr, function(client)
      M.redraw(bufnr, client.id)
    end)
  end
  local namespace = M.get_namespace(client_id)
  return vim.diagnostic.show(namespace, bufnr)
end
--- Sets the quickfix list
---
---@deprecated Prefer |vim.diagnostic.setqflist()|
---
---@param opts table|nil Configuration table. Keys:
---         - {open}: (boolean, default true)
---             - Open quickfix list after set
---         - {client_id}: (number)
---             - If nil, will consider all clients attached to buffer.
---         - {severity}: (DiagnosticSeverity)
---             - Exclusive severity to consider. Overrides {severity_limit}
---         - {severity_limit}: (DiagnosticSeverity)
---             - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---         - {workspace}: (boolean, default true)
---             - Set the list with workspace diagnostics
function M.set_qflist(opts)
  opts = opts or {}
  if opts.severity then
    opts.severity = severity_lsp_to_vim(opts.severity)
  elseif opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  if opts.client_id then
    opts.client_id = nil
    opts.namespace = M.get_namespace(opts.client_id)
  end
  local workspace = vim.F.if_nil(opts.workspace, true)
  opts.bufnr = not workspace and 0
  return vim.diagnostic.setqflist(opts)
end
--- Sets the location list
---
---@deprecated Prefer |vim.diagnostic.setloclist()|
---
---@param opts table|nil Configuration table. Keys:
---         - {open}: (boolean, default true)
---             - Open loclist after set
---         - {client_id}: (number)
---             - If nil, will consider all clients attached to buffer.
---         - {severity}: (DiagnosticSeverity)
---             - Exclusive severity to consider. Overrides {severity_limit}
---         - {severity_limit}: (DiagnosticSeverity)
---             - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---         - {workspace}: (boolean, default false)
---             - Set the list with workspace diagnostics
function M.set_loclist(opts)
  opts = opts or {}
  if opts.severity then
    opts.severity = severity_lsp_to_vim(opts.severity)
  elseif opts.severity_limit then
    opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
  end
  if opts.client_id then
    opts.client_id = nil
    opts.namespace = M.get_namespace(opts.client_id)
  end
  local workspace = vim.F.if_nil(opts.workspace, false)
  opts.bufnr = not workspace and 0
  return vim.diagnostic.setloclist(opts)
end
--- Disable diagnostics for the given buffer and client
---
---@deprecated Prefer |vim.diagnostic.disable()|
---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Disable diagnostics for the given
---       client. The default is to disable diagnostics for all attached
---       clients.
-- Note that when diagnostics are disabled for a buffer, the server will still
-- send diagnostic information and the client will still process it. The
-- diagnostics are simply not displayed to the user.
function M.disable(bufnr, client_id)
  if not client_id then
    return vim.lsp.for_each_buffer_client(bufnr, function(client)
      M.disable(bufnr, client.id)
    end)
  end
  bufnr = get_bufnr(bufnr)
  local namespace = M.get_namespace(client_id)
  return vim.diagnostic.disable(bufnr, namespace)
end
--- Enable diagnostics for the given buffer and client
---
---@deprecated Prefer |vim.diagnostic.enable()|
---
---@param bufnr (optional, number): Buffer handle, defaults to current
---@param client_id (optional, number): Enable diagnostics for the given
---       client. The default is to enable diagnostics for all attached
---       clients.
function M.enable(bufnr, client_id)
  if not client_id then
    return vim.lsp.for_each_buffer_client(bufnr, function(client)
      M.enable(bufnr, client.id)
    end)
  end
  bufnr = get_bufnr(bufnr)
  local namespace = M.get_namespace(client_id)
  return vim.diagnostic.enable(bufnr, namespace)
end
-- }}}
return M
-- vim: fdm=marker