local protocol = require 'vim.lsp.protocol'
local lsp_util = vim.lsp.util

vim.lsp.set_log_level("off")

local preview_location_callback = function(_, result)
  if result == nil or vim.tbl_isempty(result) then
    return nil
  end
  vim.lsp.util.preview_location(result[1])
end

local peek_definition = function()
  local params = vim.lsp.util.make_position_params()
  return vim.lsp.buf_request(0, 'textDocument/definition', params, preview_location_callback)
end

local inlay_hint_toggle = function()
  vim.lsp.inlay_hint.enable(nil, not vim.lsp.inlay_hint.is_enabled())
end

_G.InlayHintToggle = inlay_hint_toggle
_G.PeekDefinition  = peek_definition

protocol.CompletionItemKind = {
  '󰉿 Text'         ;
  '󰡱 Method'       ;
  '󰊕 Function'     ;
  ' Constructor'  ;
  ' Field'        ;
  ' Variable'     ;
  ' Class'        ;
  ' Interface'    ;
  '󰏗 Module'       ;
  ' Property'     ;
  ' Unit'         ;
  '󰮸 Value'        ;
  ' Enum'         ;
  '󰌋 Keyword'      ;
  '󰷈 Snippet'      ;
  '󰌁 Color'        ;
  '󰈙 File'         ;
  '󰀾 Reference'    ;
  ' Folder'       ;
  '󰀬 EnumMember'   ;
  ' Constant'     ;
  ' Struct'       ;
  '󰀾 Event'        ;
  '󰆕 Operator'     ;
  ' TypeParameter';
}

local lsp_augroup_id = vim.api.nvim_create_augroup("LSP", { clear = true })

local lsp_key_mappings = {
  { "textDocument/definition"     , 'n', 'pd'       , '<cmd>lua PeekDefinition()<CR>'               },
  { "textDocument/definition"     , 'n', 'gd'       , '<cmd>lua vim.lsp.buf.definition()<CR>'       },
--{ "textDocument/declaration"    , 'n', 'gD'       , '<cmd>lua vim.lsp.buf.declaration()<CR>'      },
  { "textDocument/typeDefinition" , 'n', '<C-k>'    , '<cmd>lua vim.lsp.buf.type_definition()<CR>'  },
  { "textDocument/implementation" , 'n', 'gD'       , '<cmd>lua vim.lsp.buf.implementation()<CR>'   },
  { "textDocument/documentSymbol" , 'n', '1gd'      , '<cmd>lua vim.lsp.buf.document_symbol()<CR>'  },
  { "workspace/symbol"            , 'n', '1gD'      , '<cmd>lua vim.lsp.buf.workspace_symbol()<CR>' },
  { "textDocument/signatureHelp"  , 'n', '<Leader>S', '<cmd>lua vim.lsp.buf.signature_help()<CR>'   },
  { "textDocument/rename"         , 'n', 'gR'       , '<cmd>lua vim.lsp.buf.rename()<CR>'           },
  { "textDocument/inlayHint"      , 'n', 'gi'       , '<cmd>lua InlayHintToggle()<CR>'              },

  { "textDocument/rangeFormatting", 'x', 'gq', '<cmd>lua vim.lsp.buf.format({async=true})<CR>' },
  { "textDocument/formatting"     , 'n', 'gq', '<cmd>lua vim.lsp.buf.format({async=true})<CR>' },

  { "textDocument/codeAction", 'n', 'ga'       , '<cmd>lua vim.lsp.buf.code_action()<CR>' },
  { "textDocument/codeAction", 'v', 'ga'       , '<cmd>lua vim.lsp.buf.code_action()<CR>' },
  { "textDocument/codeAction", 'n', '<Leader>r', '<cmd>lua vim.lsp.buf.code_action{only = \'refactor\' }<CR>' },
  { "textDocument/codeAction", 'v', '<Leader>r', '<cmd>lua vim.lsp.buf.code_action{only = \'refactor\' }<CR>' },

  { "textDocument/codeLens", 'n', '<LocalLeader>l', '<cmd>lua vim.lsp.codelens.run()<CR>'   },
  { "textDocument/codeLens", 'n', '<LocalLeader>L', '<cmd>lua vim.lsp.codelens.clear()<CR>' },

  { "textDocument/references", 'n', 'gr', '<cmd>lua vim.lsp.buf.references({ includeDeclaration = false })<CR>'},
}

local CODE_ACTION_AVAILABLE = "CodeActionAvailable"
local CODE_ACTION_GROUP     = "code-action-group"

if vim.tbl_isempty(vim.fn.sign_getdefined(CODE_ACTION_AVAILABLE)) then
  vim.fn.sign_define(CODE_ACTION_AVAILABLE, { text = "💡", texthl = "DiagnosticSignInfo" })
end

local code_action_update_sign = function(old_line, new_line, bufnr)
  bufnr = bufnr or "%"

  if old_line then
    vim.fn.sign_unplace(
      CODE_ACTION_GROUP, { id = old_line, buffer = bufnr }
    )
    vim.b.code_action_line = nil
  end

  if new_line and (vim.b.code_action_line ~= new_line) then
    vim.fn.sign_place(
      new_line, CODE_ACTION_GROUP, CODE_ACTION_AVAILABLE, bufnr,
      { lnum = new_line, priority = 10 }
    )
    vim.b.code_action_line = new_line
  end
end

