local M = {}

function M.lsp_conf()
    local lspconf = require('lspconfig')

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

        local function buf_set_keymap(...)
            vim.api.nvim_buf_set_keymap(bufnr, ...)
        end

        local opts = {noremap = true, silent = true}
        buf_set_keymap('n', 'gD', ':lua vim.lsp.buf.declaration()<CR>', opts)
        buf_set_keymap('n', 'gd', ':lua vim.lsp.buf.definition()<CR>', opts)
        buf_set_keymap('n', 'K', ':lua vim.lsp.buf.hover()<CR>', opts)
        buf_set_keymap('n', 'gi', ':lua vim.lsp.buf.implementation()<CR>', opts)
        buf_set_keymap('n', '<leader>lx', ':lua vim.lsp.buf.signature_help()<CR>', opts)
        buf_set_keymap('n', '<leader>la', ':lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
        buf_set_keymap('n', '<leader>lr', ':lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
        buf_set_keymap('n', '<leader>lw', ':lua vim.notify(vim.inspect(vim.lsp.buf.list_workspace_folders()), vim.log.levels.INFO)<CR>', opts)
        buf_set_keymap('n', '<leader>ld', ':lua vim.lsp.buf.type_definition()<CR>', opts)
        buf_set_keymap('n', '<leader>ln', ':lua vim.lsp.buf.rename()<CR>', opts)
        buf_set_keymap('n', 'gr', ':lua vim.lsp.buf.references()<CR>', opts)
        buf_set_keymap('n', '<leader>le', ':lua vim.lsp.diagnostic.show_line_diagnostics()<CR>', opts)
        buf_set_keymap('n', '[d', ':lua vim.lsp.diagnostic.goto_prev()<CR>', opts)
        buf_set_keymap('n', ']d', ':lua vim.lsp.diagnostic.goto_next()<CR>', opts)
        buf_set_keymap('n', '<leader>ll', ':lua vim.lsp.diagnostic.set_loclist()<CR>', opts)

        if client.resolved_capabilities.document_formatting then
            buf_set_keymap('n', '<leader>lo', ':lua vim.lsp.buf.formatting()<CR>', opts)
            -- vim.api.nvim_command('autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()')
        elseif client.resolved_capabilities.document_range_formatting then
            buf_set_keymap('n', '<leader>lo', ':lua vim.lsp.buf.range_formatting({},{0,0},{vim.fn.line("$"),0})<CR>', opts)
            buf_set_keymap('v', '<leader>lo', ':lua vim.lsp.buf.range_formatting()<CR>', opts)
            -- vim.api.nvim_command('autocmd BufWritePre <buffer> lua vim.lsp.buf.range_formatting({},{0,0},{vim.fn.line("$"),0})')
        end

        -- Attach lsp_signature.nvim
        require('lsp_signature').on_attach({
            bind = true, -- This is mandatory, otherwise border config doesn't work
            floating_window = true,
            fix_pos = true,
            hint_prefix = ' ',
            transpancy = 5,
            handler_opts = {border = 'none'},
            zindex = 50, -- set to 200 to make the float window on top of others
            toggle_key = '<M-x>'
        }, bufnr)
    end

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

    -- Add additional capabilities supported by nvim-cmp
    local completionItem = capabilities.textDocument.completion.completionItem
    completionItem.documentationFormat = {'markdown', 'plaintext'}
    completionItem.snippetSupport = true
    completionItem.preselectSupport = true
    completionItem.insertReplaceSupport = true
    completionItem.labelDetailsSupport = true
    completionItem.deprecatedSupport = true
    completionItem.commitCharactersSupport = true
    completionItem.tagSupport = {valueSet = {1}}
    completionItem.resolveSupport = {
        properties = {
            'documentation',
            'detail',
            'additionalTextEdits',
        }
    }

    -- Replace the default lsp diagnostic letters with prettier symbols
    vim.fn.sign_define('LspDiagnosticsSignError', {text = ''})
    vim.fn.sign_define('LspDiagnosticsSignWarning', {text = ''})
    vim.fn.sign_define('LspDiagnosticsSignInformation', {text = ''})
    vim.fn.sign_define('LspDiagnosticsSignHint', {text = ''})

    ---------------------------
    -- Server configurations --
    ---------------------------
    -- https://github.com/neovim/nvim-lspconfig/blob/master/CONFIG.md
    local servers_path = vim.fn.stdpath('data') .. '/lsp'

    -- C/C++
    lspconf.clangd.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        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'
        }
    }

    -- Lua
    local lua_lib_path = {}
    local lua_runtime_path = {}

    -- lua_lib_path[vim.fn.expand('~/.luarocks/share/lua/5.3')] = true
    -- lua_lib_path[vim.fn.expand('/usr/share/lua/5.3')] = true
    lua_lib_path[os.getenv('VIMRUNTIME') .. '/lua'] = true
    -- local function add(lib)
    --     for _, p in pairs(vim.fn.expand(lib .. '/lua', false, true)) do
    --         p = vim.loop.fs_realpath(p)
    --         if p then lua_lib_path[p] = true end
    --     end
    -- end
    -- for _, site in pairs(vim.split(vim.opt.packpath:get(), ',')) do
    --     add(site .. '/pack/*/opt/*')
    --     add(site .. '/pack/*/start/*')
    -- end

    table.insert(lua_runtime_path, 'lua/?.lua')
    table.insert(lua_runtime_path, 'lua/?/init.lua')
    -- table.insert(lua_runtime_path, '?.lua')
    -- table.insert(lua_runtime_path, '?/?.lua')
    -- table.insert(lua_runtime_path, '?/init.lua')

    for lib, _ in pairs(lua_lib_path) do
        table.insert(lua_runtime_path, lib .. '/?.lua')
        table.insert(lua_runtime_path, lib .. '/?/init.lua')
    end

    lspconf.sumneko_lua.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        cmd = {
            servers_path .. '/sumneko_lua/bin/Linux/lua-language-server',
            '-E', '-e', 'LANG=en',
            servers_path .. '/sumneko_lua/main.lua'
        },
        settings = {
            Lua = {
                diagnostics = {globals = {'vim'}},
                runtime = {version = 'LuaJIT', path = lua_runtime_path},
                workspace = {
                    library = lua_lib_path,
                    maxPreload = 1000,
                    preloadFileSize = 150
                },
                completion = {callSnippet = 'Replace'},
                hint = {enable = true},
                telemetry = {enable = false}
            }
        }
    }

    -- Go
    lspconf.gopls.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        cmd = {servers_path .. '/gopls/gopls', '--remote=auto'},
        init_options = {
            usePlaceholders = true,
            completeUnimported = true
        }
    }

    -- Yaml
    lspconf.yamlls.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        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,
                    -- editor.formatOnType = true
                }
            }
        }
    }

    -- Ansible
    lspconf.ansiblels.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        cmd = {'node', servers_path .. '/ansiblels/out/server/src/server.js', '--stdio'},
        filetypes = {'yaml', 'yaml.ansible'},
        settings = {
            -- python = {interpreterPath = 'python3'},
            executionEnvironment = {enabled = false}
        }
    }

    -- Tailwind
    -- lspconf.tailwindcss.setup {
    --     on_attach = on_attach,
    --     capabilities = capabilities,
    --     cmd = {servers_path .. '/tailwindcss/node_modules/.bin/tailwindcss-language-server', '--stdio'},
    --     settings = {
    --         tailwindCSS = {
    --             emmetCompletions = true
    --         }
    --     }
    -- }

    -- Emmet
    lspconf.emmet_ls.setup {
        cmd = {servers_path .. '/emmet_ls/node_modules/.bin/emmet-ls', '--stdio'},
        filetypes = {
            'html', 'xml', 'css', 'scss', 'sass',
            'javascript', 'javascriptreact'
        },
        capabilities = capabilities
    }

    -- HTML
    lspconf.html.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        cmd = {servers_path .. '/vscode/node_modules/.bin/vscode-html-language-server', '--stdio'},
        filetypes = {'html', 'markdown'}
    }

    -- Eslint
    lspconf.eslint.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        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'}
        }
    }

    -- JSON
    lspconf.jsonls.setup {
        on_attach = on_attach,
        capabilities = capabilities,
        cmd = {servers_path .. '/vscode/node_modules/.bin/vscode-json-language-server', '--stdio'},
        settings = {
            json = {
                -- Schema catalog: https://www.schemastore.org/api/json/catalog.json
                schemas = {
                    {
                        fileMatch = {'package.json'},
                        url = 'https://json.schemastore.org/package.json'
                    }, {
                        fileMatch = {'tsconfig*.json'},
                        url = 'https://json.schemastore.org/tsconfig.json'
                    }, {
                        fileMatch = {
                            '.prettierrc', '.prettierrc.json',
                            'prettier.config.json'
                        },
                        url = 'https://json.schemastore.org/prettierrc.json'
                    }, {
                        fileMatch = {'.eslintrc', '.eslintrc.json'},
                        url = 'https://json.schemastore.org/eslintrc.json'
                    }, {
                        fileMatch = {
                            '.babelrc', '.babelrc.json', 'babel.config.json'
                        },
                        url = 'https://json.schemastore.org/babelrc.json'
                    },
                    {
                        fileMatch = {'lerna.json'},
                        url = 'https://json.schemastore.org/lerna.json'
                    }, {
                        fileMatch = {'now.json', 'vercel.json'},
                        url = 'https://json.schemastore.org/now.json'
                    }, {
                        fileMatch = {
                            '.stylelintrc', '.stylelintrc.json',
                            'stylelint.config.json'
                        },
                        url = 'http://json.schemastore.org/stylelintrc.json'
                    }
                }
            }
        }
    }

    -- Others
    local servers = {
        rust_analyzer = {'rust-analyzer'},
        sqls = {servers_path .. '/sqls/sqls'},
        cmake = {servers_path .. '/cmake/venv/bin/cmake-language-server'},
        bashls = {servers_path .. '/bashls/node_modules/.bin/bash-language-server', 'start'},
        dockerls = {servers_path .. '/dockerls/node_modules/.bin/docker-langserver', '--stdio'},
        cssls = {servers_path .. '/vscode/node_modules/.bin/vscode-css-language-server', '--stdio'},
        pyright = {servers_path .. '/pyright/node_modules/.bin/pyright-langserver', '--stdio'},
        -- vimls = {servers_path .. '/vimls/node_modules/.bin/vim-language-server', '--stdio'},
        tsserver = {servers_path .. '/tsserver/node_modules/.bin/typescript-language-server', '--stdio'}
    }
    for server, _ in pairs(servers) do
        lspconf[server].setup {
            on_attach = on_attach,
            capabilities = capabilities,
            cmd = servers[server]
        }
    end

    ------------------------
    -- Linters/Formatters --
    ------------------------
    -- TODO: efm-langserver as another option
    local null_ls = require('null-ls')

    -- Register sources
    null_ls.config {
        sources = {
            -- Formatters --
            -- null_ls.builtins.formatting.black.with {
            --     command = 'black'
            -- },
            null_ls.builtins.formatting.clang_format.with {
                filetypes = {'c', 'cpp'}
            },
            -- null_ls.builtins.formatting.cmake_format.with {
            --     command = 'cmake-format'
            -- },
            null_ls.builtins.formatting.fish_indent,
            -- null_ls.builtins.formatting.fixjson.with {
            --     command = 'fixjson'
            -- },
            -- null_ls.builtins.formatting.fnlfmt.with {
            --     command = 'fnlfmt'
            -- },
            -- null_ls.builtins.formatting.goimports.with {
            --     command = 'goimports'
            -- },
            -- null_ls.builtins.formatting.gofmt.with {
            --     command = 'gofmt'
            -- },
            -- null_ls.builtins.formatting.isort.with {
            --     command = 'isort'
            -- },
            -- null_ls.builtins.formatting.nixfmt.with {
            --     command = 'nixfmt'
            -- },
            -- null_ls.builtins.formatting.prettier.with {
            --     command = 'prettier'
            -- },
            -- null_ls.builtins.formatting.rustfmt.with {
            --     command = 'rustfmt'
            -- },
            -- null_ls.builtins.formatting.rustywind.with {
            --     command = 'rustywind'
            -- },
            -- null_ls.builtins.formatting.shfmt,
            -- null_ls.builtins.formatting.stylua.with {
            --     condition = function(utils)
            --         return utils.root_has_file('stylua.toml')
            --     end,
            --     command = 'stylua'
            -- },
            -- null_ls.builtins.formatting.uncrustify.with {
            --     command = 'uncrustify'
            -- },
            -- null_ls.builtins.formatting.yapf.with {
            --     command = 'yapf'
            -- },
            -- null_ls.builtins.formatting.autopep8.with {
            --     command = 'autopep8'
            -- },
            -- null_ls.builtins.formatting.stylelint.with {
            --     command = 'stylelint'
            -- },
            -- Linters --
            null_ls.builtins.diagnostics.shellcheck,
            -- null_ls.builtins.diagnostics.codespell.with {
            --     command = 'codespell'
            -- },
            -- null_ls.builtins.diagnostics.flake8.with {
            --     command = 'flake8'
            -- },
            null_ls.builtins.diagnostics.hadolint.with {
                command = vim.fn.stdpath('config') .. '/scripts/hadolint',
                args = {'$FILENAME'}
            },
            null_ls.builtins.diagnostics.luacheck,
            -- null_ls.builtins.diagnostics.cppcheck,
            -- null_ls.builtins.diagnostics.write_good,
            -- null_ls.builtins.diagnostics.proselint,
            -- null_ls.builtins.diagnostics.markdownlint,
            -- null_ls.builtins.diagnostics.pylint,
            -- null_ls.builtins.diagnostics.vale,
            -- null_ls.builtins.diagnostics.misspell,
            -- null_ls.builtins.diagnostics.vint,
            -- null_ls.builtins.diagnostics.stylelint,
            -- Special --
            null_ls.builtins.hover.dictionary, -- get word definition from dictionaryapi.dev
            -- null_ls.builtins.code_actions.gitsigns
        }
    }

    lspconf['null-ls'].setup {
        on_attach = on_attach
    }
