" ---------------------------------------------------------------------------
" Try to clone all new bundles given (or all bundles in g:vundle#bundles by
" default) to g:vundle#bundle_dir.  If a:bang is 1 it will also update all
" plugins (git pull).
"
" bang   -- 1 or 0
" ...    -- any number of bundle specifications (separate arguments)
" ---------------------------------------------------------------------------
func! vundle#installer#new(bang, ...) abort
  " No specific plugins are specified. Operate on all plugins.
  if a:0 == 0
    let bundles = g:vundle#bundles
  " Specific plugins are specified for update. Update them.
  elseif (a:bang)
    let bundles = filter(copy(g:vundle#bundles), 'index(a:000, v:val.name) > -1')
  " Specific plugins are specified for installation. Install them.
  else
    let bundles = map(copy(a:000), 'vundle#config#bundle(v:val, {})')
  endif

  if empty(bundles)
    echoerr 'No bundles were selected for operation'
    return
  endif

  let names = vundle#scripts#bundle_names(map(copy(bundles), 'v:val.name_spec'))
  call vundle#scripts#view('Installer',['" Installing plugins to '.expand(g:vundle#bundle_dir, 1)], names +  ['Helptags'])

  " This calls 'add' as a normal mode command. This is a buffer local mapping
  " defined in vundle#scripts#view(). The mapping will call a buffer local
  " command InstallPlugin which in turn will call vundle#installer#run() with
  " vundle#installer#install().
  call s:process(a:bang, (a:bang ? 'add!' : 'add'))

  call vundle#config#require(bundles)
endf


" ---------------------------------------------------------------------------
" Iterate over all lines in a Vundle window and execute the given command for
" every line.  Used by the installation and cleaning functions.
"
" bang   -- not used (FIXME)
" cmd    -- the (normal mode) command to execute for every line as a string
" ---------------------------------------------------------------------------
func! s:process(bang, cmd)
  let msg = ''

  redraw
  sleep 1m

  let lines = (getline('.','$')[0:-2])

  for line in lines
    redraw

    exec ':norm '.a:cmd

    if 'error' == s:last_status
      let msg = 'With errors; press l to view log'
    endif

    if 'updated' == s:last_status && empty(msg)
      let msg = 'Plugins updated; press u to view changelog'
    endif

    " goto next one
    exec ':+1'

    setl nomodified
  endfor

  redraw
  echo 'Done! '.msg
endf


" ---------------------------------------------------------------------------
" Call another function in the different Vundle windows.
"
" func_name -- the function to call
" name      -- the bundle name to call func_name for (string)
" ...       -- the argument to be used when calling func_name (only the first
"              optional argument will be used)
" return    -- the status returned by the call to func_name
" ---------------------------------------------------------------------------
func! vundle#installer#run(func_name, name, ...) abort
  let n = a:name

  echo 'Processing '.n
  call s:sign('active')

  sleep 1m

  let status = call(a:func_name, a:1)

  call s:sign(status)

  redraw

  if 'new' == status
    echo n.' installed'
  elseif 'updated' == status
    echo n.' updated'
  elseif 'todate' == status
    echo n.' already installed'
  elseif 'deleted' == status
    echo n.' deleted'
  elseif 'helptags' == status
    echo n.' regenerated'
  elseif 'pinned' == status
    echo n.' pinned'
  elseif 'error' == status
    echohl Error
    echo 'Error processing '.n
    echohl None
    sleep 1
  else
    throw 'whoops, unknown status:'.status
  endif

  let s:last_status = status

  return status
endf


" ---------------------------------------------------------------------------
" Put a sign on the current line, indicating the status of the installation
" step.
"
" status -- string describing the status
" ---------------------------------------------------------------------------
func! s:sign(status)
  if (!has('signs'))
    return
  endif

  exe ":sign place ".line('.')." line=".line('.')." name=Vu_". a:status ." buffer=" . bufnr("%")
endf


" ---------------------------------------------------------------------------
" Install a plugin, then add it to the runtimepath and source it.
"
" bang   -- 1 or 0, passed directly to vundle#installer#install()
" name   -- the name of a bundle (string)
" return -- the return value from vundle#installer#install()
" ---------------------------------------------------------------------------
func! vundle#installer#install_and_require(bang, name) abort
  let result = vundle#installer#install(a:bang, a:name)
  let b = vundle#config#bundle(a:name, {})
  call vundle#installer#helptags([b])
  call vundle#config#require([b])
  return result
endf


" ---------------------------------------------------------------------------
" Install or update a bundle given by its name.
"
" bang   -- 1 or 0, passed directly to s:sync()
" name   -- the name of a bundle (string)
" return -- the return value from s:sync()
" ---------------------------------------------------------------------------
func! vundle#installer#install(bang, name) abort
  if !isdirectory(g:vundle#bundle_dir) | call mkdir(g:vundle#bundle_dir, 'p') | endif

  let n = substitute(a:name,"['".'"]\+','','g')
  let matched = filter(copy(g:vundle#bundles), 'v:val.name_spec == n')

  if len(matched) > 0
    let b = matched[0]
  else
    let b = vundle#config#init_bundle(a:name, {})
  endif

  return s:sync(a:bang, b)
endf


" ---------------------------------------------------------------------------
" Call :helptags for all bundles in g:vundle#bundles.
"
" return -- 'error' if an error occurred, else return 'helptags'
" ---------------------------------------------------------------------------
func! vundle#installer#docs() abort
  let error_count = vundle#installer#helptags(g:vundle#bundles)
  if error_count > 0
      return 'error'
  endif
  return 'helptags'
endf


" ---------------------------------------------------------------------------
" Call :helptags for a list of bundles.
"
" bundles -- a list of bundle dictionaries for which :helptags should be
"            called.
" return  -- the number of directories where :helptags failed
" ---------------------------------------------------------------------------
func! vundle#installer#helptags(bundles) abort
  let bundle_dirs = map(copy(a:bundles),'v:val.rtpath')
  let help_dirs = filter(bundle_dirs, 's:has_doc(v:val)')

  call s:log('')
  call s:log('Helptags:')

  let statuses = map(copy(help_dirs), 's:helptags(v:val)')
  let errors = filter(statuses, 'v:val == 0')

  call s:log('Helptags: '.len(help_dirs).' plugins processed')

  return len(errors)
endf


" ---------------------------------------------------------------------------
" List all installed plugins.
" Corresponding documentation: vundle-plugins-list
"
" bang   -- not used
" ---------------------------------------------------------------------------
func! vundle#installer#list(bang) abort
  let bundles = vundle#scripts#bundle_names(map(copy(g:vundle#bundles), 'v:val.name_spec'))
  call vundle#scripts#view('list', ['" My Plugins'], bundles)
  redraw
  echo len(g:vundle#bundles).' plugins configured'
endf


" ---------------------------------------------------------------------------
" List and remove all directories in the bundle directory which are not
" activated (added to the bundle list).
"
" bang   -- 0 if the user should be asked to confirm every deletion, 1 if they
"           should be removed unconditionally
" ---------------------------------------------------------------------------
func! vundle#installer#clean(bang) abort
  let bundle_dirs = map(copy(g:vundle#bundles), 'v:val.path()')
  let all_dirs = (v:version > 702 || (v:version == 702 && has("patch51")))
  \   ? split(globpath(g:vundle#bundle_dir, '*', 1), "\n")
  \   : split(globpath(g:vundle#bundle_dir, '*'), "\n")
  let x_dirs = filter(all_dirs, '0 > index(bundle_dirs, v:val)')

  if empty(x_dirs)
    let headers = ['" All clean!']
    let names = []
  else
    let headers = ['" Removing Plugins:']
    let names = vundle#scripts#bundle_names(map(copy(x_dirs), 'fnamemodify(v:val, ":t")'))
  end

  call vundle#scripts#view('clean', headers, names)
  redraw

  if (a:bang || empty(names))
    call s:process(a:bang, 'D')
  else
    call inputsave()
    let response = input('Continue? [Y/n]: ')
    call inputrestore()
    if (response =~? 'y' || response == '')
      call s:process(a:bang, 'D')
    endif
  endif
endf


" ---------------------------------------------------------------------------
" Delete to directory for a plugin.
"
" bang     -- not used
" dir_name -- the bundle directory to be deleted (as a string)
" return   -- 'error' if an error occurred, 'deleted' if the plugin folder was
"             successfully deleted
" ---------------------------------------------------------------------------
func! vundle#installer#delete(bang, dir_name) abort

  let cmd = ((has('win32') || has('win64')) && empty(matchstr(&shell, 'sh'))) ?
  \           'rmdir /S /Q' :
  \           'rm -rf'

  let bundle = vundle#config#init_bundle(a:dir_name, {})
  let cmd .= ' '.vundle#installer#shellesc(bundle.path())

  let out = s:system(cmd)

  call s:log('')
  call s:log('Plugin '.a:dir_name)
  call s:log(cmd, '$ ')
  call s:log(out, '> ')

  if 0 != v:shell_error
    return 'error'
  else
    return 'deleted'
  endif
endf


" ---------------------------------------------------------------------------
" Check if a bundled plugin has any documentation.
"
" rtp    -- a path (string) where the plugin is installed
" return -- 1 if some documentation was found, 0 otherwise
" ---------------------------------------------------------------------------
func! s:has_doc(rtp) abort
  return isdirectory(a:rtp.'/doc')
  \   && (!filereadable(a:rtp.'/doc/tags') || filewritable(a:rtp.'/doc/tags'))
  \   && (v:version > 702 || (v:version == 702 && has("patch51")))
  \     ? !(empty(glob(a:rtp.'/doc/*.txt', 1)) && empty(glob(a:rtp.'/doc/*.??x', 1)))
  \     : !(empty(glob(a:rtp.'/doc/*.txt')) && empty(glob(a:rtp.'/doc/*.??x')))
endf


" ---------------------------------------------------------------------------
" Update the helptags for a plugin.
"
" rtp    -- the path to the plugin's root directory (string)
" return -- 1 if :helptags succeeded, 0 otherwise
" ---------------------------------------------------------------------------
func! s:helptags(rtp) abort
  " it is important to keep trailing slash here
  let doc_path = resolve(a:rtp . '/doc/')
  call s:log(':helptags '.doc_path)
  try
    execute 'helptags ' . doc_path
  catch
    call s:log("> Error running :helptags ".doc_path)
    return 0
  endtry
  return 1
endf


" ---------------------------------------------------------------------------
" Get the URL for the remote called 'origin' on the repository that
" corresponds to a given bundle.
"
" bundle -- a bundle object to check the repository for
" return -- the URL for the origin remote (string)
" ---------------------------------------------------------------------------
func! s:get_current_origin_url(bundle) abort
  let cmd = 'cd '.vundle#installer#shellesc(a:bundle.path()).' && git config --get remote.origin.url'
  let cmd = vundle#installer#shellesc_cd(cmd)
  let out = s:strip(s:system(cmd))
  return out
endf


" ---------------------------------------------------------------------------
" Get a short sha of the HEAD of the repository for a given bundle
"
" bundle -- a bundle object
" return -- A 15 character log sha for the current HEAD
" ---------------------------------------------------------------------------
func! s:get_current_sha(bundle)
  let cmd = 'cd '.vundle#installer#shellesc(a:bundle.path()).' && git rev-parse HEAD'
  let cmd = vundle#installer#shellesc_cd(cmd)
  let out = s:system(cmd)[0:15]
  return out
endf


" ---------------------------------------------------------------------------
" Create the appropriate sync command to run according to the current state of
" the local repository (clone, pull, reset, etc).
"
" In the case of a pull (update), also return the current sha, so that we can
" later check that there has been an upgrade.
"
" bang   -- 0 if only new plugins should be installed, 1 if existing plugins
"           should be updated
" bundle -- a bundle object to create the sync command for
" return -- A list containing the command to run and the sha for the current
"           HEAD
" ---------------------------------------------------------------------------
func! s:make_sync_command(bang, bundle) abort
  let git_dir = expand(a:bundle.path().'/.git/', 1)
  if isdirectory(git_dir) || filereadable(expand(a:bundle.path().'/.git', 1))

    let current_origin_url = s:get_current_origin_url(a:bundle)
    if current_origin_url != a:bundle.uri
      call s:log('Plugin URI change detected for Plugin ' . a:bundle.name)
      call s:log('>  Plugin ' . a:bundle.name . ' old URI: ' . current_origin_url)
      call s:log('>  Plugin ' . a:bundle.name . ' new URI: ' . a:bundle.uri)
      " Directory names match but the origin remotes are not the same
      let cmd_parts = [
                  \ 'cd '.vundle#installer#shellesc(a:bundle.path()) ,
                  \ 'git remote set-url origin ' . vundle#installer#shellesc(a:bundle.uri),
                  \ 'git fetch',
                  \ 'git reset --hard origin/HEAD',
                  \ 'git submodule update --init --recursive',
                  \ ]
      let cmd = join(cmd_parts, ' && ')
      let cmd = vundle#installer#shellesc_cd(cmd)
      let initial_sha = ''
      return [cmd, initial_sha]
    endif

    if !(a:bang)
      " The repo exists, and no !, so leave as it is.
      return ['', '']
    endif

    let cmd_parts = [
                \ 'cd '.vundle#installer#shellesc(a:bundle.path()),
                \ 'git pull',
                \ 'git submodule update --init --recursive',
                \ ]
    let cmd = join(cmd_parts, ' && ')
    let cmd = vundle#installer#shellesc_cd(cmd)

    let initial_sha = s:get_current_sha(a:bundle)
  else
    let cmd = 'git clone --recursive '.vundle#installer#shellesc(a:bundle.uri).' '.vundle#installer#shellesc(a:bundle.path())
    let initial_sha = ''
  endif
  return [cmd, initial_sha]
endf


" ---------------------------------------------------------------------------
" Install or update a given bundle object with git.
"
" bang   -- 0 if only new plugins should be installed, 1 if existing plugins
"           should be updated
" bundle -- a bundle object (dictionary)
" return -- a string indicating the status of the bundle installation:
"            - todate  : Nothing was updated or the repository was up to date
"            - new     : The plugin was newly installed
"            - updated : Some changes where pulled via git
"            - error   : An error occurred in the shell command
"            - pinned  : The bundle is marked as pinned
" ---------------------------------------------------------------------------
func! s:sync(bang, bundle) abort
  " Do not sync if this bundle is pinned
  if a:bundle.is_pinned()
    return 'pinned'
  endif

  let [ cmd, initial_sha ] = s:make_sync_command(a:bang, a:bundle)
  if empty(cmd)
      return 'todate'
  endif

  let out = s:system(cmd)
  call s:log('')
  call s:log('Plugin '.a:bundle.name_spec)
  call s:log(cmd, '$ ')
  call s:log(out, '> ')

  if 0 != v:shell_error
    return 'error'
  end

  if empty(initial_sha)
    return 'new'
  endif

  let updated_sha = s:get_current_sha(a:bundle)

  if initial_sha == updated_sha
    return 'todate'
  endif

  call add(g:vundle#updated_bundles, [initial_sha, updated_sha, a:bundle])
  return 'updated'
endf


" ---------------------------------------------------------------------------
" Escape special characters in a string to be able to use it as a shell
" command with system().
"
" cmd    -- the string holding the shell command
" return -- a string with the relevant characters escaped
" ---------------------------------------------------------------------------
func! vundle#installer#shellesc(cmd) abort
  if ((has('win32') || has('win64')) && empty(matchstr(&shell, 'sh')))
    return '"' . substitute(a:cmd, '"', '\\"', 'g') . '"'
  endif
  return shellescape(a:cmd)
endf


" ---------------------------------------------------------------------------
" Fix a cd shell command to be used on Windows.
"
" cmd    -- the command to be fixed (string)
" return -- the fixed command (string)
" ---------------------------------------------------------------------------
func! vundle#installer#shellesc_cd(cmd) abort
  if ((has('win32') || has('win64')) && empty(matchstr(&shell, 'sh')))
    let cmd = substitute(a:cmd, '^cd ','cd /d ','')  " add /d switch to change drives
    return cmd
  else
    return a:cmd
  endif
endf


" ---------------------------------------------------------------------------
" Make a system call.  This can be used to change the way system calls
" are made during developing, without searching the whole code base for
" actual system() calls.
"
" cmd    -- the command passed to system() (string)
" return -- the return value from system()
" ---------------------------------------------------------------------------
func! s:system(cmd) abort
  return system(a:cmd)
endf


" ---------------------------------------------------------------------------
" Add a log message to Vundle's internal logging variable.
"
" str    -- the log message (string)
" prefix -- optional prefix for multi-line entries (string)
" return -- a:str
" ---------------------------------------------------------------------------
func! s:log(str, ...) abort
  let prefix = a:0 > 0 ? a:1 : ''
  let fmt = '%Y-%m-%d %H:%M:%S'
  let lines = split(a:str, '\n', 1)
  let time = strftime(fmt)
  for line in lines
      call add(g:vundle#log, '['. time .'] '. prefix . line)
  endfor
  return a:str
endf


" ---------------------------------------------------------------------------
" Remove leading and trailing whitespace from a string
"
" str    -- The string to rid of trailing and leading spaces
" return -- A string stripped of side spaces
" ---------------------------------------------------------------------------
func! s:strip(str)
  return substitute(a:str, '\%^\_s*\(.\{-}\)\_s*\%$', '\1', '')
endf

" vim: set expandtab sts=2 ts=2 sw=2 tw=78 norl:
