" vim-plug: Vim plugin manager
" ============================
"
" Download plug.vim and put it in ~/.vim/autoload
"
"   mkdir -p ~/.vim/autoload
"   curl -fLo ~/.vim/autoload/plug.vim \
"     https://raw.github.com/junegunn/vim-plug/master/plug.vim
"
" Edit your .vimrc
"
"   call plug#begin()
"
"   Plug 'junegunn/seoul256'
"   Plug 'junegunn/vim-easy-align'
"   " Plug 'user/repo1', 'branch_or_tag'
"   " Plug 'user/repo2', { 'rtp': 'vim/plugin/dir', 'branch': 'devel' }
"   " ...
"
"   call plug#end()
"
" Then :PlugInstall to install plugins. (default: ~/.vim/plugged)
" You can change the location of the plugins with plug#begin(path) call.
"
"
" Copyright (c) 2013 Junegunn Choi
"
" MIT License
"
" Permission is hereby granted, free of charge, to any person obtaining
" a copy of this software and associated documentation files (the
" "Software"), to deal in the Software without restriction, including
" without limitation the rights to use, copy, modify, merge, publish,
" distribute, sublicense, and/or sell copies of the Software, and to
" permit persons to whom the Software is furnished to do so, subject to
" the following conditions:
"
" The above copyright notice and this permission notice shall be
" included in all copies or substantial portions of the Software.
"
" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

if exists('g:loaded_plug')
  finish
endif
let g:loaded_plug = 1

let s:plug_source = 'https://raw.github.com/junegunn/vim-plug/master/plug.vim'
let s:plug_file = 'Plugfile'
let s:plug_win = 0
let s:is_win = has('win32') || has('win64')
let s:me = expand('<sfile>:p')

function! plug#begin(...)
  let home = a:0 > 0 ? fnamemodify(a:1, ':p') :
        \ get(g:, 'plug_home', split(&rtp, ',')[0].'/plugged')
  if !isdirectory(home)
    try
      call mkdir(home, 'p')
    catch
      echoerr 'Invalid plug directory: '. home
      return 0
    endtry
  endif
  if !executable('git')
    echoerr "`git' executable not found. vim-plug requires git."
    return 0
  endif

  let g:plug_home = home
  let g:plugs = {}

  command! -nargs=+ Plug        call s:add(1, <args>)
  command! -nargs=* PlugInstall call s:install(<f-args>)
  command! -nargs=* PlugUpdate  call s:update(<f-args>)
  command! -nargs=0 -bang PlugClean call s:clean('<bang>' == '!')
  command! -nargs=0 PlugUpgrade if s:upgrade() | execute "source ". s:me | endif
  command! -nargs=0 PlugStatus  call s:status()

  return 1
endfunction

function! plug#end()
  let keys = keys(g:plugs)
  while !empty(keys)
    let keys = keys(s:extend(keys))
  endwhile

  set nocompatible
  filetype off
  for plug in values(g:plugs)
    let rtp = s:rtp(plug)
    execute "set rtp^=".rtp
    if isdirectory(rtp.'after')
      execute "set rtp+=".rtp.'after'
    endif
  endfor
  filetype plugin indent on
  syntax on
endfunction

function! s:rtp(spec)
  return s:dirpath(a:spec.dir . get(a:spec, 'rtp', ''))
endfunction

function! s:add(...)
  let force = a:1
  let opts = { 'branch': 'master' }
  if a:0 == 2
    let plugin = a:2
  elseif a:0 == 3
    let plugin = a:2
    if type(a:3) == 1
      let opts.branch = a:3
    elseif type(a:3) == 4
      call extend(opts, a:3)
    else
      echoerr "Invalid argument type (expected: string or dictionary)"
      return
    endif
  else
    echoerr "Invalid number of arguments (1..2)"
    return
  endif

  if plugin =~ ':'
    let uri = plugin
  else
    if plugin !~ '/'
      let plugin = 'vim-scripts/'. plugin
    endif
    let uri = 'https://git:@github.com/' . plugin . '.git'
  endif

  let name = substitute(split(plugin, '/')[-1], '\.git$', '', '')
  if !force && has_key(g:plugs, name) | return | endif

  let dir  = s:dirpath( fnamemodify(join([g:plug_home, name], '/'), ':p') )
  let spec = extend(opts, { 'dir': dir, 'uri': uri })
  let g:plugs[name] = spec
