---@class InspectorFilter
---@field syntax boolean include syntax based highlight groups (defaults to true)
---@field treesitter boolean include treesitter based highlight groups (defaults to true)
---@field extmarks boolean|"all" include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
---@field semantic_tokens boolean include semantic token highlights (defaults to true)
local defaults = {
  syntax = true,
  treesitter = true,
  extmarks = true,
  semantic_tokens = true,
}

---Get all the items at a given buffer position.
---
---Can also be pretty-printed with `:Inspect!`. *:Inspect!*
---
---@param bufnr? integer defaults to the current buffer
---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
---@param filter? InspectorFilter (table|nil) a table with key-value pairs to filter the items
---               - syntax (boolean): include syntax based highlight groups (defaults to true)
---               - treesitter (boolean): include treesitter based highlight groups (defaults to true)
---               - extmarks (boolean|"all"): include extmarks. When `all`, then extmarks without a `hl_group` will also be included (defaults to true)
---               - semantic_tokens (boolean): include semantic tokens (defaults to true)
---@return {treesitter:table,syntax:table,extmarks:table,semantic_tokens:table,buffer:integer,col:integer,row:integer} (table) a table with the following key-value pairs. Items are in "traversal order":
---               - treesitter: a list of treesitter captures
---               - syntax: a list of syntax groups
---               - semantic_tokens: a list of semantic tokens
---               - extmarks: a list of extmarks
---               - buffer: the buffer used to get the items
---               - row: the row used to get the items
---               - col: the col used to get the items
function vim.inspect_pos(bufnr, row, col, filter)
  filter = vim.tbl_deep_extend('force', defaults, filter or {})

  bufnr = bufnr or 0
  if row == nil or col == nil then
    -- get the row/col from the first window displaying the buffer
    local win = bufnr == 0 and vim.api.nvim_get_current_win() or vim.fn.bufwinid(bufnr)
    if win == -1 then
      error('row/col is required for buffers not visible in a window')
    end
    local cursor = vim.api.nvim_win_get_cursor(win)
    row, col = cursor[1] - 1, cursor[2]
  end
  bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr

  local results = {
    treesitter = {},
    syntax = {},
    extmarks = {},
    semantic_tokens = {},
    buffer = bufnr,
    row = row,
    col = col,
  }

  -- resolve hl links
  ---@private
  local function resolve_hl(data)
    if data.hl_group then
      local hlid = vim.api.nvim_get_hl_id_by_name(data.hl_group)
      local name = vim.fn.synIDattr(vim.fn.synIDtrans(hlid), 'name')
      data.hl_group_link = name
    end
    return data
  end

  -- treesitter
  if filter.treesitter then
    for _, capture in pairs(vim.treesitter.get_captures_at_pos(bufnr, row, col)) do
      capture.hl_group = '@' .. capture.capture .. '.' .. capture.lang
      results.treesitter[#results.treesitter + 1] = resolve_hl(capture)
    end
  end

  -- syntax
  if filter.syntax and vim.api.nvim_buf_is_valid(bufnr) then
    vim.api.nvim_buf_call(bufnr, function()
      for _, i1 in ipairs(vim.fn.synstack(row + 1, col + 1)) do
        results.syntax[#results.syntax + 1] =
          resolve_hl({ hl_group = vim.fn.synIDattr(i1, 'name') })
      end
    end)
  end

  -- namespace id -> name map
  local nsmap = {}
  for name, id in pairs(vim.api.nvim_get_namespaces()) do
    nsmap[id] = name
  end

  --- Convert an extmark tuple into a map-like table
  --- @private
  local function to_map(extmark)
    extmark = {
      id = extmark[1],
      row = extmark[2],
      col = extmark[3],
      opts = resolve_hl(extmark[4]),
    }
    extmark.ns_id = extmark.opts.ns_id
    extmark.ns = nsmap[extmark.ns_id] or ''
    extmark.end_row = extmark.opts.end_row or extmark.row -- inclusive
    extmark.end_col = extmark.opts.end_col or (extmark.col + 1) -- exclusive
    return extmark
  end

  --- Check if an extmark overlaps this position
  --- @private
  local function is_here(extmark)
    return (row >= extmark.row and row <= extmark.end_row) -- within the rows of the extmark
      and (row > extmark.row or col >= extmark.col) -- either not the first row, or in range of the col
      and (row < extmark.end_row or col < extmark.end_col) -- either not in the last row or in range of the col
  end

  -- all extmarks at this position
  local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true })
  extmarks = vim.tbl_map(to_map, extmarks)
  extmarks = vim.tbl_filter(is_here, extmarks)

  if filter.semantic_tokens then
    results.semantic_tokens = vim.tbl_filter(function(extmark)
      return extmark.ns:find('vim_lsp_semantic_tokens') == 1
    end, extmarks)
  end

  if filter.extmarks then
    results.extmarks = vim.tbl_filter(function(extmark)
      return extmark.ns:find('vim_lsp_semantic_tokens') ~= 1
        and (filter.extmarks == 'all' or extmark.opts.hl_group)
    end, extmarks)
  end

  return results
