local servers_path = vim.fn.stdpath('data') .. '/lsp'
local util = require('user.modules.lsp.util')

local function setup_border_style()
    local border_chars = require('user.util').border_styles[vim.g.border_style]
    local border = {}
    for _, char in ipairs(border_chars) do
        table.insert(border, { char, 'FloatBorder' })
    end
    local orig_util_open_floating_preview = vim.lsp.util.open_floating_preview
    function vim.lsp.util.open_floating_preview(contents, syntax, opts, ...)
        opts = opts or {}
        opts.border = opts.border or border
        return orig_util_open_floating_preview(contents, syntax, opts, ...)
    end
end

local function setup_handlers()
    -- Use nvim-notify for notification
    vim.lsp.handlers['window/showMessage'] = function(_, result, ctx)
        local notify = require('notify')
        local client = vim.lsp.get_client_by_id(ctx.client_id)
        local lvl = ({ 'ERROR', 'WARN', 'INFO', 'DEBUG' })[result.type]

        notify({ result.message }, lvl, {
            title = 'LSP | ' .. client.name,
            timeout = 10000,
            keep = function()
                return lvl == 'ERROR' or lvl == 'WARN'
            end,
        })
    end
end

local function setup_diagnostic_style()
    -- Configure signs and virtual text
    vim.diagnostic.config {
        virtual_text = { prefix = '■', spacing = 2 },
        signs = false, -- Redefined below
        underline = true,
        update_in_insert = false,
        severity_sort = true,
    }

    -- Show only 1 sign in signcolumn
    local ns = vim.api.nvim_create_namespace('sign_namespace')
    local orig_show = vim.diagnostic.show
    local function set_signs(bufnr)
        -- Get all diagnostics from the current buffer
        local diagnostics = vim.diagnostic.get(bufnr)
        -- Find the 'worst' diagnostic per line
        local max_severity_per_line = {}
        for _, d in pairs(diagnostics) do
            local m = max_severity_per_line[d.lnum]
            if not m or d.severity < m.severity then
                max_severity_per_line[d.lnum] = d
            end
        end
        -- Show the filtered diagnostics using the custom namespace. Use the
        -- reference to the original function to avoid a loop.
        local filtered_diagnostics = vim.tbl_values(max_severity_per_line)
        orig_show(ns, bufnr, filtered_diagnostics, {
            signs = true,
            virtual_text = false,
            underline = false,
        })
    end

    function vim.diagnostic.show(namespace, bufnr, ...)
        orig_show(namespace, bufnr, ...)
        set_signs(bufnr)
    end

    -- Custom signs
    local signs = { Error = '', Warn = '', Hint = '', Info = '' }
    for type, icon in pairs(signs) do
        local hl = 'DiagnosticSign' .. type
        vim.fn.sign_define(hl, { text = icon, texthl = hl })
    end
end

local function read_dict(file)
    if not file then
        return nil
    end
    local dict = {}
    for line in io.lines(file) do
        table.insert(dict, line)
    end
    return dict
end

local common_config = {
    on_attach = util.common_on_attach,
    capabilities = util.common_capabilities(),
}

