"  guru.vim -- Vim integration for the Go guru.

func! s:RunGuru(mode, format, selected, needs_scope) range abort
  "return with a warning if the binary doesn't exist
  let bin_path = go#path#CheckBinPath("guru") 
  if empty(bin_path)
    return {'err': "bin path not found"}
  endif

  let filename = fnamemodify(expand("%"), ':p:gs?\\?/?')
  if !filereadable(filename)
    " this might happen for new buffers which are not written yet
    return {'err': "file does not exist"}
  endif

  if &modified
    " Write current unsaved buffer to a temp file and use the modified content
    let l:tmpname = tempname()
    call writefile(getline(1, '$'), l:tmpname)
    let filename = l:tmpname
  endif
  let dirname = expand('%:p:h')
  let pkg = go#package#ImportPath(dirname)

  " this is important, check it!
  if pkg == -1 && a:needs_scope
    return {'err': "current directory is not inside of a valid GOPATH"}
  endif

  " start constructing the 'command' variable
  let command = bin_path

  " enable outputting in json format
  if a:format == "json"
    let command .= " -json"
  endif

  " check for any tags
  if exists('g:go_guru_tags')
    let tags = get(g:, 'go_guru_tags')
    let command .= printf(" -tags %s", tags)
  endif

  " some modes require scope to be defined (such as callers). For these we
  " choose a sensible setting, which is using the current file's package
  let scopes = []
  if a:needs_scope
    let scopes = [pkg]
  endif

  " check for any user defined scope setting. users can define the scope,
  " in package pattern form. examples:
  "  golang.org/x/tools/cmd/guru # a single package
  "  golang.org/x/tools/...      # all packages beneath dir
  "  ...                         # the entire workspace.
  if exists('g:go_guru_scope')
    " check that the setting is of type list
    if type(get(g:, 'go_guru_scope')) != type([])
      return {'err' : "go_guru_scope should of type list"}
    endif

    let scopes = get(g:, 'go_guru_scope')
  endif

  " now add the scope to our command if there is any
  if !empty(scopes)
    " strip trailing slashes for each path in scoped. bug:
    " https://github.com/golang/go/issues/14584
    let scopes = go#util#StripTrailingSlash(scopes)

    " create shell-safe entries of the list
    let scopes = go#util#Shelllist(scopes)

    " guru expect a comma-separated list of patterns, construct it
    let scope = join(scopes, ",")
    let command .= printf(" -scope %s", scope)
  endif

  let pos = printf("#%s", go#util#OffsetCursor())
  if a:selected != -1
    " means we have a range, get it
    let pos1 = go#util#Offset(line("'<"), col("'<"))
    let pos2 = go#util#Offset(line("'>"), col("'>"))
    let pos = printf("#%s,#%s", pos1, pos2)
  endif

  " this is our final command
  let filename .= ':'.pos
  let command .= printf(' %s %s', a:mode, shellescape(filename))

  let old_gopath = $GOPATH
  let $GOPATH = go#path#Detect()

  if a:mode !=# 'what'
    " the query might take time, let us give some feedback
    call go#util#EchoProgress("analysing ...")
  endif

  " run, forrest run!!!
  let out = go#util#System(command)

  if exists("l:tmpname")
    call delete(l:tmpname)
  endif

  let $GOPATH = old_gopath
  if go#util#ShellError() != 0
    " the output contains the error message
    return {'err' : out}
  endif

  return {'out': out}
endfunc

" This uses Vim's errorformat to parse the output from Guru's 'plain output
" and put it into location list. I believe using errorformat is much more
" easier to use. If we need more power we can always switch back to parse it
" via regex.
func! s:loclistSecond(output)
  " backup users errorformat, will be restored once we are finished
  let old_errorformat = &errorformat

  " match two possible styles of errorformats:
  "
  "   'file:line.col-line2.col2: message'
  "   'file:line:col: message'
  "
  " We discard line2 and col2 for the first errorformat, because it's not
  " useful and location only has the ability to show one line and column
  " number
  let errformat = "%f:%l.%c-%[%^:]%#:\ %m,%f:%l:%c:\ %m"
  call go#list#ParseFormat("locationlist", errformat, split(a:output, "\n"))

  let errors = go#list#Get("locationlist")
  call go#list#Window("locationlist", len(errors))
endfun


function! go#guru#Scope(...)
  if a:0
    if a:0 == 1 && a:1 == '""'
      unlet g:go_guru_scope
      call go#util#EchoSuccess("guru scope is cleared")
    else
      let g:go_guru_scope = a:000
      call go#util#EchoSuccess("guru scope changed to: ". join(a:000, ","))
    endif

    return
  endif

  if !exists('g:go_guru_scope')
    call go#util#EchoError("guru scope is not set")
  else
    call go#util#EchoSuccess("current guru scope: ". join(g:go_guru_scope, ","))
  endif
endfunction

function! go#guru#Tags(...)
  if a:0
    if a:0 == 1 && a:1 == '""'
      unlet g:go_guru_tags
      call go#util#EchoSuccess("guru tags is cleared")
    else
      let g:go_guru_tags = a:1
      call go#util#EchoSuccess("guru tags changed to: ". a:1)
    endif

    return
  endif

  if !exists('g:go_guru_tags')
    call go#util#EchoSuccess("guru tags is not set")
  else
    call go#util#EchoSuccess("current guru tags: ". a:1)
  endif
endfunction

" Show 'implements' relation for selected package
function! go#guru#Implements(selected)
  let out = s:RunGuru('implements', 'plain', a:selected, 1)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Describe selected syntax: definition, methods, etc
function! go#guru#Describe(selected)
  let out = s:RunGuru('describe', 'plain', a:selected, 0)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show possible targets of selected function call
function! go#guru#Callees(selected)
  let out = s:RunGuru('callees', 'plain', a:selected, 1)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show possible callers of selected function
function! go#guru#Callers(selected)
  let out = s:RunGuru('callers', 'plain', a:selected, 1)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show path from callgraph root to selected function
function! go#guru#Callstack(selected)
  let out = s:RunGuru('callstack', 'plain', a:selected, 1)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show free variables of selection
function! go#guru#Freevars(selected)
  " Freevars requires a selection
  if a:selected == -1
    call go#util#EchoError("GoFreevars requires a selection (range) of code")
    return
  endif

  let out = s:RunGuru('freevars', 'plain', a:selected, 0)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show send/receive corresponding to selected channel op
function! go#guru#ChannelPeers(selected)
  let out = s:RunGuru('peers', 'plain', a:selected, 1)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

" Show all refs to entity denoted by selected identifier
function! go#guru#Referrers(selected)
  let out = s:RunGuru('referrers', 'plain', a:selected, 0)
  if has_key(out, 'err')
    call go#util#EchoError(out.err)
    return
  endif

  call s:loclistSecond(out.out)
endfunction

function! go#guru#What(selected)
  " nvim doesn't have JSON support, though they work on it:
  " https://github.com/neovim/neovim/pull/4131
  if has('nvim')
    return {'err': "GoWhat is not supported in Neovim"}
  endif

  " json_encode() and friends are introduced with this patch
  " https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ
  if !has('patch-7.4.1304')
    return {'err': "GoWhat is supported with Vim version 7.4-1304 or later"}
  endif

  let out = s:RunGuru('what', 'json', a:selected, 0)
  if has_key(out, 'err')
    return {'err': out.err}
  endif

  let result = json_decode(out.out)

  if type(result) != type({})
    return {'err': "malformed output from guru"}
  endif

  return result
endfunction

function! go#guru#SameIds(selected)
  call go#guru#ClearSameIds()

  let result = go#guru#What(a:selected)
  if has_key(result, 'err') && !get(g:, 'go_auto_sameids', 0)
    " only echo if it's called via `:GoSameIds, but not if it's in automode
    call go#util#EchoError(result.err)
    return
  endif

  if !has_key(result, 'sameids')
    if !get(g:, 'go_auto_sameids', 0)
      call go#util#EchoError("no same_ids founds for the given identifier")
    endif
    return
  endif

  let poslen = 0
  for enclosing in result['enclosing']
    if enclosing['desc'] == 'identifier'
      let poslen = enclosing['end'] - enclosing['start']
      break
    endif
  endfor

  " return when there's no identifier to highlight.
  if poslen == 0
    return
  endif

  hi goSameId term=bold cterm=bold ctermbg=white ctermfg=black

  let same_ids = result['sameids']
  " highlight the lines
  for item in same_ids
    let pos = split(item, ':')
    call matchaddpos('goSameId', [[str2nr(pos[-2]), str2nr(pos[-1]), str2nr(poslen)]])
  endfor
endfunction

function! go#guru#ClearSameIds()
  let m = getmatches()
  for item in m
    if item['group'] == 'goSameId'
      call matchdelete(item['id'])
    endif
  endfor
endfunction

" vim: sw=2 ts=2 et
