"=============================================================================
" FILE: install.vim
" AUTHOR:  Shougo Matsushita <Shougo.Matsu at gmail.com>
" License: MIT license
"=============================================================================

" Variables
let s:global_context = {}
let s:log = []
let s:updates_log = []
let s:progress = ''

" Global options definition.
let g:dein#install_max_processes =
      \ get(g:, 'dein#install_max_processes',
      \     dein#util#_is_windows() ? 16 : 8)
let g:dein#install_progress_type =
      \ get(g:, 'dein#install_progress_type', 'echo')
let g:dein#install_message_type =
      \ get(g:, 'dein#install_message_type', 'echo')
let g:dein#install_process_timeout =
      \ get(g:, 'dein#install_process_timeout', 120)
let g:dein#install_log_filename =
      \ get(g:, 'dein#install_log_filename', '')
let g:dein#install_github_api_token =
      \ get(g:, 'dein#install_github_api_token', '')
let g:dein#install_curl_command =
      \ get(g:, 'dein#install_curl_command', 'curl')
let g:dein#install_check_diff =
      \ get(g:, 'dein#install_check_diff', v:false)

function! s:get_job() abort
  if !exists('s:Job')
    let s:Job = vital#dein#import('System.Job')
  endif
  return s:Job
endfunction

function! dein#install#_update(plugins, update_type, async) abort
  if g:dein#_is_sudo
    call s:error('update/install is disabled in sudo session.')
    return
  endif

  let plugins = dein#util#_get_plugins(a:plugins)

  if a:update_type ==# 'install'
    let plugins = filter(plugins, { _, val -> !isdirectory(val.path) })
  endif

  if a:async && !empty(s:global_context) &&
        \ confirm('The installation has not finished. Cancel now?',
        \         "yes\nNo", 2) != 1
    return
  endif

  " Set context.
  let context = s:init_context(plugins, a:update_type, a:async)

  call s:init_variables(context)

  if empty(plugins)
    call s:notify('Target plugins are not found.')
    call s:notify('You may have used the wrong plugin name,'.
          \ ' or all of the plugins are already installed.')
    let s:global_context = {}
    return
  endif

  call s:start()

  if !a:async || has('vim_starting')
    return s:update_loop(context)
  endif

  augroup dein-install
    autocmd!
  augroup END

  if exists('s:timer')
    call timer_stop(s:timer)
    unlet s:timer
  endif

  let s:timer = timer_start(50, {-> dein#install#_polling()}, {'repeat': -1})
endfunction
function! s:update_loop(context) abort
  let errored = 0
  try
    if has('vim_starting')
      while !empty(s:global_context)
        let errored = s:install_async(a:context)
        sleep 50ms
        redraw
      endwhile
    else
      let errored = s:install_blocking(a:context)
    endif
  catch
    call s:error(v:exception)
    call s:error(v:throwpoint)
    return 1
  endtry

  return errored
endfunction

function! dein#install#_check_update(plugins, force, async) abort
  if g:dein#install_github_api_token ==# ''
    call s:error('You need to set g:dein#install_github_api_token' .
          \ ' for the feature.')
    return
  endif
  if !executable(g:dein#install_curl_command)
    call s:error('curl must be executable for the feature.')
    return
  endif

  let s:global_context.progress_type = 'echo'

  let query_max = 100
  let plugins = dein#util#_get_plugins(a:plugins)
  let processes = []
  for index in range(0, len(plugins) - 1, query_max)
    redraw
    call s:print_progress_message(
         \s:get_progress_message('', index, len(plugins)))

    let query = ''
    for plug_index in range(index,
          \ min([index + query_max, len(plugins)]) - 1)
      let plugin_names = split(plugins[plug_index].repo, '/')
      if len(plugin_names) < 2
        " Invalid repository name.
        continue
      endif

      " Note: "repository" API is faster than "search" API
      let query .= printf('a%d:repository(owner:\"%s\", name: \"%s\")' .
            \ '{ pushedAt nameWithOwner }',
            \ plug_index, plugin_names[-2], plugin_names[-1])
    endfor

    let commands = [
         \ g:dein#install_curl_command, '-H', 'Authorization: bearer ' .
         \ g:dein#install_github_api_token,
         \ '-X', 'POST', '-d',
         \ '{ "query": "query {' . query . '}" }',
         \ 'https://api.github.com/graphql'
         \ ]

    let process = {'candidates': []}
    function! process.on_out(data) abort
      let candidates = self.candidates
      if empty(candidates)
        call add(candidates, a:data[0])
      else
        let candidates[-1] .= a:data[0]
      endif

      let candidates += a:data[1:]
    endfunction
    let process.job = s:get_job().start(
        \ s:convert_args(commands),
        \ {'on_stdout': function(process.on_out, [], process)})

    call add(processes, process)
  endfor

  " Get outputs
  let results = []
  for process in processes
    call process.job.wait(g:dein#install_process_timeout * 1000)

    if !empty(process.candidates)
      let result = process.candidates[0]
      try
        let json = json_decode(result)
        let results += filter(values(json['data']),
              \ { _, val -> type(val) == v:t_dict
              \             && has_key(val, 'pushedAt') })
      catch
        call s:error('json output decode error: ' + string(result))
      endtry
    endif
  endfor

  " Get pushed time.

  let check_pushed = {}
  for node in results
    let format = '%Y-%m-%dT%H:%M:%SZ'
    let pushed_at = node['pushedAt']
    let check_pushed[node['nameWithOwner']] =
          \ exists('*strptime') ?
          \  strptime(format, pushed_at) :
          \  dein#DateTime#from_format(pushed_at, format).unix_time()
  endfor

  " Get the last updated time by rollbackfile timestamp.
  " Note: .git timestamp may be changed by git commands.
  let rollbacks = reverse(sort(glob(
        \ s:get_rollback_directory() . '/*', v:true, v:true)))
  let rollback_time = empty(rollbacks) ? -1 : getftime(rollbacks[0])

  " Compare with .git directory updated time.
  let updated = []
  for plugin in plugins
    if !has_key(check_pushed, plugin.repo)
      continue
    endif

    let git_path = plugin.path . '/.git'
    let repo_time = isdirectory(plugin.path) ? getftime(git_path) : -1

    if min([repo_time, rollback_time]) < check_pushed[plugin.repo]
      call add(updated, plugin)
    endif
  endfor

  redraw | echo ''

  " Clear global context
  let s:global_context = {}

  if empty(updated)
    call s:notify(strftime('Done: (%Y/%m/%d %H:%M:%S)'))
    return
  endif

  " Note: Use echo to display it in confirm
  call s:echo('Updated plugins: ' .
        \ string(map(copy(updated), { _, val -> val.name })), 'echo')
  if !a:force && confirm(
        \ 'Updated plugins are exists. Update now?', "yes\nNo", 2) != 1
    return
  endif

  call dein#install#_update(updated, 'update', a:async)
endfunction

function! dein#install#_reinstall(plugins) abort
  if g:dein#_is_sudo
    call s:error('update/install is disabled in sudo session.')
    return
  endif

  let plugins = dein#util#_get_plugins(a:plugins)

  for plugin in plugins
    " Remove the plugin
    if plugin.type ==# 'none'
          \ || get(plugin, 'local', 0)
          \ || (plugin.sourced &&
          \     index(['dein'], plugin.normalized_name) >= 0)
      call dein#util#_error(
            \ printf('|%s| Cannot reinstall the plugin!', plugin.name))
      continue
    endif

    " Reinstall.
    call s:print_progress_message(printf('|%s| Reinstalling...', plugin.name))

    if isdirectory(plugin.path)
      call dein#install#_rm(plugin.path)
    endif
  endfor

  call dein#install#_update(dein#util#_convert2list(a:plugins),
        \ 'install', 0)
endfunction
function! dein#install#_direct_install(repo, options) abort
  if g:dein#_is_sudo
    call s:error('update/install is disabled in sudo session.')
    return
  endif

  let options = copy(a:options)
  let options.merged = 0

  let plugin = dein#add(a:repo, options)
  if empty(plugin)
    return
  endif

  call dein#install#_update(plugin.name, 'install', 0)
  call dein#source(plugin.name)

  " Add to direct_install.vim
  let file = dein#get_direct_plugins_path()
  let line = printf('call dein#add(%s, %s)',
        \ string(a:repo), string(options))
  if !filereadable(file)
    call dein#util#_safe_writefile([line], file)
  else
    call dein#util#_safe_writefile(add(readfile(file), line), file)
  endif
endfunction
function! dein#install#_rollback(date, plugins) abort
  if g:dein#_is_sudo
    call s:error('update/install is disabled in sudo session.')
    return
  endif

  let glob = s:get_rollback_directory() . '/' . a:date . '*'
  let rollbacks = reverse(sort(glob(glob, v:true, v:true)))
  if empty(rollbacks)
    return
  endif

  call dein#install#_load_rollback(rollbacks[0], a:plugins)
endfunction

function! dein#install#_recache_runtimepath() abort
  if g:dein#_is_sudo
    return
  endif

  let start = reltime()

  " Clear runtime path.
  call s:clear_runtimepath()

  let plugins = values(dein#get())

  let merged_plugins = filter(copy(plugins), { _, val -> val.merged })
  let lazy_merged_plugins = filter(copy(merged_plugins),
        \ { _, val -> val.lazy })
  let nolazy_merged_plugins = filter(copy(merged_plugins),
        \ { _, val -> !val.lazy })
  let merge_ftdetect_plugins = filter(copy(plugins),
        \ { _, val -> get(val, 'merge_ftdetect', 0)
        \             || (val.merged && !val.lazy) })

  call s:copy_files(lazy_merged_plugins, '')

  let runtime = dein#util#_get_runtime_path()

  " Remove plugin directory
  call dein#install#_rm(runtime . '/plugin')
  call dein#install#_rm(runtime . '/after/plugin')

  call s:copy_files(nolazy_merged_plugins, '')

  call s:helptags()

  call s:generate_ftplugin()

  " Clear ftdetect and after/ftdetect directories.
  call dein#install#_rm(runtime . '/ftdetect')
  call dein#install#_rm(runtime . '/after/ftdetect')

  call s:merge_files(merge_ftdetect_plugins, 'ftdetect')
  call s:merge_files(merge_ftdetect_plugins, 'after/ftdetect')

  silent call dein#remote_plugins()

  call dein#call_hook('post_source')

  call dein#install#_save_rollback(
        \ s:get_rollback_directory() . '/' . strftime('%Y%m%d%H%M%S'), [])

  call dein#util#_clear_state()

  call s:log(strftime('Runtimepath updated: (%Y/%m/%d %H:%M:%S)'))
  call s:log('recache_runtimepath: ' . split(reltimestr(reltime(start)))[0])
endfunction
function! s:clear_runtimepath() abort
  if dein#util#_get_cache_path() ==# ''
    call dein#util#_error('Invalid base path.')
    return
  endif

  let runtimepath = dein#util#_get_runtime_path()

  " Remove runtime path
  call dein#install#_rm(runtimepath)

  if !isdirectory(runtimepath)
    " Create runtime path
    call dein#util#_safe_mkdir(runtimepath)
  endif
endfunction
function! s:helptags() abort
  if g:dein#_runtime_path ==# ''
    return ''
  endif

  try
    let tags = dein#util#_get_runtime_path() . '/doc'
    call dein#util#_safe_mkdir(tags)
    call s:copy_files(filter(values(dein#get()),
          \ { _, val -> !val.merged }), 'doc')
    silent execute 'helptags' fnameescape(tags)
  catch /^Vim(helptags):E151:/
    " Ignore an error that occurs when there is no help file
  catch
    call s:error('Error generating helptags:')
    call s:error(v:exception)
    call s:error(v:throwpoint)
  endtry
endfunction
function! s:copy_files(plugins, directory) abort
  let directory = (a:directory ==# '' ? '' : '/' . a:directory)
  let srcs = filter(map(copy(a:plugins), { _, val -> val.rtp . directory }),
        \ { _, val -> isdirectory(val) })
  let stride = 50
  for start in range(0, len(srcs), stride)
    call dein#install#_copy_directories(srcs[start : start + stride-1],
          \ dein#util#_get_runtime_path() . directory)
  endfor
endfunction
function! s:merge_files(plugins, directory) abort
  let files = []
  for plugin in a:plugins
    for file in filter(globpath(
          \ plugin.rtp, a:directory.'/**', v:true, v:true),
          \ { _, val -> !isdirectory(val) })
      let files += readfile(file, ':t')
    endfor
  endfor

  if !empty(files)
    call dein#util#_cache_writefile(files,
          \ printf('.dein/%s/%s.vim', a:directory, a:directory))
  endif
endfunction
function! dein#install#_save_rollback(rollbackfile, plugins) abort
  let revisions = {}
  for plugin in filter(dein#util#_get_plugins(a:plugins),
        \ { _, val -> s:check_rollback(val) })
    let rev = s:get_revision_number(plugin)
    if rev !=# ''
      let revisions[plugin.name] = rev
    endif
  endfor

  call dein#util#_safe_writefile(
        \ [json_encode(revisions)], expand(a:rollbackfile))
endfunction
function! dein#install#_load_rollback(rollbackfile, plugins) abort
  let revisions = json_decode(readfile(a:rollbackfile)[0])

  let plugins = dein#util#_get_plugins(a:plugins)
  call filter(plugins, { _, val -> has_key(revisions, val.name)
        \ && has_key(dein#util#_get_type(val.type),
        \            'get_rollback_command')
        \ && s:check_rollback(val)
        \ && s:get_revision_number(val) !=# revisions[val.name]
        \ })
  if empty(plugins)
    return
  endif

  for plugin in plugins
    let type = dein#util#_get_type(plugin.type)
    let cmd = type.get_rollback_command(
          \ dein#util#_get_type(plugin.type), revisions[plugin.name])
    call dein#install#_each(cmd, plugin)
  endfor

  call dein#recache_runtimepath()
  call s:error('Rollback to '.fnamemodify(a:rollbackfile, ':t').' version.')
endfunction
function! s:get_rollback_directory() abort
  let parent = printf('%s/rollbacks/%s',
        \ dein#util#_get_cache_path(), g:dein#_progname)
  call dein#util#_safe_mkdir(parent)

  return parent
endfunction
function! s:check_rollback(plugin) abort
  return !has_key(a:plugin, 'local') && !get(a:plugin, 'frozen', 0)
endfunction

function! dein#install#_get_default_ftplugin() abort
  return [
        \ 'if exists("g:did_load_ftplugin")',
        \ '  finish',
        \ 'endif',
        \ 'let g:did_load_ftplugin = 1',
        \ '',
        \ 'augroup filetypeplugin',
        \ '  autocmd FileType * call s:ftplugin()',
        \ 'augroup END',
        \ '',
        \ 'function! s:ftplugin()',
        \ '  if exists("b:undo_ftplugin")',
        \ '    silent! execute b:undo_ftplugin',
        \ '    unlet! b:undo_ftplugin b:did_ftplugin',
        \ '  endif',
        \ '',
        \ '  let filetype = expand("<amatch>")',
        \ '  if filetype !=# ""',
        \ '    if &cpoptions =~# "S" && exists("b:did_ftplugin")',
        \ '      unlet b:did_ftplugin',
        \ '    endif',
        \ '    for ft in split(filetype, ''\.'')',
        \ '      execute "runtime! ftplugin/" . ft . ".vim"',
        \ '      \ "ftplugin/" . ft . "_*.vim"',
        \ '      \ "ftplugin/" . ft . "/*.vim"',
        \ '      if has("nvim")',
        \ '        execute "runtime! ftplugin/" . ft . ".lua"',
        \ '        \ "ftplugin/" . ft . "_*.lua"',
        \ '        \ "ftplugin/" . ft . "/*.lua"',
        \ '      endif',
        \ '    endfor',
        \ '  endif',
        \ '  call s:after_ftplugin()',
        \ 'endfunction',
        \ '',
        \]
endfunction
function! s:generate_ftplugin() abort
  if empty(g:dein#_ftplugin)
    return
  endif

  " Create after/ftplugin
  let after = dein#util#_get_runtime_path() . '/after/ftplugin'
  call dein#util#_safe_mkdir(after)

  " Merge g:dein#_ftplugin
  let ftplugin = {}
  for [key, string] in items(g:dein#_ftplugin)
    for ft in (key ==# '_' ? ['_'] : split(key, '_'))
      if !has_key(ftplugin, ft)
        let ftplugin[ft] = (ft ==# '_') ? [] : [
              \ "if exists('b:undo_ftplugin')",
              \ "  let b:undo_ftplugin .= '|'",
              \ 'else',
              \ "  let b:undo_ftplugin = ''",
              \ 'endif',
              \ ]
      endif
      let ftplugin[ft] += split(string, '\n')
    endfor
  endfor

  " Generate ftplugin.vim
  call dein#util#_safe_writefile(
        \ dein#install#_get_default_ftplugin() + [
        \ 'function! s:after_ftplugin()',
        \ ] + get(ftplugin, '_', []) + ['endfunction'],
        \ dein#util#_get_runtime_path() . '/ftplugin.vim')

  " Generate after/ftplugin
  for [filetype, list] in filter(items(ftplugin),
        \ { _, val -> val[0] !=# '_' })
    call dein#util#_safe_writefile(
          \ list, printf('%s/%s.vim', after, filetype))
  endfor
endfunction

function! dein#install#_is_async() abort
  return g:dein#install_max_processes > 1
endfunction

function! dein#install#_polling() abort
  if exists('+guioptions')
    " Note: guioptions-! does not work in async state
    let save_guioptions = &guioptions
    set guioptions-=!
  endif

  call s:install_async(s:global_context)

  if exists('+guioptions')
    let &guioptions = save_guioptions
  endif
endfunction

function! dein#install#_remote_plugins() abort
  if !has('nvim') || g:dein#_is_sudo
    return
  endif

  if has('vim_starting')
    " Note: UpdateRemotePlugins is not defined in vim_starting
    autocmd dein VimEnter * silent call dein#remote_plugins()
    return
  endif

  if exists(':UpdateRemotePlugins') != 2
    return
  endif

  " Load not loaded neovim remote plugins
  let remote_plugins = filter(values(dein#get()),
        \ { _, val -> isdirectory(val.rtp . '/rplugin') && !val.sourced })

  call dein#autoload#_source(remote_plugins)

  call s:log('loaded remote plugins: ' .
        \ string(map(copy(remote_plugins), { _, val -> val.name })))

  let &runtimepath = dein#util#_join_rtp(dein#util#_uniq(
        \ dein#util#_split_rtp(&runtimepath)), &runtimepath, '')

  let result = execute('UpdateRemotePlugins', '')
  call s:log(result)
endfunction

function! dein#install#_each(cmd, plugins) abort
  let plugins = filter(dein#util#_get_plugins(a:plugins),
        \ { _, val -> isdirectory(val.path) })

  let global_context_save = s:global_context

  let context = s:init_context(plugins, 'each', 0)
  call s:init_variables(context)

  let cwd = getcwd()
  let error = 0
  try
    for plugin in plugins
      call dein#install#_cd(plugin.path)

      if dein#install#_execute(a:cmd)
        let error = 1
      endif
    endfor
  catch
    call s:error(v:exception . ' ' . v:throwpoint)
    return 1
  finally
    let s:global_context = global_context_save
    call dein#install#_cd(cwd)
  endtry

  return error
endfunction
function! dein#install#_build(plugins) abort
  let error = 0
  for plugin in filter(dein#util#_get_plugins(a:plugins),
        \ { _, val -> isdirectory(val.path) && has_key(val, 'build') })
    call s:print_progress_message('Building: ' . plugin.name)
    if dein#install#_each(plugin.build, plugin)
      let error = 1
    endif
  endfor
  return error
endfunction

function! dein#install#_get_log() abort
  return s:log
endfunction
function! dein#install#_get_updates_log() abort
  return s:updates_log
endfunction
function! dein#install#_get_context() abort
  return s:global_context
endfunction
function! dein#install#_get_progress() abort
  return s:progress
endfunction

function! s:get_progress_message(name, number, max) abort
  return printf('(%'.len(a:max).'d/%'.len(a:max).'d) [%s%s] %s',
        \ a:number, a:max,
        \ repeat('+', (a:number*20/a:max)),
        \ repeat('-', 20 - (a:number*20/a:max)),
        \ a:name)
endfunction
function! s:get_plugin_message(plugin, number, max, message) abort
  return printf('(%'.len(a:max).'d/%d) |%-20s| %s',
        \ a:number, a:max, a:plugin.name, a:message)
endfunction
function! s:get_short_message(plugin, number, max, message) abort
  return printf('(%'.len(a:max).'d/%d) %s', a:number, a:max, a:message)
endfunction
function! s:get_sync_command(plugin, update_type, number, max) abort "{{{i
  let type = dein#util#_get_type(a:plugin.type)

  if has_key(type, 'get_sync_command')
    let cmd = type.get_sync_command(a:plugin)
  else
    return ['', '']
  endif

  if empty(cmd)
    return ['', '']
  endif

  let message = s:get_plugin_message(a:plugin, a:number, a:max, string(cmd))

  return [cmd, message]
endfunction
function! s:get_revision_number(plugin) abort
  if !isdirectory(a:plugin.path)
    return ''
  endif

  let type = dein#util#_get_type(a:plugin.type)

  if has_key(type, 'get_revision_number')
    return type.get_revision_number(a:plugin)
  endif

  if !has_key(type, 'get_revision_number_command')
    return ''
  endif

  let cmd = type.get_revision_number_command(a:plugin)
  if empty(cmd)
    return ''
  endif

  let rev = s:system_cd(cmd, a:plugin.path)

  " If rev contains spaces, it is error message
  if rev =~# '\s'
    call s:error(a:plugin.name)
    call s:error('Error revision number: ' . rev)
    return ''
  elseif rev ==# ''
    call s:error(a:plugin.name)
    call s:error('Empty revision number: ' . rev)
    return ''
  endif
  return rev
endfunction
function! s:get_updated_log_message(plugin, new_rev, old_rev) abort
  let type = dein#util#_get_type(a:plugin.type)

  let cmd = has_key(type, 'get_log_command') ?
        \ type.get_log_command(a:plugin, a:new_rev, a:old_rev) : ''
  let log = empty(cmd) ? '' : s:system_cd(cmd, a:plugin.path)
  return log !=# '' ? log :
        \            (a:old_rev  == a:new_rev) ? ''
        \            : printf('%s -> %s', a:old_rev, a:new_rev)
endfunction
function! s:lock_revision(process, context) abort
  let num = a:process.number
  let max = a:context.max_plugins
  let plugin = a:process.plugin

  let type = dein#util#_get_type(plugin.type)
  if !has_key(type, 'get_revision_lock_command')
    return 0
  endif

  let cmd = type.get_revision_lock_command(plugin)

  if empty(cmd)
    " Skipped.
    return 0
  elseif type(cmd) == v:t_string && cmd =~# '^E: '
    " Errored.
    call s:error(plugin.path)
    call s:error(cmd[3:])
    return -1
  endif

  if get(plugin, 'rev', '') !=# ''
    call s:log(s:get_plugin_message(plugin, num, max, 'Locked'))
  endif

  let result = s:system_cd(cmd, plugin.path)
  let status = dein#install#_status()

  if status
    call s:error(plugin.path)
    call s:error(result)
    return -1
  endif
endfunction
function! s:get_updated_message(context, plugins) abort
  if empty(a:plugins)
    return ''
  endif

  " Diff check
  if g:dein#install_check_diff
    call s:check_diff(a:plugins)
  endif

  return "Updated plugins:\n".
        \ join(map(copy(a:plugins),
        \ { _, val -> '  ' . val.name . (val.commit_count == 0 ? ''
        \                     : printf('(%d change%s)',
        \                              val.commit_count,
        \                              (val.commit_count == 1 ? '' : 's')))
        \    . ((val.old_rev !=# ''
        \        && val.uri =~# '^\h\w*://github.com/') ? "\n"
        \      . printf('    %s/compare/%s...%s',
        \        substitute(substitute(val.uri, '\.git$', '', ''),
        \          '^\h\w*:', 'https:', ''),
        \        val.old_rev, val.new_rev) : '')
        \ }) , "\n")
endfunction
function! s:get_errored_message(plugins) abort
  if empty(a:plugins)
    return ''
  endif

  let msg = "Error installing plugins:\n".join(
        \ map(copy(a:plugins), { _, val -> '  ' . val.name }), "\n")
  let msg .= "\n"
  let msg .= "Please read the error message log with the :message command.\n"

  return msg
endfunction
function! s:check_diff(plugins) abort
  for plugin in a:plugins
    let type = dein#util#_get_type(plugin.type)
    if !has_key(type, 'get_diff_command') || plugin.old_rev ==# ''
      continue
    endif

    let diff = s:system_cd(
          \ type.get_diff_command(plugin, plugin.old_rev, plugin.new_rev),
          \ plugin.path)
    if diff !=# ''
      echo printf("%s: The documentation is updated\n%s\n\n",
            \ plugin.name, diff)
    endif
  endfor
endfunction


" Helper functions
function! dein#install#_cd(path) abort
  if !isdirectory(a:path)
    return
  endif

  try
    noautocmd execute (haslocaldir() ? 'lcd' : 'cd') fnameescape(a:path)
  catch
    call s:error('Error cd to: ' . a:path)
    call s:error('Current directory: ' . getcwd())
    call s:error(v:exception)
    call s:error(v:throwpoint)
  endtry
endfunction

function! dein#install#_system(command) abort
  return s:job_system.system(a:command)
endfunction
let s:job_system = {}
function! s:job_system.on_out(data) abort
  let candidates = s:job_system.candidates
  if empty(candidates)
    call add(candidates, a:data[0])
  else
    let candidates[-1] .= a:data[0]
  endif
  let candidates += a:data[1:]
endfunction
function! s:job_system.system(cmd) abort
  let self.candidates = []

  let job = s:get_job().start(
        \ s:convert_args(a:cmd),
        \ {'on_stdout': self.on_out})

  let s:job_system.status = job.wait(
        \ g:dein#install_process_timeout * 1000)
  return join(s:job_system.candidates, "\n")
endfunction
function! dein#install#_status() abort
  return s:job_system.status
endfunction
function! s:system_cd(command, path) abort
  let cwd = getcwd()
  try
    call dein#install#_cd(a:path)
    return dein#install#_system(a:command)
  finally
    call dein#install#_cd(cwd)
  endtry
  return ''
endfunction

function! dein#install#_execute(command) abort
  return s:job_execute.execute(a:command)
endfunction
let s:job_execute = {}
function! s:job_execute.on_out(data) abort
  for line in a:data
    echo line
  endfor

  let candidates = s:job_execute.candidates
  if empty(candidates)
    call add(candidates, a:data[0])
  else
    let candidates[-1] .= a:data[0]
  endif
  let candidates += a:data[1:]
endfunction
function! s:job_execute.execute(cmd) abort
  let self.candidates = []

  let job = s:get_job().start(
        \ s:convert_args(a:cmd),
        \ {'on_stdout': self.on_out})

  return job.wait(g:dein#install_process_timeout * 1000)
endfunction

function! dein#install#_system_bg(command) abort
  let job = s:get_job().start(
        \ s:convert_args(a:command),
        \ {
        \   'on_stderr': {
        \     v -> map(copy(v), { _, val -> dein#util#_error(val) })
        \   }
        \ })
  return job
endfunction

function! dein#install#_rm(path) abort
  if !isdirectory(a:path) && !filereadable(a:path)
    return
  endif

  try
    call delete(a:path, 'rf')
  catch
    call s:error('Error deleting directory: ' . a:path)
    call s:error(v:exception)
    call s:error(v:throwpoint)
  endtry
endfunction

function! dein#install#_copy_directories(srcs, dest) abort
  if empty(a:srcs)
    return 0
  endif

  if dein#util#_is_windows() && has('python3')
        \ && dein#install#_python_version_check()
    " In Windows, copy directory is too slow!
    " Note: Python 3.8.0 is needed
    return dein#install#_copy_directories_py(a:srcs, a:dest)
  endif

  let status = 0
  if dein#util#_is_windows()
    if !executable('robocopy')
      call dein#util#_error('robocopy command is needed.')
      return 1
    endif

    let status = dein#install#_copy_directories_robocopy(a:srcs, a:dest)
  else " Not Windows
    let srcs = map(filter(copy(a:srcs),
          \ { _, val -> len(glob(val . '/*', v:true, v:true)) }),
          \ { _, val -> shellescape(val . '/') })
    let is_rsync = executable('rsync')
    if is_rsync
      let cmdline = printf("rsync -a -q --exclude '/.git/' %s %s",
            \ join(srcs), shellescape(a:dest))
      let result = dein#install#_system(cmdline)
      let status = dein#install#_status()
    else
      for src in srcs
        let cmdline = printf('cp -Ra %s* %s', src, shellescape(a:dest))
        let result = dein#install#_system(cmdline)
        let status = dein#install#_status()
        if status
          break
        endif
      endfor
    endif
    if status
      call dein#util#_error('copy command failed.')
      call dein#util#_error(result)
      call dein#util#_error('cmdline: ' . cmdline)
    endif
  endif

  return status
endfunction
function! dein#install#_copy_directories_robocopy(srcs, dest) abort
  let jobs = []
  let format = 'robocopy.exe %s /E /NJH /NJS '
        \ . '/NDL /NC /NS /MT:8 /XO /XD ".git"'
  let srcs = a:srcs
  let MAX_LINES = 8
  while !empty(srcs)
    let temp = tempname() . '.bat'
    let lines = ['@echo off']

    while len(lines) < MAX_LINES && !empty(srcs)
      let path = substitute(printf('"%s" "%s"', srcs[0], a:dest),
            \               '/', '\\', 'g')
      call add(lines, printf(format, path))

      let srcs = srcs[1:]
    endwhile

    call dein#util#_safe_writefile(lines, temp)

    let job = dein#install#_system_bg(temp)
    call add(jobs, { 'commands': lines, 'job': job })
  endwhile

  " Async check
  let ret = 0
  while !empty(jobs)
    let i = 0
    for job in jobs
      let status = job.job.wait(100)
      if status == -1
        " Next check
        let i += 1
        continue
      endif

      " Robocopy returns between 0 and 7 upon success
      let status = (status > 7) ? status : 0

      if status
        call dein#util#_error('copy command failed.')
        call dein#util#_error('cmdline: ' . string(job.commands))
        let ret = 1
      endif

      call remove(jobs, i)

      break
    endfor
  endwhile

  return ret
endfunction
function! dein#install#_copy_directories_py(srcs, dest) abort
  py3 << EOF
import shutil
import vim
for src in vim.eval('a:srcs'):
  shutil.copytree(src, vim.eval('a:dest'),
                  dirs_exist_ok=True,
                  ignore=shutil.ignore_patterns('.git'))
EOF
endfunction
function! dein#install#_python_version_check() abort
  python3 << EOF
import vim
import sys
vim.vars['dein#_python_version_check'] = (
    sys.version_info.major,
    sys.version_info.minor,
    sys.version_info.micro) >= (3, 8, 0)
EOF
  return get(g:, 'dein#_python_version_check', 0)
endfunction

function! s:install_blocking(context) abort
  try
    while 1
      call s:check_loop(a:context)

      if empty(a:context.processes)
            \ && a:context.number == a:context.max_plugins
        break
      endif
    endwhile
  finally
    call s:done(a:context)
  endtry

  return len(a:context.errored_plugins)
endfunction
function! s:install_async(context) abort
  if empty(a:context)
    return
  endif

  call s:check_loop(a:context)

  if empty(a:context.processes)
        \ && a:context.number == a:context.max_plugins
    call s:done(a:context)
  elseif a:context.number != a:context.prev_number
        \ && a:context.number < len(a:context.plugins)
    let plugin = a:context.plugins[a:context.number]
    call s:print_progress_message(
          \ s:get_progress_message(plugin.name,
          \   a:context.number, a:context.max_plugins))
    let a:context.prev_number = a:context.number
  endif

  return len(a:context.errored_plugins)
endfunction
function! s:check_loop(context) abort
  while a:context.number < a:context.max_plugins
        \ && len(a:context.processes) < g:dein#install_max_processes

    let plugin = a:context.plugins[a:context.number]
    call s:sync(plugin, a:context)

    if !a:context.async
      call s:print_progress_message(
            \ s:get_progress_message(plugin.name,
            \   a:context.number, a:context.max_plugins))
    endif
  endwhile

  for process in a:context.processes
    call s:check_output(a:context, process)
  endfor

  " Filter eof processes.
  call filter(a:context.processes, { _, val -> !val.eof })
endfunction
function! s:restore_view(context) abort
  if a:context.progress_type ==# 'tabline'
    let &g:showtabline = a:context.showtabline
    let &g:tabline = a:context.tabline
  elseif a:context.progress_type ==# 'title'
    let &g:title = a:context.title
    let &g:titlestring = a:context.titlestring
  endif
endfunction
function! s:init_context(plugins, update_type, async) abort
  let context = {}
  let context.update_type = a:update_type
  let context.async = a:async
  let context.synced_plugins = []
  let context.errored_plugins = []
  let context.processes = []
  let context.number = 0
  let context.prev_number = -1
  let context.plugins = a:plugins
  let context.max_plugins = len(context.plugins)
  let context.progress_type = (has('vim_starting')
        \ && g:dein#install_progress_type !=# 'none') ?
        \ 'echo' : g:dein#install_progress_type
  if !has('nvim') && context.progress_type ==# 'title'
    let context.progress_type = 'echo'
  endif
  let context.message_type = (has('vim_starting')
        \ && g:dein#install_message_type !=# 'none') ?
        \ 'echo' : g:dein#install_message_type
  let context.laststatus = &g:laststatus
  let context.showtabline = &g:showtabline
  let context.tabline = &g:tabline
  let context.title = &g:title
  let context.titlestring = &g:titlestring
  return context
endfunction
function! s:init_variables(context) abort
  let s:progress = ''
  let s:global_context = a:context
  let s:log = []
  let s:updates_log = []
endfunction
function! s:convert_args(args) abort
  let args = s:iconv(a:args, &encoding, 'char')
  if type(args) != v:t_list
    let args = split(&shell) + split(&shellcmdflag) + [args]
  endif
  return args
endfunction
function! s:start() abort
  call s:notify(strftime('Update started: (%Y/%m/%d %H:%M:%S)'))
endfunction
function! s:done(context) abort
  call s:restore_view(a:context)

  if !has('vim_starting')
    call s:notify(s:get_updated_message(a:context, a:context.synced_plugins))
    call s:notify(s:get_errored_message(a:context.errored_plugins))
  endif

  if !empty(a:context.synced_plugins)
    call dein#install#_recache_runtimepath()

    call dein#source(map(copy(a:context.synced_plugins),
          \ { _, val -> val.name }))

    " Execute done_update hooks
    let done_update_plugins = filter(copy(a:context.synced_plugins),
          \ { _, val -> has_key(val, 'hook_done_update') })
    if !empty(done_update_plugins)
      if has('vim_starting')
        let s:done_updated_plugins = done_update_plugins
        autocmd dein VimEnter * call s:call_done_update_hooks(
              \ s:done_updated_plugins)
      else
        " Reload plugins to execute hooks
        runtime! plugin/**/*.vim

        if has('nvim')
          " Neovim loads lua files at startup
          runtime! plugin/**/*.lua
        endif

        call s:call_done_update_hooks(done_update_plugins)
      endif
    endif
  endif

  redraw | echo ''

  call s:notify(strftime('Done: (%Y/%m/%d %H:%M:%S)'))

  " Disable installation handler
  let s:global_context = {}
  let s:progress = ''
  augroup dein-install
    autocmd!
  augroup END
  if exists('s:timer')
    call timer_stop(s:timer)
    unlet s:timer
  endif
endfunction
function! s:call_done_update_hooks(plugins) abort
  let cwd = getcwd()
  try
    for plugin in a:plugins
      call dein#install#_cd(plugin.path)
      call dein#call_hook('done_update', plugin)
    endfor
  finally
    call dein#install#_cd(cwd)
  endtry
endfunction

function! s:sync(plugin, context) abort
  let a:context.number += 1

  let num = a:context.number
  let max = a:context.max_plugins

  if isdirectory(a:plugin.path) && get(a:plugin, 'frozen', 0)
    " Skip frozen plugin
    call s:log(s:get_plugin_message(a:plugin, num, max, 'is frozen.'))
    return
  endif

  let [cmd, message] = s:get_sync_command(
        \   a:plugin, a:context.update_type,
        \   a:context.number, a:context.max_plugins)

  if empty(cmd)
    " Skip
    call s:log(s:get_plugin_message(a:plugin, num, max, message))
    return
  endif

  if type(cmd) == v:t_string && cmd =~# '^E: '
    " Errored.

    call s:print_progress_message(s:get_plugin_message(
          \ a:plugin, num, max, 'Error'))
    call s:error(cmd[3:])
    call add(a:context.errored_plugins,
          \ a:plugin)
    return
  endif

  if !a:context.async
    call s:print_progress_message(message)
  endif

  let process = s:init_process(a:plugin, a:context, cmd)
  if !empty(process)
    call add(a:context.processes, process)
  endif
endfunction
function! s:init_process(plugin, context, cmd) abort
  let process = {}

  let cwd = getcwd()
  let lang_save = $LANG
  let prompt_save = $GIT_TERMINAL_PROMPT
  try
    let $LANG = 'C'
    " Disable git prompt (git version >= 2.3.0)
    let $GIT_TERMINAL_PROMPT = 0

    call dein#install#_cd(a:plugin.path)

    let rev = s:get_revision_number(a:plugin)

    let process = {
          \ 'number': a:context.number,
          \ 'max_plugins': a:context.max_plugins,
          \ 'rev': rev,
          \ 'plugin': a:plugin,
          \ 'output': '',
          \ 'status': -1,
          \ 'eof': 0,
          \ 'installed': isdirectory(a:plugin.path),
          \ }

    let rev_save = get(a:plugin, 'rev', '')
    if isdirectory(a:plugin.path)
          \ && !get(a:plugin, 'local', 0)
          \ && rev_save !=# ''
      try
        " Force checkout HEAD revision.
        " The repository may be checked out.
        let a:plugin.rev = ''

        call s:lock_revision(process, a:context)
      finally
        let a:plugin.rev = rev_save
      endtry
    endif

    call s:init_job(process, a:context, a:cmd)
  finally
    let $LANG = lang_save
    let $GIT_TERMINAL_PROMPT = prompt_save
    call dein#install#_cd(cwd)
  endtry

  return process
endfunction
function! s:init_job(process, context, cmd) abort
  let a:process.start_time = localtime()

  if !a:context.async
    let a:process.output = dein#install#_system(a:cmd)
    let a:process.status = dein#install#_status()
    return
  endif

  let a:process.async = {'eof': 0}
  function! a:process.async.job_handler(data) abort
    if !has_key(self, 'candidates')
      let self.candidates = []
    endif
    let candidates = self.candidates
    if empty(candidates)
      call add(candidates, a:data[0])
    else
      let candidates[-1] .= a:data[0]
    endif

    call s:print_progress_message(candidates[-1])

    let candidates += a:data[1:]
  endfunction

  function! a:process.async.on_exit(exitval) abort
    let self.exitval = a:exitval
  endfunction

  function! a:process.async.get(process) abort
    " Check job status
    let status = -1
    if has_key(a:process.job, 'exitval')
      let self.eof = 1
      let status = a:process.job.exitval
    endif

    let candidates = get(a:process.job, 'candidates', [])
    let output = join((self.eof ? candidates : candidates[: -2]), "\n")
    if output !=# '' && a:process.output !=# output
      let a:process.output = output
      let a:process.start_time = localtime()
    endif
    let self.candidates = self.eof ? [] : candidates[-1:]

    let is_timeout = (localtime() - a:process.start_time)
          \             >= get(a:process.plugin, 'timeout',
          \                    g:dein#install_process_timeout)

    if self.eof
      let is_timeout = 0
      let is_skip = 0
    else
      let is_skip = 1
    endif

    if is_timeout
      call a:process.job.stop()
      let status = -1
    endif

    return [is_timeout, is_skip, status]
  endfunction

  let a:process.job = s:get_job().start(
        \ s:convert_args(a:cmd), {
        \   'on_stdout': a:process.async.job_handler,
        \   'on_stderr': a:process.async.job_handler,
        \   'on_exit': a:process.async.on_exit,
        \ })
  let a:process.id = a:process.job.pid()
  let a:process.job.candidates = []
endfunction
function! s:check_output(context, process) abort
  if a:context.async
    let [is_timeout, is_skip, status] = a:process.async.get(a:process)
  else
    let [is_timeout, is_skip, status] = [0, 0, a:process.status]
  endif

  if is_skip && !is_timeout
    return
  endif

  let num = a:process.number
  let max = a:context.max_plugins
  let plugin = a:process.plugin

  if isdirectory(plugin.path)
       \ && get(plugin, 'rev', '') !=# ''
       \ && !get(plugin, 'local', 0)
    " Restore revision.
    call s:lock_revision(a:process, a:context)
  endif

  let new_rev = s:get_revision_number(plugin)

  if is_timeout || status
    call s:log(s:get_plugin_message(plugin, num, max, 'Error'))
    call s:error(plugin.path)
    if !a:process.installed
      if !isdirectory(plugin.path)
        call s:error('Maybe wrong username or repository.')
      elseif isdirectory(plugin.path)
        call s:error('Remove the installed directory:' . plugin.path)
        call dein#install#_rm(plugin.path)
      endif
    endif

    call s:error((is_timeout ?
          \    strftime('Process timeout: (%Y/%m/%d %H:%M:%S)') :
          \    split(a:process.output, '\n')
          \ ))

    call add(a:context.errored_plugins,
          \ plugin)
  elseif a:process.rev ==# new_rev
    call s:log(s:get_plugin_message(
          \ plugin, num, max, 'Same revision'))
  else
    call s:log(s:get_plugin_message(plugin, num, max, 'Updated'))

    let log_messages = split(s:get_updated_log_message(
          \   plugin, new_rev, a:process.rev), '\n')
    let plugin.commit_count = len(log_messages)
    call s:log(map(log_messages,
          \   { _, val -> s:get_short_message(plugin, num, max, val) }))

    let plugin.old_rev = a:process.rev
    let plugin.new_rev = new_rev

    " Execute "post_update" before "build"
    if has_key(plugin, 'hook_post_update')
      " To load plugin is needed to execute "post_update"
      call dein#source(plugin.name)
      call dein#call_hook('post_update', plugin)
    endif

    let type = dein#util#_get_type(plugin.type)
    let plugin.uri = has_key(type, 'get_uri') ?
          \ type.get_uri(plugin.repo, plugin) : ''

    if dein#install#_build([plugin.name])
      call s:log(s:get_plugin_message(plugin, num, max, 'Build failed'))
      call s:error(plugin.path)
      " Remove.
      call add(a:context.errored_plugins, plugin)
    else
      call add(a:context.synced_plugins, plugin)
    endif
  endif

  let a:process.eof = 1
endfunction

function! s:iconv(expr, from, to) abort
  if a:from ==# '' || a:to ==# '' || a:from ==? a:to
    return a:expr
  endif

  if type(a:expr) == v:t_list
    return map(copy(a:expr), { _, val -> iconv(val, a:from, a:to) })
  else
    let result = iconv(a:expr, a:from, a:to)
    return result !=# '' ? result : a:expr
  endif
endfunction
function! s:print_progress_message(msg) abort
  let msg = dein#util#_convert2list(a:msg)
  let context = s:global_context
  if empty(msg) || empty(context)
    return
  endif

  let progress_type = context.progress_type
  if progress_type ==# 'tabline'
    set showtabline=2
    let &g:tabline = join(msg, "\n")
  elseif progress_type ==# 'title'
    set title
    let &g:titlestring = join(msg, "\n")
  elseif progress_type ==# 'echo'
    call s:echo(msg, 'echo')
  endif

  call s:log(msg)

  let s:progress = join(msg, "\n")
endfunction
function! s:error(msg) abort
  let msg = dein#util#_convert2list(a:msg)
  if empty(msg)
    return
  endif

  call s:echo(msg, 'error')

  call s:updates_log(msg)
endfunction
function! s:notify(msg) abort
  let msg = dein#util#_convert2list(a:msg)
  let context = s:global_context
  if empty(msg) || empty(context)
    return
  endif

  if context.message_type ==# 'echo'
    call dein#util#_notify(a:msg)
  endif

  call s:updates_log(msg)
  let s:progress = join(msg, "\n")
endfunction
function! s:updates_log(msg) abort
  let msg = dein#util#_convert2list(a:msg)

  let s:updates_log += msg
  call s:log(msg)
endfunction
function! s:log(msg) abort
  let msg = dein#util#_convert2list(a:msg)
  let s:log += msg
  call s:append_log_file(msg)
endfunction
function! s:append_log_file(msg) abort
  let logfile = dein#util#_expand(g:dein#install_log_filename)
  if logfile ==# ''
    return
  endif

  let msg = a:msg
  " Appends to log file.
  if filereadable(logfile)
    let msg = readfile(logfile) + msg
  endif

  call dein#util#_safe_writefile(msg, logfile)
endfunction


function! s:echo(expr, mode) abort
  let msg = map(filter(dein#util#_convert2list(a:expr),
        \ { _, val -> val !=# '' }), { _, val -> '[dein] ' .  val })
  if empty(msg)
    return
  endif

  let more_save = &more
  let showcmd_save = &showcmd
  let ruler_save = &ruler
  try
    set nomore
    set noshowcmd
    set noruler

    let height = max([1, &cmdheight])
    echo ''
    for i in range(0, len(msg)-1, height)
      redraw

      let m = join(msg[i : i+height-1], "\n")
      call s:echo_mode(m, a:mode)
      if has('vim_starting')
        echo ''
      endif
    endfor
  finally
    let &more = more_save
    let &showcmd = showcmd_save
    let &ruler = ruler_save
  endtry
endfunction
function! s:echo_mode(m, mode) abort
  for m in split(a:m, '\r\?\n', 1)
    if !has('vim_starting') && a:mode !=# 'error'
      let m = s:truncate_skipping(m, &columns - 1, &columns/3, '...')
    endif

    if a:mode ==# 'error'
      echohl WarningMsg | echomsg m | echohl None
    elseif a:mode ==# 'echomsg'
      echomsg m
    else
      echo m
    endif
  endfor
endfunction

function! s:truncate_skipping(str, max, footer_width, separator) abort
  let width = strwidth(a:str)
  if width <= a:max
    let ret = a:str
  else
    let header_width = a:max - strwidth(a:separator) - a:footer_width
    let ret = s:strwidthpart(a:str, header_width) . a:separator
          \ . s:strwidthpart_reverse(a:str, a:footer_width)
  endif

  return ret
endfunction
function! s:strwidthpart(str, width) abort
  if a:width <= 0
    return ''
  endif
  let ret = a:str
  let width = strwidth(a:str)
  while width > a:width
    let char = matchstr(ret, '.$')
    let ret = ret[: -1 - len(char)]
    let width -= strwidth(char)
  endwhile

  return ret
endfunction
function! s:strwidthpart_reverse(str, width) abort
  if a:width <= 0
    return ''
  endif
  let ret = a:str
  let width = strwidth(a:str)
  while width > a:width
    let char = matchstr(ret, '^.')
    let ret = ret[len(char) :]
    let width -= strwidth(char)
  endwhile

  return ret
endfunction