" Author:  Eric Van Dewoestine
"
" Description: {{{
"   Plugin that integrates vim with the eclipse plugin eclim (ECLipse
"   IMproved).
"
"   This plugin contains shared functions that can be used regardless of the
"   current file type being edited.
"
" License:
"
" Copyright (C) 2005 - 2021  Eric Van Dewoestine
"
" This program is free software: you can redistribute it and/or modify
" it under the terms of the GNU General Public License as published by
" the Free Software Foundation, either version 3 of the License, or
" (at your option) any later version.
"
" This program is distributed in the hope that it will be useful,
" but WITHOUT ANY WARRANTY; without even the implied warranty of
" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
" GNU General Public License for more details.
"
" You should have received a copy of the GNU General Public License
" along with this program.  If not, see <http://www.gnu.org/licenses/>.
"
" }}}

" Script Variables {{{
  let s:echo_connection_errors = 1

  let s:command_ping = '-command ping'
  let s:command_settings = '-command settings'
  let s:command_settings_update = '-command settings_update -s "<settings>"'
  let s:command_shutdown = "-command shutdown"
  let s:command_jobs = '-command jobs'
  let s:connect= '^connect: .*$'

  let s:vim_settings = {}
  let s:vim_settings_defaults = {}
  let s:vim_settings_types = {}
  let s:vim_settings_validators = {}
" }}}

function! eclim#Execute(command, ...) " {{{
  " Optional args:
  "   options {
  "     One of the following to determine the eclimd instance to use, honored in
  "     the order shown here:
  "       instance: dictionary representing an eclimd instance.
  "       project: project name
  "       workspace: workspace path
  "       dir: directory path to use as the current dir
  "     exec: 1 to execute the command using execute instead of system.
  "     raw: 1 to get the result without evaluating as json
  "   }

  if exists('g:EclimDisabled')
    " if we are not in an autocmd or the autocmd is for an acwrite buffer,
    " alert the user that eclimd is disabled.
    if expand('<abuf>') == '' || &buftype == 'acwrite'
      call eclim#util#EchoWarning(
        \ "eclim is currently disabled. use :EclimEnable to enable it.")
    endif
    return
  endif

  if !eclim#EclimAvailable()
    return
  endif

  let command = '-editor vim ' . a:command

  " encode special characters
  " http://www.w3schools.com/TAGS/ref_urlencode.asp
  let command = substitute(command, '\*', '%2A', 'g')
  let command = substitute(command, '\$', '%24', 'g')
  let command = substitute(command, '<', '%3C', 'g')
  let command = substitute(command, '>', '%3E', 'g')

  " determine the eclimd instance to use
  let options = a:0 ? a:1 : {}
  let instance = get(options, 'instance', {})
  if len(instance) == 0
    let project = get(options, 'project', '')
    if project != ''
      let workspace = eclim#project#util#GetProjectWorkspace(project)
      if type(workspace) == g:LIST_TYPE
        let workspaces = workspace
        unlet workspace
        let response = eclim#util#PromptList(
          \ 'Muliple workspaces found, please choose the target workspace',
          \ workspaces, g:EclimHighlightInfo)

        " user cancelled, error, etc.
        if response < 0
          return
        endif

        let workspace = workspaces[response]
      endif
    else
      let workspace = ''
    endif

    if workspace == ''
      let workspace = get(options, 'workspace', '')
    endif

    let dir = workspace != '' ? workspace : get(options, 'dir', '')
    let chosen = eclim#client#nailgun#ChooseEclimdInstance(dir)
    if type(chosen) != g:DICT_TYPE
      return
    endif
    let instance = chosen
  endif

  let exec = get(options, 'exec', 0)
  let [retcode, result] = eclim#client#nailgun#Execute(instance, command, exec)
  let result = substitute(result, '\n$', '', '')

  " an echo during startup causes an annoying issue with vim.
  "call eclim#util#Echo(' ')

  " check for errors
  let error = ''
  if result =~ '^[^\n]*Exception:\?[^\n]*\n\s\+\<at\> ' ||
   \ result =~ '^[^\n]*ResourceException(.\{-})\[[0-9]\+\]:[^\n]*\n\s\+\<at\> '
    if g:EclimLogLevel != 'trace' && !&verbose
      let error = substitute(result, '\(.\{-}\)\n.*', '\1', '')
    else
      let error = result
    endif
  elseif retcode
    let error = result
  endif

  if retcode || error != ''
    if s:echo_connection_errors
      if error =~ s:connect
        " if we are not in an autocmd or the autocmd is for an acwrite buffer,
        " alert the user that eclimd is not running.
        if expand('<abuf>') == '' || &buftype == 'acwrite'
          call eclim#util#EchoWarning(
            \ "unable to connect to eclimd (port: " . instance.port . ") - " . error)
        endif
      else
        let error = error . "\n" .
          \ 'while executing command (port: ' . instance.port . '): ' . command
        " if we are not in an autocmd or in a autocmd for an acwrite buffer,
        " echo the error, otherwise just log it.
        if expand('<abuf>') == '' || &buftype == 'acwrite'
          call eclim#util#EchoError(error)
        else
          call eclim#util#EchoDebug(error)
        endif
      endif
    endif
    return
  endif

  let raw = get(options, 'raw', 0)
  return result != '' && !raw ? eval(result) : result