end

function M.sqls_conf()
    require('sqls').setup {
        picker = 'telescope'
    }
end

function M.lightbulb_conf()
    vim.api.nvim_command [[
    autocmd CursorHold * lua require('nvim-lightbulb').update_lightbulb({sign = {enabled = false}, status_text = {enabled = true, text = ' Code action', text_unavailable = ''}})
    ]]
end

function M.trouble_conf()
    require('trouble').setup {
        mode = 'lsp_workspace_diagnostics',
        fold_open = ' ',
        fold_closed = '',
        action_keys = {
            open_split = {'<c-h>'},
            open_vsplit = {'<c-v>'},
            open_tab = {'<c-t>'}
        },
        signs = {
            error = '',
            warning = '',
            information = '',
            hint = '',
            other = ''
        }
    }
end

-- function M.comments_conf()
--     require('todo-comments').setup {
--         signs = false,
--         -- sign_priority = 8,
--         keywords = {
--             FIX = {
--                 icon = ' ', -- icon used for the sign, and in search results
--                 color = 'error', -- can be a hex color, or a named color (see below)
--                 alt = {'FIXME', 'BUG', 'FIXIT', 'ISSUE'}, -- a set of other keywords that all map to this FIX keywords
--                 -- signs = false, -- configure signs for some keywords individually
--             },
--             TODO = {icon = ' ', color = 'info'},
--             HACK = {icon = ' ', color = 'warning'},
--             WARN = {icon = ' ', color = 'warning', alt = {'WARNING', 'XXX'}},
--             PERF = {icon = ' ', alt = {'OPTIM', 'PERFORMANCE', 'OPTIMIZE'}},
--             NOTE = {icon = ' ', color = 'hint', alt = {'INFO'}},
--         },
--         merge_keywords = true,
--         highlight = {
--             before = '', -- 'fg' or 'bg' or empty
--             keyword = 'fg', -- 'fg', 'bg', 'wide' or empty. (wide is the same as bg, but will also highlight surrounding characters)
--             after = '', -- 'fg' or 'bg' or empty
--             pattern = [[.*<(KEYWORDS)\s*:]], -- pattern or table of patterns, used for highlightng (vim regex)
--             comments_only = true, -- uses treesitter to match keywords in comments only
--             max_line_len = 400, -- ignore lines longer than this
--             exclude = {'org'}, -- list of file types to exclude highlighting
--         },
--         colors = {
--             error = {'LspDiagnosticsDefaultError', 'TSDanger', '#bf616a', '#e06c75'},
--             warning = {'LspDiagnosticsDefaultWarning', 'TSWarning', '#ebcb8b', '#e5c07b'},
--             info = {'LspDiagnosticsDefaultInformation', 'TSNote', '#81a1c1', '#61afef'},
--             hint = {'LspDiagnosticsDefaultHint', '#88c0d0', '#56b6c2'},
--             default = {'Normal', '#d8dee9', '#abb2bf'}
--         },
--         search = {
--             command = 'rg',
--             args = {
--                 '--hidden',
--                 '--color=never',
--                 '--no-heading',
--                 '--with-filename',
--                 '--line-number',
--                 '--column',
--             },
--             -- regex that will be used to match keywords.
--             -- don't replace the (KEYWORDS) placeholder
--             pattern = [[\b(KEYWORDS):]] -- ripgrep regex
--             -- pattern = [[\b(KEYWORDS)\b]] -- match without the extra colon. You'll likely get false positives
--         }
--     }
-- end

function M.outline_conf()
    vim.g.symbols_outline = {
        highlight_hovered_item = false,
        show_guides = false
    }
end

function M.dap_conf()
    local dap = require('dap')
    vim.fn.sign_define('DapBreakpoint', {text='', texthl='DapSignDefault'})
    vim.fn.sign_define('DapLogPoint', {text='', texthl='DapSignDefault'})
    vim.fn.sign_define('DapStopped', {text='ﱢ', texthl='DapSignDefault'})
    vim.fn.sign_define('DapBreakpointRejected', {text='', texthl='DapSignRejected'})

    -- Mappings
    vim.api.nvim_command('n', '<leader>dn', ':lua require("dap").continue()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dd', ':lua require("dap").disconnect()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>db', ':lua require("dap").toggle_breakpoint()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dB', ':lua require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: "))<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dl', ':lua require("dap").list_breakpoints()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dc', ':lua require("dap").run_to_cursor()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dz', ':lua require("dap").run_last()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<F9>', ':lua require("dap").step_over()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dv', ':lua require("dap").step_over()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<F10>', ':lua require("dap").step_into()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>di', ':lua require("dap").step_into()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<F11>', ':lua require("dap").step_out()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>do', ':lua require("dap").step_out()<CR>', {noremap = true, silent = true})
    vim.api.nvim_command('n', '<leader>dr', ':lua require("dap").repl.open()<CR>', {noremap = true, silent = true})
