"=============================================================================
" FILE: autoload/echodoc/util.vim
" AUTHOR:  Shougo Matsushita <Shougo.Matsu at gmail.com>
"          Tommy Allen <tommy@esdf.io>
" License: MIT license
"=============================================================================

" Return the next column and character at the column position in text.  Keep
" scanning until a usable character is found.  This should be safe for
" multi-byte characters.
function! s:mbprevchar(text, col) abort
  if a:col < 0
    return [a:col, '']
  endif
  let c = matchstr(a:text, '\%'.a:col.'c.')
  let c1 = a:col - 1
  while c1 > 0 && c ==# ''
    let c = matchstr(a:text, '\%'.c1.'c.')
    let c1 -= 1
  endwhile
  return [c1, c]
endfunction

" Try to find a function at the current position.
" Stops if `echodoc_max_blank_lines` is encountered. (max 50)
function! echodoc#util#get_func_text() abort
  let l2 = line('.')
  let c2 = col('.') - 1
  let l1 = l2
  let c1 = c2

  let skip = 0
  let last_quote = ''
  let text = getline(l1)[: c2-1]
  let found = 0
  let line_guard = 0
  let blank = 0
  let max_blank = max([1, get(b:, 'echodoc_max_blank_lines',
        \ get(g:, 'echodoc_max_blank_lines', 1))])
  let max_guard = get(b:, 'echodoc_max_line_guard',
        \ get(g:, 'echodoc_max_line_guard', 5))

  let check_quotes = s:check_quotes()

  while l1 > 0 && line_guard < max_guard && blank < max_blank
    if c1 <= 0
      let l1 -= 1
      let c1 = col([l1, '$'])
      let text = getline(l1)
      if text ==# ''
        let blank += 1
      else
        let blank = 0
      endif
      let line_guard += 1

      " Clear last quote
      let last_quote = ''
      continue
    endif

    if len(text) >= &l:columns
      break
    endif

    let [c1, c] = s:mbprevchar(text, c1)
    let p = ''

    if c1 > 0
      let [_, p] = s:mbprevchar(text, c1)
      if p ==# '\'
        continue
      endif
    endif

    if c =~# '\s'
      " Skip
      continue
    endif

    if last_quote ==# '' && index(check_quotes, c) >= 0
      let last_quote = c
      continue
    elseif last_quote !=# ''
      if last_quote == c
        let last_quote = ''
      endif
      continue
    endif

    if c ==# '('
      if skip == 0
        if p =~# '\k\|\s'
          let found = 1
          break
        endif
      else
        let skip -= 1
      endif
    elseif c ==# ')'
      let skip += 1
    endif
  endwhile

  if (found || last_quote !=# '') && l1 > 0 && c1 > 0
    let lines = getline(l1, l2)
    let lines[-1] = c2 == 0 ? '' : lines[-1][:c2 - 1]
    let lines[0] = c1 == 0 ? '' : matchstr(lines[0], '\k\+\%>'.(c1 - 1).'c.*')
    return [l1, c1, join(lines, "\n")]
  endif

  return [-1, -1, '']
endfunction

" Returns a parsed stack of functions found in the text.  Each item in the
" stack contains a dict:
" - name: Function name.
" - start: Argument start position.
" - end: Argument end position.  -1 if the function is unclosed.
" - pos: The argument position.  1-indexed, 0 = no args, -1 = closed.
" - ppos: The function's position in the previous function in the stack.
" - args: A list of arguments.
function! echodoc#util#parse_funcs(text, filetype) abort
  if a:text ==# ''
    return []
  endif

  let text = a:text

  " Function pointer pattern.
  " Example: int32_t get(void *const, const size_t)
  let text = substitute(text, '\s*(\*)\s*', '', 'g')
  let text = substitute(text, '^(\(.*\))$', '\1', '')
  let text = substitute(text, '\s\+\ze(', '', '')

  " Template arguments pattern.
  let text = substitute(text, '<\zs[^(]*\ze>', '...', 'g')

  let quote_i = -1
  let stack = []
  let open_stack = []
  let comma = 0

  " Matching pairs will count as a single argument entry so that commas can be
  " skipped within them.  The open depth is tracked in each open stack item.
  " Parenthesis is an exception since it's used for functions and can have a
  " depth of 1.
  let pairs = '({[)}]'
  let l = len(text) - 1
  let i = -1

  let check_quotes = s:check_quotes()

  while i < l
    let i += 1
    let c = text[i]

    if i > 0 && text[i - 1] ==# '\'
      continue
    endif

    if quote_i != -1
      " For languages that allow '''' ?
      " if c == "'" && text[i - 1] == c && i - quote_i > 1
      "   continue
      " endif
      if c == text[quote_i]
        let quote_i = -1
      endif
      continue
    endif

    if quote_i == -1 && index(check_quotes, c) >= 0
      " backtick (`) is not used alone in languages that I know of.
      let quote_i = i
      continue
    endif

    let prev = len(open_stack) ? open_stack[-1] : {'opens': [0, 0, 0]}
    let opened = prev.opens[0] + prev.opens[1] + prev.opens[2]

    let p = stridx(pairs, c)
    if p != -1
      let ci = p % 3
      if p == 3 && opened == 1 && prev.opens[0] == 1
        " Closing the function parenthesis
        if !empty(open_stack)
          let item = remove(open_stack, -1)
          let item.end = i - 1
          let item.pos = -1
          let item.opens[0] -= 1
          if comma <= i
            call add(item.args, text[comma :i - 1])
          endif
          let comma = item.i
        endif
      elseif p == 0
        " Opening parenthesis
        let func_i = match(text[:i - 1], '\S', comma)
        let func_name = matchstr(substitute(text[func_i :i - 1],
              \ '<[^>]*>', '', 'g'), '\k\+$')

        if func_i != -1 && func_i < i && func_name !=# ''
          let ppos = 0
          if !empty(open_stack)
            let ppos = open_stack[-1].pos
          endif

          if func_name !=# ''
            " Opening parenthesis that's preceded by a non-empty string.
            call add(stack, {
                  \ 'name': func_name,
                  \ 'i': func_i,
                  \ 'start': i + 1,
                  \ 'end': -1,
                  \ 'pos': 0,
                  \ 'ppos': ppos,
                  \ 'args': [],
                  \ 'opens': [1, 0, 0]
                  \ })
            call add(open_stack, stack[-1])

            " Function opening parenthesis marks the beginning of arguments.
            " let comma = i + 1
            let comma = i + 1
          endif
        else
          let prev.opens[0] += 1
        endif
      else
        let prev.opens[ci] += p > 2 ? -1 : 1
      endif
    elseif opened == 1 && prev.opens[0] == 1 && c ==# ','
      " Not nested in a pair.
      if !empty(open_stack) && comma <= i
        let open_stack[-1].pos += 1
        call add(open_stack[-1].args, text[comma :i - 1])
      endif
      let comma = i + 1
    endif
  endwhile

  if !empty(open_stack)
    let item = open_stack[-1]
    call add(item.args, text[comma :l])
    let item.pos += 1
  endif

  if index(['python', 'rust'], a:filetype) >= 0
    " Remove self argument
    let expr = a:filetype ==# 'python' ?
          \ "v:val !=# 'self'" :
          \ "index(['self', '&self', '&mut self'], v:val) < 0"
    for item in stack
      call filter(item.args, expr)
    endfor

    if !empty(stack) && !empty(stack[-1].args)
      " Remove heading spaces
      let stack[-1].args[0] = substitute(stack[-1].args[0], '^\s\+', '', '')
    endif
  endif

  if !empty(stack) && stack[-1].opens[0] == 0
    let item = stack[-1]
    let item.trailing = matchstr(text, '\s*\zs\p*', item.end + 2)
  endif

  return stack
endfunction


function! echodoc#util#completion_signature(completion, maxlen, filetype) abort
  if empty(a:completion)
    return {}
  endif

  let item = a:completion
  let abbrs = []
  if type(get(item, 'user_data', 0)) ==# v:t_string && item.user_data !=# ''
    let user_data = {}
    silent! let user_data = json_decode(item.user_data)
    if type(user_data) == v:t_dict && has_key(user_data, 'signature')
      call add(abbrs, user_data.signature)
    endif
  endif
  if get(item, 'info', '') =~# '^.\+(.*)'
    call add(abbrs, matchstr(item.info, '^\_s*\zs.*'))
  endif
  if get(item, 'kind', '') =~# '^.\+(.*)'
    call add(abbrs, item.kind)
  endif
  if get(item, 'menu', '') =~# '^.\+(.*)'
    call add(abbrs, item.menu)
  endif
  if get(item, 'abbr', '') =~# '^.\+(.*)'
    call add(abbrs, item.abbr)
  endif
  if item.word =~# '^.\+(.*)' || get(item, 'kind', '') ==# 'f'
    call add(abbrs, item.word)
  endif

  let stack = []
  for info in abbrs
    let info = info[:a:maxlen]
    let stack = echodoc#util#parse_funcs(info, a:filetype)
    if !empty(stack)
      break
    endif
  endfor

  if empty(stack)
    return {}
  endif

  let comp = stack[0]
  let word = matchstr(a:completion.word, '\k\+\ze[()]*')
  if index(['go', 'rust'], a:filetype) >= 0 && word !=# '' && comp.name !=# word
    " Note: It is for Rust and Go only.

    " Completion 'word' is what actually completed, if the parsed name is
    " different, it's probably because 'info' is an abstract function
    " signature.  .e.g in Go:
    " completed: BoolVar(p *bool, name string, value bool, usage string)
    " info:      func(p *bool, name string, value bool, usage string)
    let comp.name = word
  endif
  return comp
endfunction

function! s:check_quotes() abort
  " Note: It is very ugly check...
  " Rust contains quoting pattern like "fn from(s: &'s str) -> String"
  return &filetype !=# 'rust' ? ['"', '`', "'"] : ['"', '`']
endfunction