local lsp_servers = {
    clangd = {
        filetypes = { 'c', 'cpp' },
        init_options = {
            usePlaceholders = true,
            completeUnimported = true,
            clangdFileStatus = true,
        },
        cmd = {
            'clangd',
            '-j=2',
            '--background-index',
            '--clang-tidy',
            '--completion-style=detailed',
            '--pch-storage=memory',
            '--header-insertion=iwyu',
            '--header-insertion-decorators',
        },
        commands = {
            ClangdSwitchSourceHeader = {
                function()
                    util.switch_source_header_splitcmd(0, 'edit')
                end,
                description = 'Open source/header in current buffer',
            },
            ClangdSwitchSourceHeaderVSplit = {
                function()
                    util.switch_source_header_splitcmd(0, 'vsplit')
                end,
                description = 'Open source/header in a new vsplit',
            },
            ClangdSwitchSourceHeaderSplit = {
                function()
                    util.switch_source_header_splitcmd(0, 'split')
                end,
                description = 'Open source/header in a new split',
            },
        },
    },
    sumneko_lua = {
        cmd = {
            servers_path .. '/sumneko_lua/bin/lua-language-server',
            '-E',
            '-e',
            'LANG=en',
            servers_path .. '/sumneko_lua/main.lua',
        },
        settings = {
            Lua = {
                diagnostics = {
                    globals = { 'vim' },
                    neededFileStatus = { ['codestyle-check'] = 'Any' },
                },
                runtime = { version = 'LuaJIT', path = util.lua_runtime_path() },
                workspace = {
                    library = util.lua_lib_path(),
                    maxPreload = 1000,
                    preloadFileSize = 150,
                },
                completion = { callSnippet = 'Replace' },
                hint = { enable = true },
                telemetry = { enable = false },
            },
        },
    },
    pylsp = {
        cmd = { servers_path .. '/pylsp/venv/bin/pylsp' },
        settings = {
            pylsp = {
                plugins = {
                    autopep8 = { enabled = false },
                    pydocstyle = { enabled = true },
                    -- pylint = {enabled = true}
                },
            },
        },
    },
    gopls = {
        cmd = { servers_path .. '/gopls/gopls', '-remote=auto' },
        init_options = {
            usePlaceholders = true,
            completeUnimported = true,
        },
    },
    -- yamlls = {
    --     cmd = { servers_path .. '/yamlls/node_modules/.bin/yaml-language-server', '--stdio' },
    --     settings = {
    --         redhat = { telemetry = { enabled = false } },
    --         yaml = {
    --             format = {
    --                 bracketSpacing = true,
    --                 printWidth = 80,
    --                 -- proseWrap = 'always', -- default is 'preserve'
    --                 -- singleQuote = true,
    --             },
    --             schemas = require('schemastore').json.schemas()
    --         }
    --     }
    -- },
    ansiblels = {
        cmd = { servers_path .. '/ansiblels/node_modules/.bin/ansible-language-server', '--stdio' },
        filetypes = { 'yaml' },
        single_file_support = false,
        settings = {
            ansible = {
                python = { interpreterPath = 'python3' },
                executionEnvironment = { enabled = false },
            },
        },
    },
    -- tailwindcss = {
    --     cmd = { servers_path .. '/tailwindcss/node_modules/.bin/tailwindcss-language-server', '--stdio' },
    --     settings = { tailwindCSS = { emmetCompletions = true } }
    -- },
    emmet_ls = {
        cmd = { servers_path .. '/emmet_ls/node_modules/.bin/emmet-ls', '--stdio' },
        filetypes = {
            'html',
            'xml',
            'css',
            'scss',
            'sass',
            'javascript',
            'javascriptreact',
        },
    },
    -- eslint = {
    --     cmd = { servers_path .. '/vscode/node_modules/.bin/vscode-eslint-language-server', '--stdio' },
    --     filetypes = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue', 'svelte' },
    --     handlers = {
    --         ['eslint/noConfig'] = function()
    --             vim.notify('Unable to parse ESLint config.', vim.log.levels.WARN)
    --         end
    --     },
    --     settings = {
    --         codeAction = {
    --             disableRuleComment = {
    --                 enable = true,
    --                 location = 'separateLine'
    --             },
    --             showDocumentation = { enable = true }
    --         },
    --         codeActionOnSave = {
    --             enable = false, -- run ':EslintFixAll' manually
    --             mode = 'all'
    --         },
    --         format = true,
    --         nodePath = '',
    --         onIgnoredFiles = 'off',
    --         packageManager = 'npm',
    --         quiet = false,
    --         rulesCustomizations = {},
    --         run = 'onType',
    --         useESLintClass = false,
    --         validate = 'on',
    --         workingDirectory = { mode = 'auto' },
    --     },
    -- },
    jsonls = {
        cmd = { servers_path .. '/vscode/node_modules/.bin/vscode-json-language-server', '--stdio' },
        -- Schema catalog: https://www.schemastore.org/api/json/catalog.json
        settings = {
            json = { schemas = require('schemastore').json.schemas() },
        },
    },
    rust_analyzer = {
        settings = {
            ['rust-analyzer'] = {
                checkOnSave = {
                    enable = true,
                    command = 'clippy',
                },
                callInfo = { full = true },
                lens = {
                    enable = true,
                    references = true,
                    implementations = true,
                    enumVariantReferences = true,
                    methodReferences = true,
                },
                inlayHints = {
                    enable = true,
                    typeHints = true,
                    parameterHints = true,
                },
                hoverActions = { enable = true },
                cargo = { autoreload = true },
            },
        },
    },
    texlab = {
        settings = {
            texlab = {
                auxDirectory = '.',
                bibtexFormatter = 'texlab',
                build = {
                    args = { '-pdf', '-interaction=nonstopmode', '-synctex=1', '%f' },
                    executable = 'latexmk',
                    forwardSearchAfter = false,
                    onSave = true,
                },
                chktex = {
                    onEdit = false,
                    onOpenAndSave = false,
                },
                forwardSearch = {
                    executable = 'zathura',
                    args = { '--synctex-forward', '%l:1:%f', '%p' },
                },
                diagnosticsDelay = 300,
                formatterLineLength = 80,
                latexFormatter = 'latexindent',
                latexindent = { modifyLineBreaks = false },
            },
        },
    },
    ltex = {
        cmd = { vim.fn.stdpath('config') .. '/scripts/ltex-ls' },
        settings = {
            ltex = {
                -- FIX: ltex.dictionary doesn't work with external files
                -- ('ltex/workspaceSpecificConfiguration' is not implemented)
                dictionary = {
                    ['en-US'] = read_dict(vim.fn.stdpath('config') .. '/spell/en.utf-8.add'),
                },
                completionEnabled = true,
            },
        },
    },
    sqls = {
        cmd = { servers_path .. '/sqls/sqls' },
        on_attach = function(client, bufnr)
            util.common_on_attach(client, bufnr)
            require('sqls').on_attach(client, bufnr)
        end,
    },
    cmake = { cmd = { servers_path .. '/cmake/venv/bin/cmake-language-server' } },
    bashls = { cmd = { servers_path .. '/bashls/node_modules/.bin/bash-language-server', 'start' } },
    dockerls = { cmd = { servers_path .. '/dockerls/node_modules/.bin/docker-langserver', '--stdio' } },
    html = { cmd = { servers_path .. '/vscode/node_modules/.bin/vscode-html-language-server', '--stdio' } },
    cssls = { cmd = { servers_path .. '/vscode/node_modules/.bin/vscode-css-language-server', '--stdio' } },
    -- vimls = { cmd = { servers_path .. '/vimls/node_modules/.bin/vim-language-server', '--stdio' } },
    tsserver = { cmd = { servers_path .. '/tsserver/node_modules/.bin/typescript-language-server', '--stdio' } },
}

local function lsp_init()
    -- vim.lsp.set_log_level('warn')
    setup_handlers()
    setup_border_style()
    setup_diagnostic_style()

    local lspconf = require('lspconfig')
    for server, config in pairs(lsp_servers) do
        local merged_config = vim.tbl_extend('force', common_config, config)

        -- Servers that don't need on_attach
        if server == 'emmet_ls' then
            merged_config.on_attach = nil
        end

        lspconf[server].setup(merged_config)
    end
end

lsp_init()