end

---Show all the items at a given buffer position.
---
---Can also be shown with `:Inspect`. *:Inspect*
---
---@param bufnr? integer defaults to the current buffer
---@param row? integer row to inspect, 0-based. Defaults to the row of the current cursor
---@param col? integer col to inspect, 0-based. Defaults to the col of the current cursor
---@param filter? InspectorFilter (table|nil) see |vim.inspect_pos()|
function vim.show_pos(bufnr, row, col, filter)
  local items = vim.inspect_pos(bufnr, row, col, filter)

  local lines = { {} }

  ---@private
  local function append(str, hl)
    table.insert(lines[#lines], { str, hl })
  end

  ---@private
  local function nl()
    table.insert(lines, {})
  end

  ---@private
  local function item(data, comment)
    append('  - ')
    append(data.hl_group, data.hl_group)
    append(' ')
    if data.hl_group ~= data.hl_group_link then
      append('links to ', 'MoreMsg')
      append(data.hl_group_link, data.hl_group_link)
      append(' ')
    end
    if comment then
      append(comment, 'Comment')
    end
    nl()
  end

  -- treesitter
  if #items.treesitter > 0 then
    append('Treesitter', 'Title')
    nl()
    for _, capture in ipairs(items.treesitter) do
      item(capture, capture.lang)
    end
    nl()
  end

  -- semantic tokens
  if #items.semantic_tokens > 0 then
    append('Semantic Tokens', 'Title')
    nl()
    local sorted_marks = vim.fn.sort(items.semantic_tokens, function(left, right)
      local left_first = left.opts.priority < right.opts.priority
        or left.opts.priority == right.opts.priority and left.opts.hl_group < right.opts.hl_group
      return left_first and -1 or 1
    end)
    for _, extmark in ipairs(sorted_marks) do
      item(extmark.opts, 'priority: ' .. extmark.opts.priority)
    end
    nl()
  end

  -- syntax
  if #items.syntax > 0 then
    append('Syntax', 'Title')
    nl()
    for _, syn in ipairs(items.syntax) do
      item(syn)
    end
    nl()
  end

  -- extmarks
  if #items.extmarks > 0 then
    append('Extmarks', 'Title')
    nl()
    for _, extmark in ipairs(items.extmarks) do
      if extmark.opts.hl_group then
        item(extmark.opts, extmark.ns)
      else
        append('  - ')
        append(extmark.ns, 'Comment')
        nl()
      end
    end
    nl()
  end

  if #lines[#lines] == 0 then
    table.remove(lines)
  end

  local chunks = {}
  for _, line in ipairs(lines) do
    vim.list_extend(chunks, line)
    table.insert(chunks, { '\n' })
  end
  if #chunks == 0 then
    chunks = {
      {
        'No items found at position '
          .. items.row
          .. ','
          .. items.col
          .. ' in buffer '
          .. items.buffer,
      },
    }
  end
  vim.api.nvim_echo(chunks, false, {})
end
