lua/goto-preview.lua
local lib = require "goto-preview.lib"

local function get_offset_encoding()
  local client = vim.lsp.get_clients({ bufnr = 0 })[1]
  return client and client.offset_encoding or "utf-16"
end

local M = {
  conf = {
    width = 120, -- Width of the floating window
    height = 15, -- Height of the floating window
    border = { "↖", "─", "┐", "│", "┘", "─", "└", "│" }, -- Border characters of the floating window
    default_mappings = false, -- Bind default mappings
    resizing_mappings = false, -- Binds arrow keys to resizing the floating window.
    debug = false, -- Print debug information
    opacity = nil, -- 0-100 opacity level of the floating window where 100 is fully transparent.
    lsp_configs = {
      -- Lsp result configs
      get_config = function(data)
        lib.logger.debug("data from the lsp", vim.inspect(data))

        local uri = data.targetUri or data.uri
        local range = data.targetRange or data.range

        if range == nil then
          vim.notify("Warning: Range is nil, returning default configuration.", vim.log.levels.WARN)
          return uri, { 1, 0 } -- default safe values
        end

        return uri, { range.start.line + 1, range.start.character }
      end,
    },
    post_open_hook = nil, -- A function taking two arguments, a buffer and a window to be ran as a hook.
    post_close_hook = nil, -- A function taking two arguments, a buffer and a window to be ran as a hook.
    references = {
      provider = "telescope", -- telescope|fzf_lua|snacks|mini_pick|default
      telescope = nil,
    },
    focus_on_open = true, -- Focus the floating window when opening it.
    dismiss_on_move = false, -- Dismiss the floating window when moving the cursor.
    force_close = true, -- passed into vim.api.nvim_win_close's second argument. See :h nvim_win_close
    bufhidden = "wipe", -- the bufhidden option to set on the floating window. See :h bufhidden
    stack_floating_preview_windows = true, -- Whether to nest floating windows
    same_file_float_preview = true, -- Whether to open a new floating window for a reference within the current file
    preview_window_title = { enable = true, position = "left" }, -- Preview window title configuration
    zindex = 1, -- Starting zindex for the stack of floating windows
    vim_ui_input = false, -- Whether to override vim.ui.input with our custom implementation
  },
}

M.setup = function(conf)
  conf = conf or {}
  M.conf = vim.tbl_deep_extend("force", M.conf, conf)
  lib.setup_lib(M.conf)
  lib.logger.debug("non-lib:", vim.inspect(M.conf))
  lib.setup_aucmds()

  if M.conf.default_mappings then
    M.apply_default_mappings()
  end
  if M.conf.resizing_mappings then
    M.apply_resizing_mappings()
  end

  -- Setup custom input UI if enabled
  if M.conf.vim_ui_input then
    lib.setup_custom_input()
  end
end

local function print_lsp_error(lsp_call)
  print("goto-preview: Error calling LSP " .. lsp_call .. ". The current language lsp might not support it.")
end

--- Preview definition.
--- @param opts table: Custom config
---        • focus_on_open boolean: Focus the floating window when opening it.
---        • dismiss_on_move boolean: Dismiss the floating window when moving the cursor.
--- @see require("goto-preview").setup()
-- Helper function to get LSP clients that support a specific method
local function get_capable_clients(bufnr, method)
  local clients = vim.lsp.get_clients({ bufnr = bufnr })
  local capable_clients = {}

  -- Map LSP methods to their capability paths
  local capability_map = {
    ["textDocument/definition"] = "definitionProvider",
    ["textDocument/typeDefinition"] = "typeDefinitionProvider",
    ["textDocument/implementation"] = "implementationProvider",
    ["textDocument/declaration"] = "declarationProvider",
    ["textDocument/references"] = "referencesProvider",
  }

  local capability_key = capability_map[method]
  if not capability_key then
    lib.logger.debug("Unknown method:", method)
    return clients -- fallback to all clients
  end

  for _, client in ipairs(clients) do
    if client.supports_method(method) then
      table.insert(capable_clients, client)
      lib.logger.debug("Client supports", method, ":", client.name)
    else
      lib.logger.debug("Client does NOT support", method, ":", client.name)
    end
  end

  return capable_clients
end