endfunction " }}}

function! eclim#Disable() " {{{
  if !exists('g:EclimDisabled')
    let g:EclimDisabled = 1
  endif
endfunction " }}}

function! eclim#Enable() " {{{
  if exists('g:EclimDisabled')
    unlet g:EclimDisabled
  endif
endfunction " }}}

function! eclim#EclimAvailable(...) " {{{
  " Optional args:
  "   echo: Whether or not to echo an error if eclim is not available
  "         (default: 1)
  let instances = eclim#UserHome() . '/.eclim/.eclimd_instances'
  let available = filereadable(instances)
  let echo = a:0 ? a:1 : 1
  if echo && !available && expand('<abuf>') == ''
    call eclim#util#EchoError(printf(
      \ 'No eclimd instances found running (eclimd created file not found %s)',
      \ eclim#UserHome() . '/.eclim/.eclimd_instances'))
  endif
  return available
endfunction " }}}

function! eclim#PingEclim(echo, ...) " {{{
  " If echo is non 0, then the result is echoed to the user.
  " Optional args:
  "   workspace

  let workspace = a:0 ? a:1 : ''
  if a:echo
    let result = eclim#Execute(s:command_ping, {'workspace': workspace})
    if type(result) == g:DICT_TYPE
      call eclim#util#Echo(
        \ 'eclim   ' . result.eclim . "\n" .
        \ 'eclipse ' . result.eclipse)
    endif
  else
    let savedErr = s:echo_connection_errors
    let savedLog = g:EclimLogLevel
    let s:echo_connection_errors = 0
    let g:EclimLogLevel = 'off'

    let result = eclim#Execute(s:command_ping, {'workspace': workspace})

    let s:echo_connection_errors = savedErr
    let g:EclimLogLevel = savedLog

    return type(result) == g:DICT_TYPE
  endif
endfunction " }}}

function! eclim#ParseSettingErrors(errors) " {{{
  let errors = []
  for error in a:errors
    let message = error.message
    let setting = substitute(message, '^\(.\{-}\): .*', '\1', '')
    let message = substitute(message, '^.\{-}: \(.*\)', '\1', '')
    if error.line == 1 && setting != error.message
      let line = search('^\s*' . setting . '\s*=', 'cnw')
      let error.line = line > 0 ? line : 1
    endif
    call add(errors, {
        \ 'bufnr': bufnr('%'),
        \ 'lnum': error.line,
        \ 'text': message,
        \ 'type': error.warning == 1 ? 'w' : 'e',
      \ })
  endfor
  return errors
endfunction " }}}

