" Copyright 2011 The Go Authors. All rights reserved.
" Use of this source code is governed by a BSD-style
" license that can be found in the LICENSE file.
"
" This file provides a utility function that performs auto-completion of
" package names, for use by other commands.

" don't spam the user when Vim is started in Vi compatibility mode
let s:cpo_save = &cpo
set cpo&vim

let s:goos = $GOOS
let s:goarch = $GOARCH

if len(s:goos) == 0
  if exists('g:golang_goos')
    let s:goos = g:golang_goos
  elseif has('win32') || has('win64')
    let s:goos = 'windows'
  elseif has('macunix')
    let s:goos = 'darwin'
  else
    let s:goos = '*'
  endif
endif

if len(s:goarch) == 0
  if exists('g:golang_goarch')
    let s:goarch = g:golang_goarch
  else
    let s:goarch = '*'
  endif
endif

function! s:paths() abort
  let dirs = []

  if !exists("s:goroot")
    if executable('go')
      let s:goroot = go#util#env("goroot")
      if go#util#ShellError() != 0
        call go#util#EchoError('`go env GOROOT` failed')
      endif
    else
      let s:goroot = $GOROOT
    endif
  endif

  if len(s:goroot) != 0 && isdirectory(s:goroot)
    let dirs += [s:goroot]
  endif

  let workspaces = split(go#path#Default(), go#util#PathListSep())
  if workspaces != []
    let dirs += workspaces
  endif

  return dirs
endfunction

function! s:module() abort
  let [l:out, l:err] = go#util#ExecInDir(['go', 'list', '-m', '-f', '{{.Dir}}'])
  if l:err != 0
    return {}
  endif
  let l:dir = split(l:out, '\n')[0]

  let [l:out, l:err] = go#util#ExecInDir(['go', 'list', '-m', '-f', '{{.Path}}'])
  if l:err != 0
    return {}
  endif
  let l:path = split(l:out, '\n')[0]

  return {'dir': l:dir, 'path': l:path}
endfunction

function! s:vendordirs() abort
  let l:vendorsuffix = go#util#PathSep() . 'vendor'
  let l:module = s:module()
  if empty(l:module)
    let [l:root, l:err] = go#util#ExecInDir(['go', 'list', '-f', '{{.Root}}'])
    if l:err != 0
      return []
    endif
    if empty(l:root)
      return []
    endif

    let l:root = split(l:root, '\n')[0] . go#util#PathSep() . 'src'

    let [l:dir, l:err] = go#util#ExecInDir(['go', 'list', '-f', '{{.Dir}}'])
    if l:err != 0
      return []
    endif
    let l:dir = split(l:dir, '\n')[0]

    let l:vendordirs = []
    while l:dir != l:root
      let l:vendordir = l:dir . l:vendorsuffix
      if isdirectory(l:vendordir)
        let l:vendordirs = add(l:vendordirs, l:vendordir)
      endif

      let l:dir = fnamemodify(l:dir, ':h')
    endwhile

    return l:vendordirs
  endif

  let l:vendordir = l:module.dir . l:vendorsuffix
  if !isdirectory(l:vendordir)
    return []
  endif
  return [l:vendordir]
endfunction

let s:import_paths = {}
" ImportPath returns the import path of the package for current buffer. It
" returns -1 if the import path cannot be determined.
function! go#package#ImportPath() abort
  let l:dir = expand("%:p:h")
  if has_key(s:import_paths, dir)
    return s:import_paths[l:dir]
  endif

  let l:importpath = go#package#FromPath(l:dir)
  if type(l:importpath) == type(0)
    return -1
  endif

  let s:import_paths[l:dir] = l:importpath

  return l:importpath
endfunction

let s:in_gopath = {}
" InGOPATH returns TRUE when the package of the current buffer is within
" GOPATH.
function! go#package#InGOPATH() abort
  let l:dir = expand("%:p:h")
  if has_key(s:in_gopath, dir)
    return s:in_gopath[l:dir][0] !=#  '_'
  endif

  try
    " turn off module support so that `go list` will report the package name
    " with a leading '_' when the current buffer is not within GOPATH.
    let Restore_modules = go#util#SetEnv('GO111MODULE', 'off')
    let [l:out, l:err] = go#util#ExecInDir(['go', 'list'])
    if l:err != 0
      return 0
    endif

    let l:importpath = split(l:out, '\n')[0]
    if len(l:importpath) > 0
      let s:in_gopath[l:dir] = l:importpath
    endif
  finally
    call call(Restore_modules, [])
  endtry

  return len(l:importpath) > 0 && l:importpath[0] !=# '_'
endfunction

" go#package#FromPath returns the import path of arg. -1 is returned when arg
" does not specify a package. -2 is returned when arg is a relative path
" outside of GOPATH, not in a module, and not below the current working
" directory. A relative path is returned when in a null module at or below the
" current working directory..
function! go#package#FromPath(arg) abort
  let l:path = fnamemodify(a:arg, ':p')
  if !isdirectory(l:path)
    let l:path = fnamemodify(l:path, ':h')
  endif

  let l:dir = go#util#Chdir(l:path)
  try
    if glob("*.go") == ""
      " There's no Go code in this directory. We might be in a module directory
      " which doesn't have any code at this level. To avoid `go list` making a
      " bunch of HTTP requests to fetch dependencies, short-circuit `go list`
      " and return -1 immediately.
      if !empty(s:module())
        return -1
      endif
    endif
    let [l:out, l:err] = go#util#Exec(['go', 'list'])
    if l:err != 0
      return -1
    endif

    let l:importpath = split(l:out, '\n')[0]
  finally
    call go#util#Chdir(l:dir)
  endtry

  " go list returns '_CURRENTDIRECTORY' if the directory is in a null module
  " (i.e. neither in GOPATH nor in a module). Return a relative import path
  " if possible or an error if that is the case.
  if l:importpath[0] ==# '_'
    let l:relativeimportpath = fnamemodify(l:importpath[1:], ':.')
    if go#util#IsWin()
      let l:relativeimportpath = substitute(l:relativeimportpath, '\\', '/', 'g')
    endif

    if l:relativeimportpath == l:importpath[1:]
      return '.'
    endif

    if l:relativeimportpath[0] == '/'
      return -2
    endif

    let l:importpath= printf('./%s', l:relativeimportpath)
  endif

  return l:importpath
endfunction

function! go#package#CompleteMembers(package, member) abort
  let [l:content, l:err] = go#util#Exec(['go', 'doc', a:package])
  if l:err || !len(content)
    return []
  endif

  let lines = filter(split(content, "\n"),"v:val !~ '^\\s\\+$'")
  try
    let mx1 = '^\s\+\(\S+\)\s\+=\s\+.*'
    let mx2 = '^\%(const\|var\|type\|func\) \([A-Z][^ (]\+\).*'
    let candidates = map(filter(copy(lines), 'v:val =~ mx1'),
          \ 'substitute(v:val, mx1, "\\1", "")')
          \ + map(filter(copy(lines), 'v:val =~ mx2'),
          \ 'substitute(v:val, mx2, "\\1", "")')
    return filter(candidates, '!stridx(v:val, a:member)')
  catch
    return []
  endtry
endfunction

function! go#package#Complete(ArgLead, CmdLine, CursorPos) abort
  let words = split(a:CmdLine, '\s\+', 1)

  " do not complete package members for these commands
  let neglect_commands = ["GoImportAs", "GoGuruScope"]

  if len(words) > 2 && index(neglect_commands, words[0]) == -1
    " Complete package members
    return go#package#CompleteMembers(words[1], words[2])
  endif

  let dirs = s:paths()
  let module = s:module()

  if len(dirs) == 0 && empty(module)
    " should not happen
    return []
  endif

  let vendordirs = s:vendordirs()

  let l:modcache = go#util#env('gomodcache')

  let ret = {}
  for dir in dirs
    " this may expand to multiple lines
    let root = split(expand(dir . '/pkg/' . s:goos . '_' . s:goarch), "\n")
    if l:modcache != ''
      let root = add(root, l:modcache)
    else
      let root = add(root, expand(dir . '/pkg/mod'))
    endif
    let root = add(root, expand(dir . '/src'), )
    let root = extend(root, vendordirs)
    let root = add(root, module)
    for item in root
      " item may be a dictionary when operating in a module.
      if type(item) == type({})
        if empty(item)
          continue
        endif
        let dir = item.dir
        let path = item.path
      else
        let dir = item
        let path = item
      endif

      if !empty(module) && dir ==# module.dir
        if stridx(a:ArgLead, module.path) == 0
          if len(a:ArgLead) != len(module.path)
            let glob = globpath(module.dir, substitute(a:ArgLead, module.path . '/\?', '', '').'*')
          else
            let glob = module.dir
          endif
        elseif stridx(module.path, a:ArgLead) == 0 && stridx(module.path, '/', len(a:ArgLead)) < 0
          " use the module directory when module.path begins wih a:ArgLead and
          " module.path does not have any path segments after a:ArgLead.
          let glob = module.dir
        else
          continue
        endif
      else
        let glob = globpath(dir, a:ArgLead.'*')
      endif
      for candidate in split(glob)
        if isdirectory(candidate)
          " TODO(bc): use wildignore instead of filtering out vendor
          " directories manually?
          if fnamemodify(candidate, ':t') == 'vendor'
            continue
          endif
          " if path contains version info, strip it out
          let vidx = strridx(candidate, '@')
          if vidx >= 0
            let candidate = strpart(candidate, 0, vidx)
          endif
          let candidate .= '/'
        elseif candidate !~ '\.a$'
          continue
        endif

        if dir !=# path
          let candidate = substitute(candidate, '^' . dir, path, 'g')
        else
          let candidate = candidate[len(dir)+1:]
        endif
        " replace a backslash with a forward slash and drop .a suffixes
        let candidate = substitute(substitute(candidate, '[\\]', '/', 'g'),
                          \ '\.a$', '', 'g')

        " without this the result can have duplicates in form of
        " 'encoding/json' and '/encoding/json/'
        let candidate = go#util#StripPathSep(candidate)

        let ret[candidate] = candidate
      endfor
    endfor
  endfor
  return sort(keys(ret))
endfunction

" restore Vi compatibility settings
let &cpo = s:cpo_save
unlet s:cpo_save

" vim: sw=2 ts=2 et