"=============================================================================
" profile.vim --- Profile startuptime
" Copyright (c) 2016-2021 Wang Shidong & Contributors
" Author: Wang Shidong < wsdjeg@outlook.com >
" URL: https://spacevim.org
" License: GPLv3
"=============================================================================

let s:CPT = SpaceVim#api#import('vim#compatible')

let s:plugin_hints = [
      \ 'autoload',
      \ 'colors',
      \ 'compiler',
      \ 'filetype.vim',
      \ 'ftdetect',
      \ 'ftplugin',
      \ 'indent',
      \ 'keymap',
      \ 'plugin',
      \ 'rplugin',
      \ 'syntax',
      \ ]

let s:levels = [
      \ 'Flawless Victory',
      \ 'Outstanding',
      \ 'Fatality',
      \ 'Toasty',
      \ 'Impressive',
      \ 'Well Done',
      \ 'Test your might',
      \ ]


function! s:plugin_sort(a, b) abort
  return len(a:b[0]) - len(a:a[0])
endfunction


function! s:result_sort(a, b) abort
  if a:a[1] < a:b[1]
    return 1
  elseif a:a[1] > a:b[1]
    return -1
  endif
  return 0
endfunction


function! s:get_vimrc() abort
  if exists('$MYVIMRC') && !empty($MYVIMRC)
    return expand($MYVIMRC)
  endif

  return expand(matchstr(s:CPT.execute('silent scriptnames'), '1: \zs\f\+'))
endfunction


