function! build#dir(...) abort
  if a:0 == 0
    " No arguments, find build directories.
    let s:dirs = split(substitute(globpath('.', 'build*'), '\.\/', '', 'g'), '\n')
    let l:len = len(s:dirs)
    if l:len == 0
      echoerr 'no build directories found'
    elseif l:len == 1
      " One build directory found, use it.
      let l:dir = s:dirs[0]
      unlet s:dirs
    else
      " Multiple build directories found, select one.
      if exists('*popup_menu')
        " Create popup menu to select the build directory. Callback to this
        " function on completion, handled in the else branch below.
        call popup_menu(s:dirs, #{
        \   filter: 'popup_filter_menu',
        \   callback: 'build#dir',
        \ })
      else
        " Fallback to inputlist when popup_menu is not available.
        let l:choices = []
        let l:index = 1
        for l:dir in s:dirs
          call add(l:choices, string(l:index).': '.l:dir)
          let l:index += 1
        endfor
        let l:index = inputlist(l:choices)
        let l:dir = s:dirs[l:index - 1]
        echomsg ' '.l:dir
      endif
    endif
  else
    if a:0 == 1
      " Single argument, invoked by :BuildDir.
      let l:dir = a:1
    elseif a:0 == 2
      " Two arguments, called back by popup_menu().
      let l:dirs = s:dirs
      unlet s:dirs
      if a:2 == -1
        " Selection in popup_menu() was cancelled.
        return
      endif
      let l:dir = l:dirs[a:2 - 1]
    else
      echoerr 'build#dir called with too many arguments'
    endif
  endif
  if exists('l:dir')
    " Set build directory.
    let l:cwd = substitute(getcwd(), '\\', '\/', 'g')
    let $BUILD_DIR = l:cwd.'/'.substitute(l:dir, '\/$', '', '')
    if executable('compdb')
      " Post-process compile_commands.json with compdb, adds header files to
      " missing compile_commands.json for more accurate diagnostics.
      let l:database_dir = l:cwd.'/.vim'
      let l:compile_commands = l:database_dir.'/compile_commands.json'
      call systemlist('compdb -p '.$BUILD_DIR.' list > '.l:compile_commands)
    else
      let l:database_dir = $BUILD_DIR
    endif
    " Read/create .vim/coc-settings.json
    let l:coc_settings = {}
    if isdirectory('.vim')
      let l:coc_settings = json_decode(join(readfile('.vim/coc-settings.json'), ''))
    else
      call mkdir('.vim')
    endif
    " Update .vim/coc-settings.json with new build directory.
    let l:coc_settings['clangd.compilationDatabasePath'] = l:database_dir
    let l:coc_settings['cmake.lsp.buildDirectory'] = $BUILD_DIR
    call writefile([json_encode(l:coc_settings)], '.vim/coc-settings.json')
    " Finally restart coc.nvim with new config.
    CocRestart
  endif
endfunction

function! build#targets(ArgLead, CmdLine, CursorPos) abort
  if !exists('$BUILD_DIR')
    echoerr 'build directory not set'
    return ''
  endif
  let l:targets = []
  if filereadable($BUILD_DIR.'/build.ninja')
    " Ask ninja for the list of targets and prepare for returning.
    for l:target in split(system('ninja -C '.$BUILD_DIR.' -t targets'), '\n')
      call add(l:targets, substitute(l:target, ':.*$', '', ''))
    endfor
  elseif filereadable($BUILD_DIR.'/Makefile')
    " TODO: Support make, it's much less straight forwards than ninja.
  endif
  return join(l:targets, "\n")
endfunction

function! build#run(...) abort
  if !exists('$BUILD_DIR')
    call echo#error('build directory not set')
    return
  endif
  let l:build_dir = $BUILD_DIR
  if filereadable($BUILD_DIR.'/build.ninja')
    " Execute ninja in a terminal window.
    execute 'terminal ninja -C '.l:build_dir.' '.join(a:000, ' ')
  elseif filereadable($BUILD_DIR.'/Makefile')
    " Execute make in a terminal window.
    execute 'terminal make -C '.l:build_dir.' '.join(a:000, ' ')
  endif
endfunction