local cargo_reload_workspace = function()
  vim.lsp.buf_request(0, 'rust-analyzer/reloadWorkspace', nil, function(err)
    if err then
      vim.schedule(function()
        vim.notify('Cargo reload workspace gave error: ' .. tostring(err), vim.log.levels.ERROR)
      end)
    else
      vim.schedule(function()
        vim.notify 'Cargo workspace reloaded'
      end)
    end
  end)
end

local get_active_client_by_name = function(bufnr, servername)
  for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
    if client.name == servername then
      return client
    end
  end
end

local switch_source_header = function()
  local clangd_client = get_active_client_by_name(0, 'clangd')
  local params = { uri = vim.uri_from_bufnr(0) }

  if clangd_client then
    clangd_client.request('textDocument/switchSourceHeader', params, function(err, result)
      if err then
        vim.schedule(function()
          vim.notify('Clangd: switchSourceHeader gave error: ' .. tostring(err), vim.log.levels.ERROR)
        end)
      end

      if not result then
        vim.schedule(function()
          vim.notify('Clangd: switchSourceHeader, corresponding file not determined', vim.log.levels.ERROR)
        end)
        return
      end

      vim.cmd.edit(vim.uri_to_fname(result))
    end, 0)
  else
    vim.schedule(function()
      vim.notify('Clangd: switchSourceHeader is not supported', vim.log.levels.ERROR)
    end)
  end
end

local client_custom_setup = function(client)
  if client.name == 'clangd' then
    vim.api.nvim_create_user_command("ClangdSwitchSourceHeader", switch_source_header, {desc = 'Switch between source/header'})
  end

  if client.name == 'rust-analyzer' then
    vim.api.nvim_create_user_command("CargoReload", cargo_reload_workspace, {desc = 'Reload current cargo workspace'})
  end

  if client.name == 'tsserver' then
    -- Disable tsserver formatting, we want formatting via prettier
    client.server_capabilities.documentFormattingProvider      = false
    client.server_capabilities.documentRangeFormattingProvider = false
  end
end

local client_custom_cleanup = function(client)
  if client.name == 'clangd' then
    vim.api.nvim_del_user_command("ClangdSwitchSourceHeader")
  end

  if client.name == 'rust-analyzer' then
    vim.api.nvim_del_user_command("CargoReload")
  end
end

local on_attach = function(client, bufnr)
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  if client.config.flags then
    client.config.flags.allow_incremental_sync = true
    client.config.flags.debounce_text_changes  = 100
  end

  local opts = { noremap=true, silent=true }

  client_custom_setup(client)

  for _, mappings in pairs(lsp_key_mappings) do
    local capability, mode, lhs, rhs = unpack(mappings)
    if client.supports_method(capability) then
      vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
    end
  end

  if client.supports_method('textDocument/codeAction') then
    vim.api.nvim_create_autocmd({"CursorHold", "CursorHoldI"}, {
      group    = lsp_augroup_id,
      buffer   = bufnr,
      callback = function()
        -- Taken from https://github.com/neovim/nvim-lspconfig/wiki/Code-Actions
        local context  = { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
        local params   = lsp_util.make_range_params()
        params.context = context
        vim.lsp.buf_request(0, 'textDocument/codeAction', params, function(err, result, ctx, config)
          if result and not vim.tbl_isempty(result) then
            local line = params.range.start.line
            code_action_update_sign(vim.b.code_action_line, line + 1, bufnr)
          else
            code_action_update_sign(vim.b.code_action_line, nil, bufnr)
          end
        end)
      end
    })
  end

  if client.supports_method('textDocument/codeLens') then
    vim.api.nvim_create_autocmd({"CursorHold", "CursorHoldI", "InsertLeave"}, {
      group    = lsp_augroup_id,
      buffer   = bufnr,
      callback = vim.lsp.codelens.refresh,
    })
  end

  if client.supports_method('textDocument/documentHighlight') then
    vim.api.nvim_create_autocmd("CursorHold", {
      group    = lsp_augroup_id,
      buffer   = bufnr,
      callback = vim.lsp.buf.document_highlight,
    })
    vim.api.nvim_create_autocmd("CursorMoved", {
      group    = lsp_augroup_id,
      buffer   = bufnr,
      callback = vim.lsp.buf.clear_references,
    })
  end

  if client.supports_method('textDocument/inlayHint') then
    vim.lsp.inlay_hint.enable(bufnr, false)
  end
end

vim.api.nvim_create_autocmd("LspAttach", {
  group = lsp_augroup_id,
  callback = function(args)
    local bufnr = args.buf
    local client = vim.lsp.get_client_by_id(args.data.client_id)
    local opts = { noremap=true, silent=true }

    for _, mappings in pairs(lsp_key_mappings) do
      local capability, mode, lhs, rhs = unpack(mappings)
      if client.supports_method(capability) then
        vim.api.nvim_buf_set_keymap(bufnr, mode, lhs, rhs, opts)
      end
    end

    on_attach(client, bufnr)
  end,
})

vim.api.nvim_create_autocmd("LspDetach", {
  group = lsp_augroup_id,
  callback = function(args)
    local bufnr = args.buf
    local client = vim.lsp.get_client_by_id(args.data.client_id)

    for _, mappings in pairs(lsp_key_mappings) do
      local capability, mode, lhs, _ = unpack(mappings)
      if client.supports_method(capability) then
        vim.api.nvim_buf_del_keymap(bufnr, mode, lhs)
      end
    end

    client_custom_cleanup(client)
  end,
})

vim.api.nvim_create_autocmd("LspProgress", {
  group   = lsp_augroup_id,
  command = "redrawstatus"
})
