local nvim_lsp   = require 'lspconfig'
local protocol   = require 'vim.lsp.protocol'
local ts_utils   = require 'nvim-lsp-ts-utils'
local null_ls    = require 'null-ls'
local util       = require 'lspconfig/util'
local cmp        = require 'cmp_nvim_lsp'

-- https://github.com/neovim/nvim-lspconfig/wiki/User-contributed-tips
function FormatRangeOperator()
  local old_func = vim.go.operatorfunc
  _G.op_func_formatting = function()
    local start = vim.api.nvim_buf_get_mark(0, '[')
    local finish = vim.api.nvim_buf_get_mark(0, ']')
    vim.lsp.buf.range_formatting({}, start, finish)
    vim.go.operatorfunc = old_func
    _G.op_func_formatting = nil
  end
  vim.go.operatorfunc = 'v:lua.op_func_formatting'
  vim.api.nvim_feedkeys('g@', 'n', false)
end

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

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

-- This should be called only once, so cannot be in on_attach as earlier.
-- See https://github.com/jose-elias-alvarez/null-ls.nvim/issues/38
-- Required by nvim-lsp-ts-utils to provide ESLint code actions, diagnostics
-- and formatting.
local null_ls_sources = {
    null_ls.builtins.diagnostics.shellcheck,
}
null_ls.config({ sources = null_ls_sources })
nvim_lsp["null-ls"].setup {}

local ts_utils_setup = function(client, bufnr, opts)
  if client.name == 'tsserver' then
    -- Disable tsserver formatting as we plan on formatting via null-ls
    client.resolved_capabilities.document_formatting       = false
    client.resolved_capabilities.document_range_formatting = false

    ts_utils.setup {
      debug                          = false,
      disable_commands               = false,
      enable_import_on_completion    = false,

      import_all_timeout             = 5000,
      import_all_scan_buffers        = 100,
      import_all_select_source       = false,
      import_all_priorities = {
        buffers        = 4, -- loaded buffer names
        buffer_content = 3, -- loaded buffer content
        local_files    = 2, -- git files or files with relative path markers
        same_file      = 1, -- add to existing import statement
      },

      eslint_enable_code_actions     = true,
      eslint_bin                     = "eslint",
      eslint_enable_disable_comments = true,
      eslint_enable_diagnostics      = true,
      eslint_config_fallback         = nil,
      eslint_show_rule_id            = false,

      enable_formatting              = true,
      formatter                      = "prettier",
      formatter_config_fallback      = nil,

      update_imports_on_move         = false,
      require_confirmation_on_move   = false,
      watch_dir                      = nil,

      filter_out_diagnostics_by_severity = {},
      filter_out_diagnostics_by_code     = {},

      auto_inlay_hints      = true,
      inlay_hints_highlight = "Hint",
    }

    ts_utils.setup_client(client)

    vim.api.nvim_buf_set_keymap(bufnr, 'n', 'go', ':TSLspOrganize<CR>'  , opts)
    vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gf', ':TSLspFixCurrent<CR>', opts)
    vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gI', ':TSLspImportAll<CR>' , opts)
  end
end

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

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

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

  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd'       , '<cmd>lua vim.lsp.buf.definition()<CR>'       , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD'       , '<cmd>lua vim.lsp.buf.declaration()<CR>'      , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr'       , '<cmd>lua vim.lsp.buf.references()<CR>'       , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi'       , '<cmd>lua vim.lsp.buf.implementation()<CR>'   , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>'    , '<cmd>lua vim.lsp.buf.type_definition()<CR>'  , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '1gd'      , '<cmd>lua vim.lsp.buf.document_symbol()<CR>'  , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '1gD'      , '<cmd>lua vim.lsp.buf.workspace_symbol()<CR>' , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gR'       , '<cmd>lua vim.lsp.buf.rename()<CR>'           , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'pd'       , '<cmd>lua PeekDefinition()<CR>'               , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<Leader>k', '<cmd>lua vim.lsp.buf.hover()<CR>'            , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<Leader>S', '<cmd>lua vim.lsp.buf.signature_help()<CR>'   , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'ga'       , '<cmd>lua vim.lsp.buf.code_action()<CR>'      , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'v', 'ga'       , '<cmd>lua vim.lsp.buf.range_code_action()<CR>', opts)

  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<Leader>r', '<cmd>lua vim.lsp.buf.code_action{ only = \'refactor\' }<CR>'      , opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'v', '<Leader>r', '<cmd>lua vim.lsp.buf.range_code_action{ only = \'refactor\' }<CR>', opts)

  if client.resolved_capabilities.document_range_formatting then
    vim.api.nvim_buf_set_keymap(bufnr, 'x', 'gq', '<cmd>lua FormatRangeOperator()<CR>', opts)
  end
  if client.resolved_capabilities.document_formatting then
    vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gq', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)
  end

  if client.resolved_capabilities.code_lens then
    vim.api.nvim_buf_set_keymap(bufnr, "n", "<Leader>l", "<cmd>lua vim.lsp.codelens.run()<CR>", opts)
    vim.api.nvim_command [[autocmd CursorHold,CursorHoldI,InsertLeave <buffer> lua vim.lsp.codelens.refresh()]]
  end

  vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]]
  vim.api.nvim_command [[autocmd CursorMoved,InsertEnter <buffer> lua vim.lsp.buf.clear_references()]]

  protocol.CompletionItemKind = {
    ' '; -- text
    ' '; -- method
    ' '; -- function
    '全'; -- ctor
    ' '; -- field
    ' '; -- variable
    ' '; -- class
    ' '; -- interface
    ' '; -- module
    ' '; -- property
    ' '; -- unit
    ' '; -- value
    '螺'; -- enum
    ' '; -- keyword
    ' '; -- snippet
    ' '; -- color
    ' '; -- file
    ' '; -- reference
    ' '; -- folder
    ' '; -- enum member
    ' '; -- constant
    ' '; -- struct
    ' '; -- event
    '璉'; -- operator
    ' '; -- type parameter
  }

  ts_utils_setup(client, bufnr, opts)

  vim.cmd [[autocmd CursorHold,CursorHoldI <buffer> lua require'nvim-lightbulb'.update_lightbulb()]]
