{ config, lib, pkgs, ... }:

let
  frameline = pkgs.callPackage (pkgs.fetchFromGitea
    { domain = "maxwell.ydns.eu/git";
      owner  = "rnhmjoj";
      repo   = "nvim-frameline";
      rev    = "v0.1.0";
      sha256 = "PrTSSoXbu+qtTsJUv81z+MuTUmB1RHLPEWFQQnu6+J8=";
    }) { };

  plugins = with pkgs.vimPlugins;
    [ # UI
      undotree gitsigns-nvim
      frameline nvim-fzf

      # Syntax
      vim-pandoc-syntax
      nix-queries
      (nvim-treesitter.withPlugins (p: with p;
        [ bash fish
          html css markdown
          nix python
        ]))

      # Misc
      vim-fugitive supertab neomake
      auto-pairs plenary-nvim
    ];

  pack = pkgs.linkFarm "neovim-plugins"
    (map (pkg:
      { name = "pack/${pkg.name}/start/${pkg.name}";
        path = toString pkg;
      }) (lib.concatMap (p: [p] ++ p.dependencies or []) plugins));

  neovim-wrapped = pkgs.runCommand "${pkgs.neovim-unwrapped.name}"
    { nativeBuildInputs = [ pkgs.makeWrapper ]; }
    ''
      mkdir -p "$out"
      makeWrapper '${pkgs.neovim-unwrapped}/bin/nvim' "$out/bin/nvim" \
        --add-flags "-u ${conf}"
    '';

  nix-queries = pkgs.writeTextDir "/queries/nix/injections.scm"
  ''
    ; /*language*/ highlight (too slow)
    ; ((comment) @language
    ;   [
    ;     (string_expression (string_fragment) @content)
    ;     (indented_string_expression (string_fragment) @content)
    ;   ]
    ;   (#gsub! @language "/%*%s*([%w%p]+)%s*%*/" "%1"))
    ; @combined

    ; writeText highlight
    (apply_expression
      function:
        (apply_expression
          function: (_) @_func
          argument: (string_expression (string_fragment) @language))
      argument: [(string_expression (string_fragment) @content)
                 (indented_string_expression (string_fragment) @content)]
      (#match? @_func "(^|\\.)writeText(Dir)?$")
      (#gsub! @language ".*%.(.*)" "%1"))
    @combined
  '';

  conf = pkgs.writeText "init.lua" ''
    local opt     = vim.opt
    local cmd     = vim.api.nvim_command
    local keymap  = vim.keymap.set
    local autocmd = vim.api.nvim_create_autocmd

    -- Load plugins
    opt.packpath = ${builtins.toJSON pack}
    opt.runtimepath:prepend(${builtins.toJSON pack})


    --
    -- Options
    --

    local cache   = os.getenv('XDG_CACHE_HOME')..'/nvim'
    opt.directory = cache..'/tmp'
    opt.backupdir = cache..'/tmp'
    opt.shadafile = cache..'/shada'
    opt.undodir   = cache..'/undo'

    opt.hidden     = true   -- Hide buffers
    opt.mouse      = 'a'    -- Enable mouse support
    opt.ignorecase = true   -- Case insensitive search...
    opt.smartcase  = true   -- ...for lowercase terms

    opt.fsync       = true   -- Sync writes
    opt.swapfile    = false  -- Disable swap files
    opt.writebackup = true   -- Backup file before overwriting...
    opt.backup      = false  -- ...but delete it on success
    opt.undofile    = true   -- Store all changes

    opt.modeline   = false          -- Disable for Security
    opt.showmatch  = true           -- Highlight matched parenthesis
    opt.clipboard  = 'unnamedplus'  -- Yank to clipboard

    -- Files to ignore
    opt.wildignore = {
      '*.so', '*.hi', '*.a', '*.la', '*.mod',
      '*/__pycache__/*',
      '*/dist/*',
      '*/result/*',
      '*/.git/*'
    }

    opt.shiftwidth = 2     -- Tabs
    opt.tabstop    = 2     --
    opt.expandtab  = true  --

    opt.number      = true    -- Line numbering
    opt.smartindent = true    -- Indentation
    opt.showmode    = false   -- Disable printing of mode changes
    opt.ruler       = false   -- Already in statusline
    opt.fillchars   = {
      eob=' ',                -- Hide ~ on empty lines
      vert='│',               -- make vertical split sign better
      fold=' ',               -- Hide . in fold markers
    }

    opt.foldmethod = "expr"                       -- Folding
    opt.foldexpr   = 'nvim_treesitter#foldexpr()' --
    opt.foldlevel  = 99                           -- open by default

    --
    -- OSC 52 clipboard
    --
    --

    if os.getenv('DISPLAY') == nil then
      function copy(sel)
        return function(lines, _)
          local data = vim.fn.system([[base64 -w0]], lines)
          io.stdout:write('\027]52;'..sel..';'..data..'\a')
          vim.g.fallback_clip = lines
        end
      end

      function paste(sel)
        -- currently impossible to implement
        return function() return {vim.g.fallback_clip, ""} end
      end

      vim.g.clipboard = {
        name='ssh',
        copy={['*']=copy's', ['+']=copy'c'},
        paste={['*']=paste's', ['+']=paste'c'},
      }
    end

    --
    -- Terminal mode
    --

    -- Hide some UI elements
    autocmd('TermOpen',  {pattern='*', command='setlocal nonumber'})
    autocmd('TermEnter', {pattern='*', command='set noshowcmd'})
    autocmd('TermLeave', {pattern='*', command='set showcmd'})

    -- Exit without confirmation
    autocmd('TermClose', {pattern='*', command='call feedkeys("\\<CR>")'})

    keymap('t', '<C-b>',  '<C-\\><C-n>',       {noremap=true})  -- Easier escape
    keymap('n', '<C-b>',  '<Nop>',             {noremap=true})  --
    keymap('n', '<C-w>-', ':split +term<CR>',  {silent=true})   -- Tmux-like moves
    keymap('n', '<C-w>|', ':vsplit +term<CR>', {silent=true})   --
    keymap('n', '<C-w>t', ':tabnew +term<CR>', {silent=true})   --
    keymap('n', '<C-w>c', ':quit<CR>',         {silent=true})   --

    --
    -- Keybindings
    --

    vim.g.mapleader = ','

    function listToggle()
      for _, win in ipairs(vim.fn.getwininfo()) do
        if win.loclist == 1 then return cmd('lclose') end
      end
      cmd('lopen')
    end

    fzf = require'fzf'

    function searchFiles()
      local query = [[\( -name .git -o -name __pycache__ -o -path ./dist -o -path ./build \) -prune -o -type f]]
      coroutine.wrap(function()
        res = fzf.fzf('find '..query, "", {border='none'})
        cmd('edit '..res[1])
      end)()
    end

    function searchCommands()
      coroutine.wrap(function()
        local history = {}
        for i = 1, vim.fn.histnr("cmd") do
          history[i] = vim.fn.histget("cmd", i)
        end
        res = fzf.fzf(history, "", {border='none'})
        vim.fn.feedkeys(':'..res[1], 't')
      end)()
    end

    keymap('n', '<C-p>',     searchFiles,           {silent=true})  -- Fuzzy search files
    keymap('n', '<C-e>',     searchCommands,        {silent=true})  -- Fuzzy search command history
    keymap('n', '<Leader>u', ':UndotreeToggle<CR>', {silent=true})  -- Toggle UndoTree
    keymap('n', '<leader>l', listToggle,            {silent=true})  -- Toggle Neomake errors

    keymap('i', 'kj',  '<ESC>', {noremap=true})   -- Exit with kj
    keymap('n', 'o',   'o<ESC>', {noremap=true})  -- Add empty lines
    keymap('n', 'O',   'O<ESC>', {noremap=true})  --
    keymap('x', 'p',   'p:let @+=@0<CR>', {noremap=true, silent=true})  -- Keep selection after p
    keymap('c', 'w!!', 'w !sudo tee >/dev/null %', {silent=true})       -- Save with sudo


    --
    -- Colors
    --

    opt.bg = 'light' -- Use dark colors

    function color(group, args)
      vim.api.nvim_set_hl(0, group, args)
    end

    -- Source code
    color('Cursor',         {ctermfg=14})
    color('Keyword',        {ctermfg=04})
    color('Define',         {ctermfg=03})
    color('Type',           {ctermfg=06})
    color('Identifier',     {ctermfg=13})
    color('Constant',       {ctermfg=03})
    color('Function',       {ctermfg=02})
    color('Include',        {ctermfg=04})
    color('Statement',      {ctermfg=11})
    color('String',         {ctermfg=03})
    color('Number',         {ctermfg=04})
    color('Comment',        {ctermfg=07})
    color('SpecialComment', {ctermfg=15})
    color('Operator',       {ctermfg='none'})
    color('Conceal',        {ctermfg='none', ctermbg='none'})

    -- Text
    color('Title',              {ctermfg=4})
    color('Special',            {ctermfg=12})
    color('Delimiter',          {ctermfg=1})
    color('PandocReferenceURL', {ctermfg=3})
    color('PandocCiteKey',      {ctermfg=6})
    color('PandocTableDelims',  {ctermfg=8})
    color('texBeginEndName',    {ctermfg=2})
    color('Error', {ctermfg='none', ctermbg='none', underline=true})

    -- Editor UI
    color('NonText',    {ctermfg=0})
    color('LineNr',     {ctermfg=8})
    color('Pmenu',      {ctermfg=12, ctermbg=0})
    color('Folded',     {ctermfg=7,  ctermbg=0, cterm={}})
    color('VertSplit',  {ctermfg=8,        cterm={}})
    color('FoldColumn', {ctermfg='none',   cterm={}})
    color('Visual',     {ctermfg='none', ctermbg=0})
    color('Search',     {ctermfg='none', ctermbg=0})

    -- Diff mode
    color('DiffAdd',    {ctermfg=2, ctermbg=0, underline=true})
    color('DiffChange', {ctermfg=3, ctermbg=0, underline=true})
    color('DiffText',   {ctermfg=1, ctermbg=0, underline=true})
    color('DiffDelete', {ctermfg=0, ctermbg=1, underline=true})

    -- Spelling
    color('SpellBad', {ctermfg=1, ctermbg=0, underline=true})
    color('SpellCap', {ctermfg=3, underline=true})

    -- Neomake
    color('NeomakeWarning', {ctermfg=3, underline=true})
    color('ErrorMsg',       {ctermfg=1, ctermbg='none'})
    color('WarningMsg',     {ctermfg=3})

    -- Git signs
    color('SignColumn', {ctermfg=1, ctermbg='none', cterm={}})
    color('GitAdd',     {ctermfg=2})
    color('GitChange',  {ctermfg=3})
    color('GitDelete',  {ctermfg=1})

    -- Statusline
    color('StatusLine',     {ctermfg=8, ctermbg=0, cterm={}})
    color('StatusLineNC',   {ctermfg=4, ctermbg=0, cterm={}})
    color('User1',          {ctermfg=8, ctermbg=0})  -- base
    color('User2',          {ctermfg=3, ctermbg=0})  -- location
    color('StatusLineErr',  {ctermfg=0, ctermbg=1})
    color('StatusLineWarn', {ctermfg=0, ctermbg=3})
    color('TablineTab',     {ctermfg=8, ctermbg=0})
    color('TablineTabCur',  {ctermfg=7, ctermbg=8})
    color('ModeNormal',     {ctermfg=7, ctermbg=14})
    color('ModeInsert',     {ctermfg=7, ctermbg=1})
    color('ModeCommand',    {ctermfg=7, ctermbg=4})
    color('ModeVisual',     {ctermfg=7, ctermbg=3})
    color('ModeVLine',      {ctermfg=7, ctermbg=11})
    color('ModeVBloc',      {ctermfg=7, ctermbg=11})
    color('ModeTerm',       {ctermfg=7, ctermbg=2})

    --
    -- Plugin options
    --

    -- Neomake
    vim.call('neomake#configure#automake', 'nwr', 750)
    vim.g.neomake_warning_sign = {text='W→', texthl='WarningMsg'}
    vim.g.neomake_error_sign = {text='E→', texthl='ErrorMsg'}
    vim.g.neomake_highlight_lines = 1
    vim.g.neomake_virtualtext_current_error = 0

    -- Pandoc Markdown
    autocmd({'BufNewFile', 'BufFilePre', 'BufRead'},
            {pattern='*.md', command='set filetype=markdown.pandoc'})

    -- Git signs
    require'gitsigns'.setup{
      signs={
        add          = {hl='GitAdd'   , text='+'},
        change       = {hl='GitChange', text='δ'},
        delete       = {hl='GitDelete', text='-'},
        topdelete    = {hl='GitDelete', text='‾'},
        changedelete = {hl='GitChange', text='~'},
      },
      keymaps={
        ['n <leader>gb'] = ':lua require"gitsigns".blame_line(true)<CR>',
        ['n <leader>gp'] = ':lua require"gitsigns".preview_hunk()<CR>',
      }
    }

    -- Tree-sitter
    require'nvim-treesitter.configs'.setup{
      highlight={enable=true},
      indent={enable=true},
    }

    -- Non built-in filetypes
    autocmd({'BufNewFile', 'BufRead'},
            {pattern='*.nix', command='setlocal filetype=nix'})

    --
    -- Statusline
    --
    --

    local frameline = require 'frameline'
    local utils = frameline.utils

    function mode(win, buf)
      if not win.is_active then return end

      local k = vim.api.nvim_get_mode().mode
      local modes = {
        n={'Normal', 'Normal'},
        i={'Insert', 'Insert'},
        R={'Replas', 'Insert'},
        v={'Visual', 'Visual'},
        V={'V⋅Line', 'VLine'},
        t={'Termin', 'Term'},
        ['']={'V⋅Bloc', 'VBloc'}
      }
      return utils.highlight('Mode'..modes[k][2], ' '..modes[k][1]..' ')
    end

    function branch(_, buf)
      local head = vim.fn.FugitiveHead()
      if buf.modifiable and head ~= "" then return '⚑ '..head end
    end

    function filename(_, buf)
      local delta = ""
      if buf.modified then delta = 'Δ' end
      if not buf.modifiable then delta = '∇' end
      local fname = buf.name ~= "" and utils.filename or 'new-file'
      return delta..fname
    end

    function neomake()
      local res, msg = vim.call('neomake#statusline#LoclistCounts'), ""
      if res.E then
        msg = msg..utils.highlight('StatusLineErr', ' '..res.E..'E ')
      end
      if res.W then
        msg = msg..utils.highlight('StatusLineWarn', ' '..res.W..'W ')
      end
      return msg
    end

    function readonly(_, buf)
      if buf.modifiable and buf.readonly then return '∅' end
    end

    function encoding(_, buf)
      return buf.fileencoding ~= "" and buf.fileencoding or nil
    end

    function filetype(_, buf)
      return buf.filetype ~= "" and buf.filetype or 'no ft'
    end

    -- Tabline
    frameline.setup_tabline(function()
      local segments = {}
      local api = vim.api
      local color = '%#StatusLine#'

      -- Tabs
      local current = api.nvim_get_current_tabpage()
      local label = ' %d %s '
      for i, tab in pairs(api.nvim_list_tabpages()) do
        -- tab -> active win -> active buf -> name
        local active_buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tab))
        local name = api.nvim_buf_get_name(active_buf)
        name = vim.fn.fnamemodify(name, ':t')  -- filename only

        local group = tab == current and 'TablineTabCur' or 'TablineTab'
        table.insert(segments, utils.highlight(group, label:format(i, name)))
      end

      table.insert(segments, color)
      table.insert(segments, utils.split)

      -- Current date
      table.insert(segments, vim.fn.strftime("%a %H:%M"))

      -- Battery level
      local level  = io.popen('cat /sys/class/power_supply/BAT*/capacity'):read()
      local symbol = ' ∘ '..utils.highlight('User2', '⚡')..color
      local battery = level and symbol..level..'%% ' or ""
      table.insert(segments, battery)

      return table.concat(segments)
    end)

    -- Statusline
    frameline.setup_statusline(function()
      local segments = {}

      -- Left section
      table.insert(segments, utils.subsection{items={mode}})
      table.insert(segments, utils.subsection{
        separator=' → ',
        items={branch, filename, readonly},
      })
      table.insert(segments, utils.split)

      -- Right section
      table.insert(segments, utils.subsection{items={neomake}})
      table.insert(segments, utils.subsection{
        user=2,
        separator=':', stop=' ',
        items={utils.line_number, utils.column_number},
      })
      table.insert(segments, utils.subsection{
        user=1,
        separator=' ∘ ',
        items={utils.percent, encoding, filetype}
      })

      return segments
    end)
  '';
in

{

  # nix build -f '<nixpkgs/nixos>' pkgs.neovim for testing
  nixpkgs.overlays = lib.singleton (self: super: {
    neovim = neovim-wrapped;
  });

}