M.lsp_request_definition = function(opts)
  local params = vim.lsp.util.make_position_params(nil, get_offset_encoding())
  local lsp_call = "textDocument/definition"

  -- Get clients that support definition requests
  local capable_clients = get_capable_clients(0, lsp_call)
  local all_clients = vim.lsp.get_clients({ bufnr = 0 })

  lib.logger.debug("Found", #capable_clients, "capable clients for", lsp_call, "out of", #all_clients, "total clients")

  -- Strategy: prefer capable clients, but fallback to all clients if none capable
  local clients_to_try = #capable_clients > 0 and capable_clients or all_clients
  
  if #capable_clients == 0 and #all_clients > 0 then
    lib.logger.debug("No capable clients found, attempting fallback with all", #all_clients, "clients")
  elseif #all_clients == 0 then
    lib.logger.debug("No LSP clients attached to buffer")
    print_lsp_error(lsp_call)
    return
  end

  -- Create a map for easier lookup during result processing  
  local client_ids_to_try = {}
  for _, client in ipairs(clients_to_try) do
    client_ids_to_try[client.id] = true
  end

  -- Use buf_request_all but only process results from our target clients
  local success, request_id = pcall(vim.lsp.buf_request_all, 0, lsp_call, params, function(results)
    lib.logger.debug("buf_request_all results:", vim.inspect(results))

    -- Process results only from clients we intended to query
    for client_id, result in pairs(results) do
      if result.result and not vim.tbl_isempty(result.result) and client_ids_to_try[client_id] then
        lib.logger.debug("Using result from client_id:", client_id)
        local handler = lib.get_handler(lsp_call, opts)
        handler(nil, result.result, nil, nil)
        return -- Only process the first valid result
      end
    end

    lib.logger.debug("No valid results found from target clients")
    print_lsp_error(lsp_call)
  end)

  lib.logger.debug("lsp_request_definition", success, "request_id:", request_id)
  if not success then
    print_lsp_error(lsp_call)
  end
end

--- Preview type definition.
--- @param opts table: Custom config
---        • focus_on_open boolean: Focus the floating window when opening it.
---        • dismiss_on_move boolean: Dismiss the floating window when moving the cursor.
--- @see require("goto-preview").setup()
M.lsp_request_type_definition = function(opts)
  local params = vim.lsp.util.make_position_params(nil, get_offset_encoding())
  local lsp_call = "textDocument/typeDefinition"
  local success, _ = pcall(vim.lsp.buf_request, 0, lsp_call, params, lib.get_handler(lsp_call, opts))
  lib.logger.debug("lsp_request_type_definition", success)
  if not success then
    print_lsp_error(lsp_call)
  end
end

--- Preview implementation.
--- @param opts table: Custom config
---        • focus_on_open boolean: Focus the floating window when opening it.
---        • dismiss_on_move boolean: Dismiss the floating window when moving the cursor.
--- @see require("goto-preview").setup()
M.lsp_request_implementation = function(opts)
  local params = vim.lsp.util.make_position_params(nil, get_offset_encoding())
  local lsp_call = "textDocument/implementation"
  local success, _ = pcall(vim.lsp.buf_request, 0, lsp_call, params, lib.get_handler(lsp_call, opts))
  lib.logger.debug("lsp_request_implementation", success)
  if not success then
    print_lsp_error(lsp_call)
  end
end

--- Preview declaration.
--- @param opts table: Custom config
---        • focus_on_open boolean: Focus the floating window when opening it.
---        • dismiss_on_move boolean: Dismiss the floating window when moving the cursor.
--- @see require("goto-preview").setup()
M.lsp_request_declaration = function(opts)
  local params = vim.lsp.util.make_position_params(nil, get_offset_encoding())
  local lsp_call = "textDocument/declaration"
  local success, _ = pcall(vim.lsp.buf_request, 0, lsp_call, params, lib.get_handler(lsp_call, opts))
  lib.logger.debug("lsp_request_declaration", success)
  if not success then
    print_lsp_error(lsp_call)
  end
end

M.lsp_request_references = function(opts)
  local params = vim.lsp.util.make_position_params(nil, get_offset_encoding())

  lib.logger.debug("params pre manipulation", vim.inspect(params))
  if not params.context then
    params.context = {
      includeDeclaration = true,
    }
  end
  lib.logger.debug("params post manipulation", vim.inspect(params))

  local lsp_call = "textDocument/references"
  local success, _ = pcall(vim.lsp.buf_request, 0, lsp_call, params, lib.get_handler(lsp_call, opts))
  if not success then
    print_lsp_error(lsp_call)
  end
end

M.close_all_win = function(options)
  local windows = vim.api.nvim_tabpage_list_wins(0)

  for _, win in pairs(windows) do
    local index = lib.tablefind(lib.windows, win)
    table.remove(lib.windows, index)

    if options and options.skip_curr_window then
      if win ~= vim.api.nvim_get_current_win() then
        pcall(lib.close_if_is_goto_preview, win)
      end
    else
      pcall(lib.close_if_is_goto_preview, win)
    end
  end
end

M.remove_win = lib.remove_win
M.buffer_entered = lib.buffer_entered
M.buffer_left = lib.buffer_left
M.dismiss_preview = lib.dismiss_preview
M.goto_preview_definition = M.lsp_request_definition
M.goto_preview_type_definition = M.lsp_request_type_definition
M.goto_preview_implementation = M.lsp_request_implementation
M.goto_preview_declaration = M.lsp_request_declaration
M.goto_preview_references = M.lsp_request_references
-- Mappings

M.apply_default_mappings = function()
  if M.conf.default_mappings then
    vim.keymap.set("n", "gpd", require("goto-preview").goto_preview_definition, { desc = "Preview definition" })
    vim.keymap.set("n", "gpt", require("goto-preview").goto_preview_type_definition, { desc = "Preview type definition" })
    vim.keymap.set("n", "gpi", require("goto-preview").goto_preview_implementation, {
      desc = "Preview implementation",
    })
    vim.keymap.set("n", "gpD", require("goto-preview").goto_preview_declaration, {
      desc = "Preview declaration",
    })
    vim.keymap.set("n", "gpr", require("goto-preview").goto_preview_references, { desc = "Preview references" })
    vim.keymap.set("n", "gP", require("goto-preview").close_all_win, { desc = "Close preview windows" })
  end
end

M.apply_resizing_mappings = function()
  if M.conf.resizing_mappings then
    vim.keymap.set("n", "<left>", "<C-w><", { noremap = true })
    vim.keymap.set("n", "<right>", "<C-w>>", { noremap = true })
    vim.keymap.set("n", "<up>", "<C-w>-", { noremap = true })
    vim.keymap.set("n", "<down>", "<C-w>+", { noremap = true })
  end
end

return M