function! eclim#SaveSettings(command, project) " {{{
  " don't check modified since undo seems to not set the modified flag
  "if &modified
    let tempfile = substitute(tempname(), '\', '/', 'g')

    " get all lines, filtering out comments and blank lines
    let lines = filter(getline(1, line('$')), 'v:val !~ "^\\s*\\(#\\|$\\)"')

    " convert lines into a settings dict
    let index = 0
    let settings = {}
    let pattern = '^\s*\([[:alnum:]_.-]\+\)\s*=\s*\(.*\)'
    while index < len(lines)
      if lines[index] =~ pattern
        let name = substitute(lines[index], pattern, '\1', '')
        let value = substitute(lines[index], pattern, '\2', '')
        while value =~ '\\$'
          let index += 1
          let value = substitute(value, '\\$', '', '')
          let value .= substitute(lines[index], '^\s*', '', '')
        endwhile

        " convert arrays to vim list
        if value =~ '^\[.*\]$'
          " before we convert, we need to escape any backslashes otherwise
          " they will be lost.
          let value = substitute(value, '\\', '\\\\', 'g')
          " should be safe since unlike eval in other langs, this doesn't
          " execute code.
          let value = eval(value)
        endif

        let settings[name] = value
      endif
      let index += 1
    endwhile
    call writefile([json_encode(settings)], tempfile)

    let command = a:command
    let command = substitute(command, '<project>', a:project, '')
    let command = substitute(command, '<settings>', tempfile, '')

    if exists('b:eclimd_instance')
      let result = eclim#Execute(command, {'instance': b:eclimd_instance})
    else
      let result = eclim#Execute(command)
    endif

    if type(result) == g:LIST_TYPE
      call eclim#util#EchoError
        \ ("Operation contained errors.  See location list for details.")
      let errors = eclim#ParseSettingErrors(result)
      call eclim#util#SetLocationList(errors)
    else
      call eclim#util#ClearLocationList()
      call eclim#util#Echo(result)
    endif

    setlocal nomodified
  "endif
endfunction " }}}

function! eclim#Settings(workspace) " {{{
  let instance = eclim#client#nailgun#ChooseEclimdInstance(a:workspace)
  if type(instance) != g:DICT_TYPE
    return
  endif

  let settings = eclim#Execute(s:command_settings, {'instance': instance})
  if type(settings) != g:LIST_TYPE
    return
  endif

  let content = ['# Global settings for workspace: ' . instance.workspace, '']
  let path = ''
  for setting in settings
    if setting.path != path
      if path != ''
        let content += ['# }', '']
      endif
      let path = setting.path
      call add(content, '# ' . path . ' {')
    endif
    let description = split(setting.description, '\n')
    let content += map(description, "'\t# ' . v:val")
    let value = setting.value
    if type(value) == g:LIST_TYPE
      let value = json_encode(value)
      let value = substitute(value, '\\\\', '\\', 'g')
    endif
    call add(content, "\t" . setting.name . '=' . value)
  endfor
  if path != ''
    call add(content, '# }')
  endif

  call eclim#util#TempWindow("Workspace_Settings", content)
  setlocal buftype=acwrite
  setlocal filetype=jproperties
  setlocal noreadonly
  setlocal modifiable
  setlocal foldmethod=marker
  setlocal foldmarker={,}
  let b:eclimd_instance = instance

  augroup eclim_settings
    autocmd! BufWriteCmd <buffer>
    exec 'autocmd BufWriteCmd <buffer> ' .
      \ 'call eclim#SaveSettings(s:command_settings_update, "")'
  augroup END
endfunction " }}}

function! eclim#ShutdownEclim() " {{{
  call eclim#Execute(s:command_shutdown)
endfunction " }}}

function! eclim#LoadVimSettings() " {{{
  " Set some initial logging variables for the case where eclim#UserHome needs
  " to make a system call
  if !exists('g:EclimLogLevel')
    let g:EclimLogLevel = 'info'
    let g:EclimHighlightTrace = 'Normal'
  endif

  let settings_file = eclim#UserHome() . '/.eclim/.eclim_settings'
  if filereadable(settings_file)
    let lines = readfile(settings_file)
    if len(lines) == 1 && lines[0] =~ '^{.*}$'
      let settings = eval(lines[0])
      for [key, value] in items(settings)
        let name = 'g:Eclim' . key
        if !exists(name)
          exec 'let ' . name . ' = ' . string(value)
        endif
        unlet value
      endfor
    endif
  endif

  " Handle legacy sign/log level values
  let legacy = {0: 'off', 1: 'error', 2: 'error', 3: 'warning', 4: 'info', 5: 'debug', 6: 'trace'}
  if exists('g:EclimLogLevel') && type(g:EclimLogLevel) == g:NUMBER_TYPE
    let g:EclimLogLevel = get(legacy, g:EclimLogLevel, 'info')
  endif
  if exists('g:EclimSignLevel') && type(g:EclimSignLevel) == g:NUMBER_TYPE
    let g:EclimSignLevel = get(legacy, g:EclimSignLevel, 'info')
  endif
endfunction " }}}

function! eclim#AddVimSetting(namespace, name, default, desc, ...) " {{{
  " Optional args:
  "   regex: regular expression used to validate the setting's value.
  if !has_key(s:vim_settings, a:namespace)
    let s:vim_settings[a:namespace] = {}
  endif
  let name = substitute(a:name, 'g:Eclim', '', '')
  let s:vim_settings[a:namespace][name] = {'desc': a:desc}
  let s:vim_settings_defaults[name] = a:default
  let s:vim_settings_types[name] = type(a:default)
  let regex = a:0 ? a:1 : ''
  if regex != ''
    let s:vim_settings_validators[name] = regex
    if exists(a:name)
      exec 'let value = ' . a:name
      if value !~ '^' . regex . '$'
        echo a:name . ' must match ' . regex
        exec 'unlet ' . a:name
      endif
    endif
  endif

  if !exists(a:name)
    exec 'let ' . a:name . ' = ' . string(a:default)
  endif
endfunction " }}}

function! eclim#VimSettings() " {{{
  let content = [
      \ "# Eclim's global vim settings",
      \ "# The settings here allow you to configure the vim side behavior of eclim.",
      \ "# You can use <cr> on a setting name to open the eclim docs for that setting.",
      \ "#",
      \ "# Note: If you have g:EclimXXX variables set in your .vimrc, those will take",
      \ "# precedence over any changes you make here.",
    \ ]
  for namespace in sort(keys(s:vim_settings))
    let content += ['', '# ' . namespace . ' {{{']
    for name in sort(keys(s:vim_settings[namespace]))
      let setting = s:vim_settings[namespace][name]
      let desc = split(setting.desc, '\n')
      let content += map(desc, "'\t# ' . v:val")
      exec 'let value = string(g:Eclim' . name . ')'
      call add(content, "\t" . name . '=' . value)
    endfor
    let content += ['# }}}']
  endfor

  call eclim#util#TempWindow("Vim_Settings", content)
  setlocal buftype=acwrite
  setlocal filetype=jproperties
  setlocal noreadonly
  setlocal modifiable
  setlocal foldmethod=marker
  setlocal foldmarker={{{,}}}
  nnoremap <buffer> <cr> :call <SID>VimSettingHelp()<cr>

  augroup eclim_settings
    autocmd! BufWriteCmd <buffer>
    autocmd BufWriteCmd <buffer> call eclim#SaveVimSettings()
  augroup END
endfunction " }}}

function! s:VimSettingHelp() " {{{
  let pos = getpos('.')
  try
    call cursor(0, 1)
    normal! w
    let syntax =  synIDattr(synID(line('.'), col('.'), 1), 'name')
  finally
    call setpos('.', pos)
  endtry

  if syntax == 'jpropertiesIdentifier'
    let line = getline('.')
    let name = substitute(line, '^\s*\(\w\+\)=.*', '\1', '')
    if name != line
      exec 'EclimHelp g:Eclim' . name
    endif
  endif
endfunction " }}}

function! eclim#SaveVimSettings() " {{{
  " get all lines, filtering out comments and blank lines
  let lines = filter(getline(1, line('$')), 'v:val !~ "^\\s*\\(#\\|$\\)"')

  " convert lines into a settings dict
  let index = 0
  let settings = {}
  let pattern = '^\s*\([[:alnum:]_.-]\+\)\s*=\s*\(.*\)'
  let errors = []
  while index < len(lines)
    try
      if lines[index] =~ pattern
        let name = substitute(lines[index], pattern, '\1', '')
        if !has_key(s:vim_settings_types, name)
          continue
        endif

        let value = substitute(lines[index], pattern, '\2', '')
        while value =~ '\\$'
          let index += 1
          let value = substitute(value, '\\$', '', '')
          let value .= substitute(lines[index], '^\s*', '', '')
        endwhile
        let value = substitute(value, "\\(^['\"]\\|['\"]$\\)", '', 'g')

        if has_key(s:vim_settings_validators, name)
          let regex = s:vim_settings_validators[name]
          if value !~ '^' . regex . '$'
            let [line, col] = searchpos('^\s*' . name . '=', 'nw')
            call add(errors, {
                \ 'filename': expand('%'),
                \ 'message': name . ': must match ' . regex,
                \ 'line': line,
                \ 'column': col,
                \ 'type': 'error',
              \ })
            continue
          endif
        endif

        if s:vim_settings_types[name] != g:STRING_TYPE
          try
            let typed_value = eval(value)
            unlet value
            let value = typed_value
          catch /E121\|E115/
            let [line, col] = searchpos('^\s*' . name . '=', 'nw')
            call add(errors, {
                \ 'filename': expand('%'),
                \ 'message': name . ': ' . v:exception,
                \ 'line': line,
                \ 'column': col,
                \ 'type': 'error',
              \ })
            continue
          endtry
        endif

        let default = s:vim_settings_defaults[name]
        if value != default
          let settings[name] = value
        endif
      endif
    finally
      let index += 1
      unlet! value typed_value default
    endtry
  endwhile

  if len(errors)
    call eclim#util#SetLocationList(eclim#util#ParseLocationEntries(errors))
    call eclim#util#EchoError(
      \ len(errors) . ' error' . (len(errors) > 1 ? 's' : '') . ' found.')
    return
  endif

  call eclim#util#ClearLocationList()

  if !isdirectory(eclim#UserHome() . '/.eclim')
    call mkdir(eclim#UserHome() . '/.eclim')
  endif
  let settings_file = eclim#UserHome() . '/.eclim/.eclim_settings'
  if writefile([string(settings)], settings_file) == 0
    call eclim#util#Echo('Settings saved. You may need to restart vim for all changes to take affect.')
  else
    call eclim#util#Echo('Unable to write settings.')
  endif

  setlocal nomodified
endfunction " }}}

function! eclim#UserHome() " {{{
  if exists('g:EclimUserHome')
    let home = g:EclimUserHome
  else
    let home = $HOME
  endif
  return substitute(home, '\', '/', 'g')
endfunction " }}}

function! eclim#WaitOnRunningJobs(timeout) " {{{
  " Args:
  "   timeout: max time to wait in milliseconds
  let running = 1
  let waited = 0
  while running && waited < a:timeout
    let jobs = eclim#Execute(s:command_jobs)
    if type(jobs) == g:LIST_TYPE
      let running = 0
      for job in jobs
        if job.status == 'running'
          call eclim#util#EchoDebug('Wait on job: ' . job.job)
          let running = 1
          let waited += 300
          sleep 300m
          break
        endif
      endfor
    endif
  endwhile
  if running
    call eclim#util#EchoDebug('Timeout exceeded waiting on jobs')
  endif
endfunction " }}}

" vim:ft=vim:fdm=marker