endfunction

function! s:install(...)
  call s:update_impl(0, a:000)
endfunction

function! s:update(...)
  call s:update_impl(1, a:000)
endfunction

function! s:apply()
  for spec in values(g:plugs)
    let docd = join([spec.dir, 'doc'], '/')
    if isdirectory(docd)
      execute "helptags ". join([spec.dir, 'doc'], '/')
    endif
  endfor
  runtime! plugin/*.vim
  runtime! after/*.vim
  silent! source $MYVIMRC
endfunction

function! s:syntax()
  syntax clear
  syntax region plug1 start=/\%1l/ end=/\%2l/ contains=ALL
  syntax region plug2 start=/\%2l/ end=/\%3l/ contains=ALL
  syn match plugNumber /[0-9]\+[0-9.]*/ containedin=plug1 contained
  syn match plugBracket /[[\]]/ containedin=plug2 contained
  syn match plugDash /^-/
  syn match plugName /\(^- \)\@<=[^:]*/
  syn match plugError /^- [^:]\+: (x).*/
  hi def link plug1       Title
  hi def link plug2       Repeat
  hi def link plugBracket Structure
  hi def link plugNumber  Number
  hi def link plugDash    Special
  hi def link plugName    Label
  hi def link plugError   Error
endfunction

function! s:lpad(str, len)
  return a:str . repeat(' ', a:len - len(a:str))
endfunction

function! s:system(cmd)
  let lines = split(system(a:cmd), '\n')
  return get(lines, -1, '')
endfunction

function! s:prepare()
  execute s:plug_win . 'wincmd w'
  if exists('b:plug')
    %d
  else
    vertical topleft new
    noremap <silent> <buffer> q :q<cr>
    let b:plug = 1
    let s:plug_win = winnr()
    call s:assign_name()
  endif
  setlocal buftype=nofile bufhidden=wipe nobuflisted noswapfile nowrap cursorline
  setf vim-plug
  call s:syntax()
endfunction

function! s:assign_name()
  " Assign buffer name
  let prefix = '[Plugins]'
  let name   = prefix
  let idx    = 2
  while bufexists(name)
    let name = printf("%s (%s)", prefix, idx)
    let idx = idx + 1
  endwhile
  silent! execute "f ".fnameescape(name)
endfunction

function! s:finish()
  call append(line('$'), '')
  call append(line('$'), 'Finishing ... ')
  redraw
  call s:apply()
  call s:syntax()
  call setline(line('$'), getline(line('$')) . 'Done!')
  normal! G
endfunction

function! s:update_impl(pull, args)
  let threads = len(a:args) > 0 ? a:args[0] : get(g:, 'plug_threads', 16)

  call s:prepare()
  call append(0, a:pull ? 'Updating plugins' : 'Installing plugins')
  call append(1, '['. s:lpad('', len(g:plugs)) .']')
  normal! 2G
  redraw

  if has('ruby') && threads > 1
    call s:update_parallel(a:pull, threads)
  else
    call s:update_serial(a:pull)
  endif
  call s:finish()
endfunction

function! s:extend(names)
  let prev = copy(g:plugs)
  try
    command! -nargs=+ Plug call s:add(0, <args>)
    for name in a:names
      let plugfile = s:rtp(g:plugs[name]) . s:plug_file
      if filereadable(plugfile)
        execute "source ". plugfile
      endif
    endfor
  finally
    command! -nargs=+ Plug call s:add(1, <args>)
  endtry
  return filter(copy(g:plugs), '!has_key(prev, v:key)')
endfunction

function! s:update_progress(pull, cnt, total)
  call setline(1, (a:pull ? 'Updating' : 'Installing').
        \ " plugins (".a:cnt."/".a:total.")")
  call s:progress_bar(2, a:cnt, a:total)
  normal! 2G
  redraw
endfunction

