local M = {}

-- All configuration is taken from nvim-lspconfig except for may be minor changes
--
-- Deno specific functions
local buf_cache = function(bufnr, client)
  local params = {}
  params['referrer'] = { uri = vim.uri_from_bufnr(bufnr) }
  params['uris'] = {}
  client.request_sync('deno/cache', params)
end

local virtual_text_document_handler = function(uri, res, client)
  if not res then
    return nil
  end

  local lines = vim.split(res.result, '\n')
  local bufnr = vim.uri_to_bufnr(uri)

  local current_buf = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  if #current_buf ~= 0 then
    return nil
  end

  vim.api.nvim_buf_set_lines(bufnr, 0, -1, nil, lines)

  vim.api.nvim_buf_set_option(bufnr, 'readonly'  , true)
  vim.api.nvim_buf_set_option(bufnr, 'modified'  , false)
  vim.api.nvim_buf_set_option(bufnr, 'modifiable', false)

  vim.lsp.buf_attach_client(bufnr, client.id)
end

local virtual_text_document = function(uri, client)
  local params = {
    textDocument = {
      uri = uri,
    },
  }
  local result = client.request_sync('deno/virtualTextDocument', params)

  virtual_text_document_handler(uri, result, client)
end

local denols_handler = function(err, result, ctx)
  if not result or vim.tbl_isempty(result) then
    return nil
  end

  local client = vim.lsp.get_client_by_id(ctx.client_id)

  for _, res in pairs(result) do
    local uri = res.uri or res.targetUri

    if uri:match '^deno:' then
      virtual_text_document(uri, client)
      res['uri'] = uri
      res['targetUri'] = uri
    end
  end

  vim.lsp.handlers[ctx.method](err, result, ctx)
end
-- End of deno specific functions

local get_root_directory = function(root_files)
  local path = vim.fs.find(root_files, { type = "file" })

  return vim.fs.dirname(path[1])
end

local default_capabilities = vim.lsp.protocol.make_client_capabilities()

function M.bashls_config()
  local root_directory = vim.fn.getcwd()

  return {
    name                = "bashls",
    cmd                 = { 'bash-language-server', 'start' },
    capabilities        = default_capabilities,
    root_dir            = root_directory,
    filetypes           = { 'sh' },
    single_file_support = true,
    cmd_env = {
      -- Prevent recursive scanning which will cause issues when opening a file
      -- directly in the home directory (e.g. ~/foo.sh).
      --
      -- Default upstream pattern is "**/*@(.sh|.inc|.bash|.command)".
      GLOB_PATTERN = vim.env.GLOB_PATTERN or '*@(.sh|.inc|.bash|.command)',
    },
  }
end

function M.clangd_config()
  local root_files     = { '.clangd', '.clang-tidy', '.clang-format', 'compile_commands.json', 'compile_flags.txt' }
  local clangd_cmd     = { "clangd", "--background-index", "--pch-storage=memory", "--clang-tidy", "--header-insertion=never" }
  local root_directory = get_root_directory(root_files)
  local clangd_caps    = {
    textDocument   = { completion = { editsNearCursor = true } },
    offsetEncoding = { 'utf-8', 'utf-16' },
  }

  return {
    name                = "clangd",
    cmd                 = clangd_cmd,
    capabilities        = clangd_caps,
    root_dir            = root_directory,
    filetypes           = { 'c', 'cpp' },
    single_file_support = true,
  }
end


function M.deno_config()
  local root_files     = { 'deno.json', 'deno.jsonc' }
  local file_types     = { 'javascript', 'javascriptreact', 'javascript.jsx', 'typescript', 'typescriptreact', 'typescript.tsx' }
  local root_directory = get_root_directory(root_files)

  return {
    cmd          = { 'deno', 'lsp' },
    capabilities = default_capabilities,
    filetypes    = file_types,
    root_dir     = root_directory,
    settings = {
      deno = {
        enable  = true,
        suggest = {
          imports = {
            hosts = {
              ['https://deno.land']   = true,
              ['https://crux.land']   = true,
              ['https://x.nest.land'] = true,
            },
          },
        },
      },
    },
    handlers = {
      ['textDocument/definition']     = denols_handler,
      ['textDocument/typeDefinition'] = denols_handler,
      ['textDocument/references']     = denols_handler,
      ['workspace/executeCommand']    = function(err, result, context)
        if context.params.command == 'deno.cache' then
          buf_cache(context.bufnr, vim.lsp.get_client_by_id(context.client_id))
        else
          vim.lsp.handlers[context.method](err, result, context)
        end
      end,
    },
    commands = {
      DenolsCache = {
        function()
          local clients = vim.lsp.get_active_clients()
          for _, client in ipairs(clients) do
            if client.name == 'denols' then
              buf_cache(0, client)
              break
            end
          end
        end,
        description = 'Cache a module and all of its dependencies.',
      },
    }
  }
end

function M.gopls_config()
  local root_files     = { 'go.mod', 'go.work', '.git' }
  local root_directory = get_root_directory(root_files)

  return {
    name         = "gopls",
    cmd          = { "gopls" },
    capabilities = default_capabilities,
    root_dir     = root_directory,
    filetypes    = { 'go', 'gomod', 'gowork', 'gotmpl' },
  }