end

function M.dapui_conf()
    local dap, dapui = require('dap'), require('dapui')
    dap.listeners.after.event_initialized['dapui_config'] = function() dapui.open() end
    dap.listeners.before.event_terminated['dapui_config'] = function() dapui.close() end
    dap.listeners.before.event_exited['dapui_config'] = function() dapui.close() end

    require('dapui').setup {
        icons = {expanded = '', collapsed = ''},
        mappings = {
            expand = {'<CR>', '<2-LeftMouse>'},
            open = 'o',
            remove = 'd',
            edit = 'e',
            repl = 'r'
        },
        sidebar = {
            elements = {
                -- Provide as ID strings or tables with 'id' and 'size' keys
                -- 'size' can be float or integer > 1
                {id = 'scopes', size = 0.25},
                {id = 'breakpoints', size = 0.25},
                {id = 'stacks', size = 0.25},
                {id = 'watches', size = 0.25},
            },
            size = 40,
            position = 'left'
        },
        tray = {
            elements = {'repl'},
            size = 10,
            position = 'bottom'
        },
        floating = {
            max_height = nil, -- These can be integers or a float between 0 and 1.
            max_width = nil, -- Floats will be treated as percentage of your screen.
            mappings = {
                close = {'q', '<Esc>'}
            }
        },
        windows = {indent = 1}
    }

    -- Mappings
    vim.api.nvim_set_keymap('n', '<leader>de', ':lua require("dapui").eval()<CR>', {noremap = true, silent = true})
    vim.api.nvim_set_keymap('v', '<leader>de', ':lua require("dapui").eval()<CR>', {noremap = true, silent = true})
    vim.api.nvim_set_keymap('n', '<leader>df', ':lua require("dapui").float_element()<CR>', {noremap = true, silent = true})
    vim.api.nvim_set_keymap('n', '<leader>dt', ':lua require("dapui").toggle()<CR>', {noremap = true, silent = true})
end

return M