function! s:update_serial(pull)
  let st    = reltime()
  let base  = g:plug_home
  let todo  = copy(g:plugs)
  let total = len(todo)
  let done  = {}

  while !empty(todo)
    for [name, spec] in items(todo)
      let done[name] = 1
      if isdirectory(spec.dir)
        execute 'cd '.spec.dir
        let [valid, msg] = s:git_valid(spec, 0)
        if valid
          let result = a:pull ?
            \ s:system(
            \ printf('git checkout -q %s 2>&1 && git pull origin %s 2>&1',
            \   spec.branch, spec.branch)) : 'Already installed'
          let error = a:pull ? v:shell_error != 0 : 0
        else
          let result = msg
          let error = 1
        endif
      else
        if !isdirectory(base)
          call mkdir(base, 'p')
        endif
        execute 'cd '.base
        let d = shellescape(substitute(spec.dir, '[\/]\+$', '', ''))
        let result = s:system(
              \ printf('git clone --recursive %s -b %s %s 2>&1',
              \ shellescape(spec.uri), shellescape(spec.branch), d))
        let error = v:shell_error != 0
      endif
      cd -
      if error
        let result = '(x) ' . result
      endif
      call append(3, '- ' . name . ': ' . result)
      call s:update_progress(a:pull, len(done), total)
    endfor

    if !empty(s:extend(keys(todo)))
      let todo = filter(copy(g:plugs), '!has_key(done, v:key)')
      let total += len(todo)
      call s:update_progress(a:pull, len(done), total)
    else
      break
    endif
  endwhile

  call setline(1, "Updated. Elapsed time: " . split(reltimestr(reltime(st)))[0] . ' sec.')
endfunction

function! s:update_parallel(pull, threads)
  ruby << EOF
  require 'thread'
  require 'fileutils'
  require 'timeout'
  st    = Time.now
  iswin = VIM::evaluate('s:is_win').to_i == 1
  cd    = iswin ? 'cd /d' : 'cd'
  pull  = VIM::evaluate('a:pull').to_i == 1
  base  = VIM::evaluate('g:plug_home')
  all   = VIM::evaluate('copy(g:plugs)')
  limit = VIM::evaluate('get(g:, "plug_timeout", 60)')
  done  = {}
  skip  = 'Already installed'
  mtx   = Mutex.new
  take1 = proc { mtx.synchronize { all.shift } }
  logh  = proc {
    cnt, tot = done.length, VIM::evaluate('len(g:plugs)')
    $curbuf[1] = "#{pull ? 'Updating' : 'Installing'} plugins (#{cnt}/#{tot})"
    $curbuf[2] = '[' + ('=' * cnt).ljust(tot) + ']'
    VIM::command('normal! 2G')
    VIM::command('redraw') unless iswin
  }
  log = proc { |name, result, ok|
    mtx.synchronize do
      done[name] = true
      result = '(x) ' + result unless ok
      result = "- #{name}: #{result}"
      $curbuf.append 3, result
      logh.call
    end
  }
  bt = proc { |cmd|
    begin
      Timeout::timeout(limit) do
        if iswin
          tmp = VIM::evaluate('tempname()')
          system("#{cmd} > #{tmp}")
          data = File.read(tmp).chomp
          File.unlink tmp rescue nil
        else
          data = `#{cmd}`.chomp
        end
        [$? == 0, data]
      end
    rescue Timeout::Error
      [false, "Timeout!"]
    end
  }

  until all.empty?
    names = all.keys
    [names.length, VIM::evaluate('a:threads').to_i].min.times.map { |i|
      Thread.new(i) do
        while pair = take1.call
          name = pair.first
          dir, uri, branch = pair.last.values_at *%w[dir uri branch]
          ok, result =
            if File.directory? dir
              ret, data = bt.call "#{cd} #{dir} && git rev-parse --abbrev-ref HEAD 2>&1 && git config remote.origin.url"
              current_uri = data.lines.to_a.last
              if ret && current_uri == uri
                if pull
                  bt.call "#{cd} #{dir} && git checkout -q #{branch} 2>&1 && git pull origin #{branch} 2>&1"
                else
                  [true, skip]
                end
              else
                [false, "PlugClean required. Invalid status."]
              end
            else
              FileUtils.mkdir_p(base)
              d = dir.sub(%r{[\\/]+$}, '')
              bt.call "#{cd} #{base} && git clone --recursive #{uri} -b #{branch} #{d} 2>&1"
            end
          result = result.lines.to_a.last
          log.call name, (result && result.strip), ok
        end
      end
    }.each(&:join)
    all.merge! VIM::evaluate("s:extend(#{names.inspect})")
    logh.call
  end
  $curbuf[1] = "Updated. Elapsed time: #{"%.6f" % (Time.now - st)} sec."
EOF
endfunction