end

function M.hls_config()
  local root_files     = { 'hie.yaml', 'stack.yaml', 'cabal.project', '*.cabal', 'package.yaml' }
  local root_directory = get_root_directory(root_files)

  return {
    name                = "hls",
    cmd                 = { 'haskell-language-server-wrapper', '--lsp' },
    capabilities        = default_capabilities,
    root_dir            = root_directory,
    filetypes           = { 'cabal', 'haskell' },
    single_file_support = true,
    settings            = {
      haskell = {
        checkParents            = 'CheckOnSave',
        checkProject            = false,
        cabalFormattingProvider = 'cabalfmt',
        formattingProvider      = 'ormolu',
        maxCompletions          = 20,
        plugin                  = { stan = { globalOn = false } }
      }
    },
  }
end

function M.leanls_config()
  local root_files     = { 'lakefile.lean', 'lean-toolchain', 'leanpkg.toml' }
  local root_directory = get_root_directory(root_files)

  return {
    name                = "leanls",
    cmd                 = { 'lake', 'serve', '--' },
    capabilities        = default_capabilities,
    root_dir            = root_directory,
    filetypes           = { 'lean' },
    single_file_support = true,
  }
end

function M.pursls_config()
  local root_files     = { 'bower.json', 'psc-package.json', 'spago.dhall' }
  local root_directory = get_root_directory(root_files)

  return {
    name                = "purescriptls",
    cmd                 = { 'purescript-language-server', '--stdio' },
    capabilities        = default_capabilities,
    root_dir            = root_directory,
    filetypes           = { 'purescript' },
    single_file_support = false,
    settings            = {
      purescript = {
        formatter = 'purs-tidy',
      }
    },
  }
end

function M.ra_config()
  local root_files     = { "Cargo.toml" }
  local root_directory = get_root_directory(root_files)
  local ra_caps        = vim.lsp.protocol.make_client_capabilities()

  ra_caps.experimental = {
    serverStatusNotification = true,
  }

  return {
    name         = "rust-analyzer",
    cmd          = { "rust-analyzer" },
    capabilities = ra_caps,
    root_dir     = root_directory,
    settings = {
      ["rust-analyzer"] = {
        procMacro = {
          enable = true
        },
        checkOnSave = {
          command = "clippy"
        },
      },
    },
  }
end

function M.ruff_config()
  local root_files     = { 'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile' }
  local root_directory = get_root_directory(root_files)

  return {
    name                = "ruff",
    cmd                 = { "ruff-lsp" },
    filetypes           = { 'python' },
    capabilities        = default_capabilities,
    root_dir            = root_directory,
    single_file_support = true,
  }
end

function M.scheme_config()
  local root_files     = { '.git' }
  local root_directory = get_root_directory(root_files)

  return {
    name                = "racket-langserver",
    cmd                 = { 'racket', '--lib', 'racket-langserver' },
    capabilities        = default_capabilities,
    filetypes           = { 'scheme', 'racket' },
    root_dir            = root_directory,
    single_file_support = true,
  }
end

function M.tsserver_config()
  local root_files     = { 'jsconfig.json', 'tsconfig.json', 'package.json' }
  local file_types     = { 'javascript', 'javascriptreact', 'javascript.jsx', 'typescript', 'typescriptreact', 'typescript.tsx' }
  local root_directory = get_root_directory(root_files)

  return {
    name         = "tsserver",
    cmd          = { "typescript-language-server", "--stdio" },
    -- Support Yarn PnP
    -- hostInfo with init_options is required to allow yarn pnp sdk to
    -- identify editor and patch paths appropriately.
    init_options = { hostInfo = 'neovim' },
    capabilities = default_capabilities,
    root_dir     = root_directory,
    filetypes    = file_types,
    settings  = {
      typescript = {
        inlayHints = {
          includeInlayParameterNameHints                        = 'all',
          includeInlayParameterNameHintsWhenArgumentMatchesName = false,
          includeInlayFunctionParameterTypeHints                = true,
          includeInlayVariableTypeHints                         = true,
          includeInlayPropertyDeclarationTypeHints              = true,
          includeInlayFunctionLikeReturnTypeHints               = true,
          includeInlayEnumMemberValueHints                      = true,
        }
      },
      javascript = {
        inlayHints = {
          includeInlayParameterNameHints                        = 'all',
          includeInlayParameterNameHintsWhenArgumentMatchesName = false,
          includeInlayFunctionParameterTypeHints                = true,
          includeInlayVariableTypeHints                         = true,
          includeInlayPropertyDeclarationTypeHints              = true,
          includeInlayFunctionLikeReturnTypeHints               = true,
          includeInlayEnumMemberValueHints                      = true,
        }
      }
    }
  }
end

function M.typescript_config()
  local root_files = { 'jsconfig.json', 'tsconfig.json', 'package.json' }
  local root_dir   = get_root_directory(root_files)

  -- Look for npm based typescript files first and if found return tsserver
  -- configuration, else return deno configuration. This allows us to work
  -- with both npm and deno based typescript projects.
  if root_dir then
    return M.tsserver_config()
  else
    return M.deno_config()
  end
end

return M