end

-- See https://github.com/sumneko/lua-language-server/wiki/Setting-without-VSCode#neovim-with-built-in-lsp-client
-- on why we do this library thing
-- Idea taken from https://gist.github.com/folke/fe5d28423ea5380929c3f7ce674c41d8
local determine_setup_lua = function ()
  local library = {}
  local cwd = vim.fn.getcwd()

  local add_to_library = function(lib_path)
      for _, p in pairs(vim.fn.expand(lib_path, false, true)) do
      p = vim.loop.fs_realpath(p)
      library[p] = true
    end
  end

  if string.find(cwd, "neovim") then
    add_to_library("$VIMRUNTIME")
  elseif string.find(cwd, "dotfiles") then
    add_to_library("$VIMRUNTIME")
    add_to_library("~/.config/nvim")
    add_to_library("~/.local/share/nvim/site/pack/packer/opt/*")
    add_to_library("~/.local/share/nvim/site/pack/packer/start/*")
  else
    add_to_library(cwd .. "/*")
  end

  local runtime_path = vim.split(package.path, ';')
  table.insert(runtime_path, "lua/?.lua")
  table.insert(runtime_path, "lua/?/init.lua")

  local lua_version = function()
    if string.find(cwd, "neovim") or string.find(cwd, "dotfiles") then
      return "LuaJIT"
    else
      return "Lua 5.4"
    end
  end

  return library, runtime_path, lua_version()
end

local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities = cmp.update_capabilities(capabilities)

local rust_tool_opts = {
  tools = {
    autoSetHints       = true,
    hover_with_actions = false,
    runnables          = { use_telescope = false },
    debuggables        = { use_telescope = false },
    inlay_hints = {
      only_current_line         = false,
      only_current_line_autocmd = "CursorHold",
      show_parameter_hints      = true,
      parameter_hints_prefix    = "<- ",
      other_hints_prefix        = "=> ",
      highlight                 = "Hint",
    },
    hover_actions = {
      auto_focus = true
    },
    crate_graph = {
      backend = "svg",
      output  = "crates_graph.svg",
      full    = false,
    }
  },
  server = {
    on_attach    = on_attach,
    capabilities = capabilities,
    settings = {
      ["rust-analyzer"] = {
        checkOnSave = {
          command = "clippy"
        },
      }
    }
  },
}

local servers = { 'hls', 'pylsp' }
for _, lsp in ipairs(servers) do
  nvim_lsp[lsp].setup {
    on_attach    = on_attach,
    capabilities = capabilities
  }
end

nvim_lsp.clangd.setup {
  on_attach    = on_attach,
  capabilities = capabilities,
  default_config = {
    cmd       = { "clangd", "--background-index", "--pch-storage=memory", "--clang-tidy", "--suggest-missing-includes", "--header-insertion=never" },
    filetypes = { 'c', 'cpp' },
    root_dir = function(fname)
      -- We specify build/compile_commands.json as that is where the compile_commands.json
      -- gets generated automatically for meson projects.
      local root_pattern = util.root_pattern('build/compile_commands.json', 'compile_commands.json', 'compile_flags.txt', '.git')
      local filename     = util.path.is_absolute(fname) and fname or util.path.join(vim.loop.cwd(), fname)
      return root_pattern(filename) or util.path.dirname(filename)
    end,
  }
}

require('rust-tools').setup(rust_tool_opts)

local library, runtime_path, lua_version = determine_setup_lua()
nvim_lsp.sumneko_lua.setup {
  cmd          = { "/usr/bin/lua-language-server" };
  on_attach    = on_attach,
  capabilities = capabilities,
  settings = {
    Lua = {
      runtime = {
        version = lua_version,
        path    = runtime_path,
      },
      completion = { callSnippet = "Both" },
      diagnostics = {
        globals = {'vim'},
      },
      workspace = {
        library         = library,
        maxPreload      = 2000,
        preloadFileSize = 50000
      },
      telemetry = {
        enable = false,
      },
    },
  },
}

nvim_lsp.tsserver.setup {
  init_options = require("nvim-lsp-ts-utils").init_options,
  on_attach    = on_attach,
}