function! s:path(path)
  return substitute(s:is_win ? substitute(a:path, '/', '\', 'g') : a:path,
        \ '[/\\]*$', '', '')
endfunction

function! s:dirpath(path)
  let path = s:path(a:path)
  if s:is_win
    return path !~ '\\$' ? path.'\' : path
  else
    return path !~ '/$' ? path.'/' : path
  endif
endfunction

function! s:glob_dir(path)
  return map(filter(split(globpath(a:path, '**'), '\n'), 'isdirectory(v:val)'), 's:dirpath(v:val)')
endfunction

function! s:progress_bar(line, cnt, total)
  call setline(a:line, '[' . s:lpad(repeat('=', a:cnt), a:total) . ']')
endfunction

function! s:git_valid(spec, cd)
  let ret = 1
  let msg = 'OK'
  if isdirectory(a:spec.dir)
    if a:cd | execute "cd " . a:spec.dir | endif
    let remote = s:system("git config remote.origin.url")

    if remote != a:spec.uri
      let msg = 'Invalid remote: ' . remote . '. Try PlugClean.'
      let ret = 0
    else
      let branch = s:system('git rev-parse --abbrev-ref HEAD')
      if v:shell_error != 0
        let msg = 'Invalid git repository. Try PlugClean.'
        let ret = 0
      elseif a:spec.branch != branch
        let msg = 'Invalid branch: '.branch.'. Try PlugUpdate.'
        let ret = 0
      endif
    endif
    if a:cd | cd - | endif
  else
    let msg = 'Not found'
    let ret = 0
  endif
  return [ret, msg]
endfunction

function! s:clean(force)
  call s:prepare()
  call append(0, 'Searching for unused plugins in '.g:plug_home)
  call append(1, '')

  " List of valid directories
  let dirs = []
  let [cnt, total] = [0, len(g:plugs)]
  for spec in values(g:plugs)
    if s:git_valid(spec, 1)[0]
      call add(dirs, spec.dir)
    endif
    let cnt += 1
    call s:progress_bar(2, cnt, total)
    redraw
  endfor

  let allowed = {}
  for dir in dirs
    let allowed[dir] = 1
    for child in s:glob_dir(dir)
      let allowed[child] = 1
    endfor
  endfor

  let todo = []
  let found = sort(s:glob_dir(g:plug_home))
  while !empty(found)
    let f = remove(found, 0)
    if !has_key(allowed, f) && isdirectory(f)
      call add(todo, f)
      call append(line('$'), '- ' . f)
      let found = filter(found, 'stridx(v:val, f) != 0')
    end
  endwhile

  normal! G
  redraw
  if empty(todo)
    call append(line('$'), 'Already clean.')
  else
    call inputsave()
    let yes = a:force || (input("Proceed? (Y/N) ") =~? '^y')
    call inputrestore()
    if yes
      for dir in todo
        if isdirectory(dir)
          call system((s:is_win ? 'rmdir /S /Q ' : 'rm -rf ') . dir)
        endif
      endfor
      call append(line('$'), 'Removed.')
    else
      call append(line('$'), 'Cancelled.')
    endif
  endif
  normal! G
endfunction

function! s:upgrade()
  if executable('curl')
    let mee = shellescape(s:me)
    let new = shellescape(s:me . '.new')
    echo "Downloading ". s:plug_source
    redraw
    let mv = s:is_win ? 'move /Y' : 'mv -f'
    call system(printf(
      \ "curl -fLo %s %s && ".mv." %s %s.old && ".mv." %s %s",
      \ new, s:plug_source, mee, mee, new, mee))
    if v:shell_error == 0
      unlet g:loaded_plug
      echo "Downloaded ". s:plug_source
      return 1
    else
      echoerr "Error upgrading vim-plug"
      return 0
    endif
  else
    echoerr "`curl' not found"
    return 0
  endif
endfunction

function! s:status()
  call s:prepare()
  call append(0, 'Checking plugins')

  let errs = 0
  for [name, spec] in items(g:plugs)
    let err = 'OK'
    if isdirectory(spec.dir)
      execute 'cd '.spec.dir
      let [valid, msg] = s:git_valid(spec, 0)
      if !valid
        let err = '(x) '. msg
      endif
      cd -
    else
      let err = '(x) Not found. Try PlugInstall.'
    endif
    let errs += err != 'OK'
    call append(2, printf('- %s: %s', name, err))
    call cursor(3, 1)
    redraw
  endfor
  call setline(1, 'Finished. '.errs.' error(s).')
endfunction