function! s:init_plugins(vimrc) abort
  let vimrc_path = substitute(fnamemodify(a:vimrc, ':p:h') . '/',
        \ '//', '/', 'g')
  let default_vimrc_path = fnamemodify('~/.vim', ':p')
  let runtime_path = fnamemodify(expand('$VIMRUNTIME'), ':p')
  let nvim_config = substitute((exists('$XDG_CONFIG_HOME')
        \ ? expand('$XDG_CONFIG_HOME') : fnamemodify('~/.config', ':p'))
        \ . '/nvim/', '//', '/', 'g')
  let home = fnamemodify('~', ':p')
  let seen = [vimrc_path, default_vimrc_path, runtime_path, nvim_config]
  let s:plugins = []

  if exists('g:plugs')
    for [plugin, info] in items(g:plugs)
      if !has_key(info, 'dir') || index(seen, info.dir) != -1
        continue
      endif
      call add(s:plugins, [info.dir, plugin])
      call add(seen, info.dir)
    endfor
  elseif exists('g:dein#_plugins')
    for [plugin, info] in items(g:dein#_plugins)
      if !has_key(info, 'rtp') || index(seen, info.rtp) != -1
        continue
      endif
      let dir = fnamemodify(info.rtp, ':p')
      call add(s:plugins, [dir, plugin])
      call add(seen, dir)
    endfor
  endif

  for path in split(&runtimepath, ',')
    let path = fnamemodify(path, ':p')
    if path =~# '/$'
      let path = path[:-2]
    endif

    if path =~# '/after$'
      let path = fnamemodify(path, ':h')
      if path =~# '/$'
        let path = path[:-2]
      endif
    endif

    if isdirectory(path) && index(seen, path . '/') == -1
      for hint in s:plugin_hints
        let hint_path = path . '/' . hint
        if isdirectory(hint_path) || filereadable(hint_path)
          let name = fnamemodify(path, ':t')
          call add(s:plugins, [path . '/', name])
          call add(seen, path . '/')
          break
        endif
      endfor
    endif
  endfor

  call sort(s:plugins, function('s:plugin_sort'))
  call add(s:plugins, [runtime_path, '[runtime]'])

  if vimrc_path != home
    call add(s:plugins, [vimrc_path, '[vimrc]'])
  endif

  if vimrc_path != default_vimrc_path && isdirectory(default_vimrc_path)
    call add(s:plugins, [default_vimrc_path, '[vimrc]'])
  endif

  if isdirectory(nvim_config)
    call add(s:plugins, [nvim_config, '[vimrc]'])
  endif
endfunction


function! s:get_plugin(fname) abort
  for [path, name] in s:plugins
    if len(path) < len(a:fname) && a:fname[:len(path)-1] == path
      return name
    endif
  endfor

  return '[unknown]'
endfunction


function! s:get_samples(cmd, count, tmp) abort
  let c = 0
  let phase_order = []
  let phases = {'startup': {'_files': {}, '_time': 0}}
  let totals = {}
  let total_time = 0
  let logs = []

  while c < a:count
    if getchar(0) == 27
      echomsg printf('Stopped after %d samples', c)
      break
    endif

    let c += 1
    redraw
    echo printf('Sample %d/%d', c, a:count)
    call system(a:cmd)

    if !filereadable(a:tmp)
      echohl ErrorMsg
      echo 'Profile log wasn''t created'
      echohl None
      break
    endif

    let phase = 'startup'
    let log = readfile(a:tmp)
    call add(logs, log)

    for line in log
      if line =~# '^\%(\d\+\.\d\+\s*\)\{2}:'
        if c == 1
          call add(phase_order, phase)
        endif
        " call add(phases, {'phase': phase, 'times': cur_phase})
        " let cur_phase = {}
        let phase = matchstr(line, '\d\+\.\d\+: \zs.*')
        if !has_key(phases, phase)
          let phases[phase] = {'_files': {}, '_time': 0}
        endif
      elseif line =~# '^\%(\d\+\.\d\+\s*\)\{3}: sourcing '
        let [time, fname] = split(matchstr(line, '\d\+\.\d\+: .*'), ':\s*sourcing\s*')
        let plugin = s:get_plugin(fname)

        if !has_key(phases[phase], plugin)
          let phases[phase][plugin] = 0
          let phases[phase]['_files'][plugin] = {}
        endif

        if !has_key(phases[phase]['_files'][plugin], fname)
          let phases[phase]['_files'][plugin][fname] = 0
        endif

        if !has_key(totals, plugin)
          let totals[plugin] = 0
        endif

        let t = str2float(time)
        let phases[phase][plugin] += t
        let phases[phase]['_time'] += t
        let phases[phase]['_files'][plugin][fname] += t
        let totals[plugin] += t
        let total_time += t
      endif
    endfor

    call delete(a:tmp)
  endwhile

  for phase in keys(phases)
    for plugin in keys(phases[phase])
      if plugin != '_files'
        let phases[phase][plugin] = phases[phase][plugin] / c
      else
        for fplugin in keys(phases[phase][plugin])
          for fname in keys(phases[phase][plugin][fplugin])
            let phases[phase][plugin][fplugin][fname] = phases[phase][plugin][fplugin][fname] / c
          endfor
        endfor
      endif
    endfor
  endfor

  for plugin in keys(totals)
    let totals[plugin] = totals[plugin] / c
  endfor

  let total_time = total_time / c

  return [total_time, totals, phase_order, phases, logs]
endfunction


function! SpaceVim#dev#profile#run(...)
  let sample_count = 10
  let vimrc = s:get_vimrc()
  let extra_args = []

  for arg in a:000
    if arg == '--'
      call add(extra_args, '')
    elseif !empty(extra_args)
      call add(extra_args, arg)
    elseif arg =~# '\d\+'
      let sample_count = str2nr(arg)
    elseif arg =~# '\f\+' && filereadable(expand(arg))
      let vimrc = expand(arg)
    endif
  endfor

  if a:0 && type(a:1) == type(0) && a:1 > 0
    let sample_count = a:1
  endif

  call s:init_plugins(vimrc)

  if exists('v:progpath') && !empty(v:progpath) && executable(v:progpath)
    let exe = v:progpath
  else
    let exe = has('nvim') ? 'nvim' : 'vim'
    if has('win32')
      let exe .= '.exe'
    endif
  endif

  let tmp = tempname()
  let wintmp = ''
  let quiet_arg = has('nvim') ? '--headless' : '--not-a-term'
  let args = ' -i NONE --startuptime ' . tmp . ' +qa!'
  if !empty(vimrc)
    let args = ' -u ' . vimrc . args
  endif

  if !empty(extra_args)
    let args .= ' ' . join(extra_args, ' ')
  endif

  call system(exe . ' ' . quiet_arg . ' +qa!')

  if v:shell_error
    " Use `script` so Vim doesn't issue a delay warning
    if has('macunix')
      let cmd = 'script -q /dev/null ' . exe . args
    elseif has('win32')
      " Just hope for the best
      let wintmp = tempname()
      let cmd = exe . args . ' >' . wintmp .' 2>&1'
    else
      let cmd = 'script -q -c "' . exe . args . '" /dev/null'
    endif
  else
    let cmd = exe . ' ' . quiet_arg . args
  endif

  echomsg 'Sampling with command:' cmd

  let [total_time, totals, phase_order,
        \ phases, logs] = s:get_samples(cmd, sample_count, tmp)
  let total_samples = len(logs)

  if !empty(wintmp) && filereadable(wintmp)
    call delete(wintmp)
  endif

  let level_time = 1000 / (len(s:levels) - 1)
  let l = float2nr(floor(min([float2nr(total_time), 1000]) / level_time))
  let level = s:levels[l]

  let lines = [printf('Total Time: %8.3f -- %s', total_time, level), '']

  let slowest = sort(items(totals), function('s:result_sort'))[:9]
  let width = max(map(copy(slowest), 'len(v:val[0])'))
  let lines += ['', printf('Slowest %d plugins (out of %d)~', len(slowest), len(totals))]

  for [plugin, time] in slowest
    call add(lines, printf("%*s\t%-8.3f", width, plugin, time))
  endfor

  let lines += ['', 'Phase Detail:~', '']

  for phase in phase_order
    let item = phases[phase]
    let files = remove(item, '_files')
    let phase_total = remove(item, '_time')

    if empty(item)
      continue
    endif

    let lines += [printf('%s (%0.3f)~', phase, phase_total)]
    for [plugin, time] in sort(items(item), function('s:result_sort'))
      let lines += [printf("%-8.3f  %s >", time, plugin)]
      for [fname, time] in sort(items(files[plugin]), function('s:result_sort'))
        let lines += [printf("\t%-8.3f  %s", time, fname)]
      endfor
      let lines += ['<']
    endfor
    let lines += ['']
  endfor

  let banner_line = repeat('=', 34)
  let lines += ['', printf('%s FULL LOGS %s', banner_line, banner_line), '']

  let i = 0
  for log in logs
    let i += 1
    let lines += [printf('Log %d/%d >', i, total_samples)]
    let l = 0
    for line in log
      if line !~# '^\s*$'
        let log = log[l :]
        break
      endif
      let l += 1
    endfor
    let lines += map(log, '"  " . v:val') + ['<']
  endfor

  enew
  silent %put=lines
  call cursor(1, 1)
  silent delete _
  set buftype=nofile syntax=help foldmethod=marker foldmarker=>,< nomodified
  silent file startup-log.txt
  normal! zM
endfunction
