" Vim filetype plugin file
" filetype: ledger
" by Johann Klähn; Use according to the terms of the GPL>=2.
" vim:ts=2:sw=2:sts=2:foldmethod=marker

if exists("b:did_ftplugin")
  finish
endif

let b:did_ftplugin = 1

let b:undo_ftplugin = "setlocal ".
                    \ "foldtext< ".
                    \ "include< comments< commentstring< omnifunc< formatprg<"

if !exists('current_compiler')
  compiler ledger
endif

setl foldtext=LedgerFoldText()
setl include=^!\\?include
setl comments=b:;
setl commentstring=;%s
setl omnifunc=LedgerComplete

" set location of ledger binary for checking and auto-formatting
if ! exists("g:ledger_bin") || empty(g:ledger_bin) || ! executable(g:ledger_bin)
  if executable('ledger')
    let g:ledger_bin = 'ledger'
  else
    unlet! g:ledger_bin
    echohl WarningMsg
    echomsg "ledger command not found. Set g:ledger_bin or extend $PATH ".
          \ "to enable error checking and auto-formatting."
    echohl None
  endif
endif

if exists("g:ledger_bin")
  exe 'setl formatprg='.substitute(g:ledger_bin, ' ', '\\ ', 'g').'\ -f\ -\ print'
endif

if !exists('g:ledger_extra_options')
  let g:ledger_extra_options = ''
endif

if !exists('g:ledger_date_format')
  let g:ledger_date_format = '%Y/%m/%d'
endif

" You can set a maximal number of columns the fold text (excluding amount)
" will use by overriding g:ledger_maxwidth in your .vimrc.
" When maxwidth is zero, the amount will be displayed at the far right side
" of the screen.
if !exists('g:ledger_maxwidth')
  let g:ledger_maxwidth = 0
endif

if !exists('g:ledger_fillstring')
  let g:ledger_fillstring = ' '
endif

if !exists('g:ledger_decimal_sep')
  let g:ledger_decimal_sep = '.'
endif

if !exists('g:ledger_align_at')
  let g:ledger_align_at = 60
endif

if !exists('g:ledger_default_commodity')
  let g:ledger_default_commodity = ''
endif

if !exists('g:ledger_commodity_before')
  let g:ledger_commodity_before = 1
endif

if !exists('g:ledger_commodity_sep')
  let g:ledger_commodity_sep = ''
endif

" If enabled this will list the most detailed matches at the top {{{
" of the completion list.
" For example when you have some accounts like this:
"   A:Ba:Bu
"   A:Bu:Bu
" and you complete on A:B:B normal behaviour may be the following
"   A:B:B
"   A:Bu:Bu
"   A:Bu
"   A:Ba:Bu
"   A:Ba
"   A
" with this option turned on it will be
"   A:B:B
"   A:Bu:Bu
"   A:Ba:Bu
"   A:Bu
"   A:Ba
"   A
" }}}
if !exists('g:ledger_detailed_first')
  let g:ledger_detailed_first = 1
endif

" only display exact matches (no parent accounts etc.)
if !exists('g:ledger_exact_only')
  let g:ledger_exact_only = 0
endif

" display original text / account name as completion
if !exists('g:ledger_include_original')
  let g:ledger_include_original = 0
endif

" Settings for Ledger reports {{{
if !exists('g:ledger_main')
  let g:ledger_main = '%'
endif

if !exists('g:ledger_winpos')
  let g:ledger_winpos = 'B'  " Window position (see s:winpos_map)
endif

if !exists('g:ledger_use_location_list')
  let g:ledger_use_location_list = 0  " Use quickfix list by default
endif

if !exists('g:ledger_cleared_string')
  let g:ledger_cleared_string = 'Cleared: '
endif

if !exists('g:ledger_pending_string')
  let g:ledger_pending_string = 'Cleared or pending: '
endif

if !exists('g:ledger_target_string')
  let g:ledger_target_string = 'Difference from target: '
endif
" }}}

" Settings for the quickfix window {{{
if !exists('g:ledger_qf_register_format')
  let g:ledger_qf_register_format = '%(date) %-50(payee) %-30(account) %15(amount) %15(total)\n'
endif

if !exists('g:ledger_qf_reconcile_format')
  let g:ledger_qf_reconcile_format = '%(date) %-4(code) %-50(payee) %-30(account) %15(amount)\n'
endif

if !exists('g:ledger_qf_size')
  let g:ledger_qf_size = 10  " Size of the quickfix window
endif

if !exists('g:ledger_qf_vertical')
  let g:ledger_qf_vertical = 0
endif

if !exists('g:ledger_qf_hide_file')
  let g:ledger_qf_hide_file = 1
endif
" }}}

" Highlight groups for Ledger reports {{{
hi link LedgerNumber Number
hi link LedgerNegativeNumber Special
hi link LedgerCleared Constant
hi link LedgerPending Todo
hi link LedgerTarget Statement
hi link LedgerImproperPerc Special
" }}}

let s:rx_amount = '\('.
                \   '\%([0-9]\+\)'.
                \   '\%([,.][0-9]\+\)*'.
                \ '\|'.
                \   '[,.][0-9]\+'.
                \ '\)'.
                \ '\s*\%([[:alpha:]¢$€£]\+\s*\)\?'.
                \ '\%(\s*;.*\)\?$'

function! LedgerFoldText() "{{{1
  " find amount
  let amount = ""
  let lnum = v:foldstart + 1
  while lnum <= v:foldend
    let line = getline(lnum)

    " Skip metadata/leading comment
    if line !~ '^\%(\s\+;\|\d\)'
      " No comment, look for amount...
      let groups = matchlist(line, s:rx_amount)
      if ! empty(groups)
        let amount = groups[1]
        break
      endif
    endif
    let lnum += 1
  endwhile

  let fmt = '%s %s '
  " strip whitespace at beginning and end of line
  let foldtext = substitute(getline(v:foldstart),
                          \ '\(^\s\+\|\s\+$\)', '', 'g')

  " number of columns foldtext can use
  let columns = s:get_columns()
  if g:ledger_maxwidth
    let columns = min([columns, g:ledger_maxwidth])
  endif
  let columns -= s:multibyte_strlen(printf(fmt, '', amount))

  " add spaces so the text is always long enough when we strip it
  " to a certain width (fake table)
  if strlen(g:ledger_fillstring)
    " add extra spaces so fillstring aligns
    let filen = s:multibyte_strlen(g:ledger_fillstring)
    let folen = s:multibyte_strlen(foldtext)
    let foldtext .= repeat(' ', filen - (folen%filen))

    let foldtext .= repeat(g:ledger_fillstring,
                  \ s:get_columns()/filen)
  else
    let foldtext .= repeat(' ', s:get_columns())
  endif

  " we don't use slices[:5], because that messes up multibyte characters
  let foldtext = substitute(foldtext, '.\{'.columns.'}\zs.*$', '', '')

  return printf(fmt, foldtext, amount)
endfunction "}}}

function! LedgerComplete(findstart, base) "{{{1
  if a:findstart
    let lnum = line('.')
    let line = getline('.')
    let b:compl_context = ''
    if line =~ '^\s\+[^[:blank:];]' "{{{2 (account)
      " only allow completion when in or at end of account name
      if matchend(line, '^\s\+\%(\S \S\|\S\)\+') >= col('.') - 1
        " the start of the first non-blank character
        " (excluding virtual-transaction and 'cleared' marks)
        " is the beginning of the account name
        let b:compl_context = 'account'
        return matchend(line, '^\s\+[*!]\?\s*[\[(]\?')
      endif
    elseif line =~ '^\d' "{{{2 (description)
      let pre = matchend(line, '^\d\S\+\%(([^)]*)\|[*?!]\|\s\)\+')
      if pre < col('.') - 1
        let b:compl_context = 'description'
        return pre
      endif
    elseif line =~ '^$' "{{{2 (new line)
      let b:compl_context = 'new'
    endif "}}}
    return -1
  else
    if ! exists('b:compl_cache')
      let b:compl_cache = s:collect_completion_data()
      let b:compl_cache['#'] = changenr()
    endif
    let update_cache = 0

    let results = []
    if b:compl_context == 'account' "{{{2 (account)
      let hierarchy = split(a:base, ':')
      if a:base =~ ':$'
        call add(hierarchy, '')
      endif

      let results = ledger#find_in_tree(b:compl_cache.accounts, hierarchy)
      let exacts = filter(copy(results), 'v:val[1]')

      if len(exacts) < 1
        " update cache if we have no exact matches
        let update_cache = 1
      endif

      if g:ledger_exact_only
        let results = exacts
      endif

      call map(results, 'v:val[0]')

      if g:ledger_detailed_first
        let results = reverse(sort(results, 's:sort_accounts_by_depth'))
      else
        let results = sort(results)
      endif
    elseif b:compl_context == 'description' "{{{2 (description)
      let results = ledger#filter_items(b:compl_cache.descriptions, a:base)

      if len(results) < 1
        let update_cache = 1
      endif
    elseif b:compl_context == 'new' "{{{2 (new line)
      return [strftime(g:ledger_date_format)]
    endif "}}}


    if g:ledger_include_original
      call insert(results, a:base)
    endif

    " no completion (apart from a:base) found. update cache if file has changed
    if update_cache && b:compl_cache['#'] != changenr()
      unlet b:compl_cache
      return LedgerComplete(a:findstart, a:base)
    else
      unlet! b:compl_context
      return results
    endif
  endif
endf "}}}

" Deprecated functions {{{1
let s:deprecated = {
  \ 'LedgerToggleTransactionState': 'ledger#transaction_state_toggle',
  \ 'LedgerSetTransactionState': 'ledger#transaction_state_set',
  \ 'LedgerSetDate': 'ledger#transaction_date_set'
  \ }

for [s:old, s:new] in items(s:deprecated)
  let s:fun = "function! {s:old}(...)\nechohl WarningMsg\necho '" . s:old .
            \ " is deprecated. Use ".s:new." instead!'\nechohl None\n" .
            \ "call call('" . s:new . "', a:000)\nendf"
  exe s:fun
endfor
unlet s:old s:new s:fun
" }}}1

function! s:collect_completion_data() "{{{1
  let transactions = ledger#transactions()
  let cache = {'descriptions': [], 'tags': {}, 'accounts': {}}
  let accounts = ledger#declared_accounts()
  for xact in transactions
    " collect descriptions
    if has_key(xact, 'description') && index(cache.descriptions, xact['description']) < 0
      call add(cache.descriptions, xact['description'])
    endif
    let [t, postings] = xact.parse_body()
    let tagdicts = [t]

    " collect account names
    for posting in postings
      if has_key(posting, 'tags')
        call add(tagdicts, posting.tags)
      endif
      " remove virtual-transaction-marks
      let name = substitute(posting.account, '\%(^\s*[\[(]\?\|[\])]\?\s*$\)', '', 'g')
      if index(accounts, name) < 0
        call add(accounts, name)
      endif
    endfor

    " collect tags
    for tags in tagdicts | for [tag, val] in items(tags)
      let values = get(cache.tags, tag, [])
      if index(values, val) < 0
        call add(values, val)
      endif
      let cache.tags[tag] = values
    endfor | endfor
  endfor

  for account in accounts
    let last = cache.accounts
    for part in split(account, ':')
      let last[part] = get(last, part, {})
      let last = last[part]
    endfor
  endfor

  return cache
endf "}}}

" Helper functions {{{1

" return length of string with fix for multibyte characters
function! s:multibyte_strlen(text) "{{{2
   return strlen(substitute(a:text, ".", "x", "g"))
endfunction "}}}

" get # of visible/usable columns in current window
function! s:get_columns() " {{{2
  " As long as vim doesn't provide a command natively,
  " we have to compute the available columns.
  " see :help todo.txt -> /Add argument to winwidth()/

  let columns = (winwidth(0) == 0 ? 80 : winwidth(0)) - &foldcolumn
  if &number
    " line('w$') is the line number of the last line
    let columns -= max([len(line('w$'))+1, &numberwidth])
  endif

  " are there any signs/is the sign column displayed?
  redir => signs
  silent execute 'sign place buffer='.string(bufnr("%"))
  redir END
  if signs =~# 'id='
    let columns -= 2
  endif

  return columns
endf "}}}

function! s:sort_accounts_by_depth(name1, name2) "{{{2
  let depth1 = s:count_expression(a:name1, ':')
  let depth2 = s:count_expression(a:name2, ':')
  return depth1 == depth2 ? 0 : depth1 > depth2 ? 1 : -1
endf "}}}

function! s:count_expression(text, expression) "{{{2
  return len(split(a:text, a:expression, 1))-1
endf "}}}

function! s:autocomplete_account_or_payee(argLead, cmdLine, cursorPos) "{{{2
  return (a:argLead =~ '^@') ?
        \ map(filter(systemlist(g:ledger_bin . ' -f ' . shellescape(expand(g:ledger_main)) . ' payees'),
        \ "v:val =~? '" . strpart(a:argLead, 1) . "' && v:val !~? '^Warning: '"), '"@" . escape(v:val, " ")')
        \ :
        \ map(filter(systemlist(g:ledger_bin . ' -f ' . shellescape(expand(g:ledger_main)) . ' accounts'),
        \ "v:val =~? '" . a:argLead . "' && v:val !~? '^Warning: '"), 'escape(v:val, " ")')
endf "}}}

function! s:reconcile(file, account) "{{{2
  " call inputsave()
  let l:amount = input('Target amount' . (empty(g:ledger_default_commodity) ? ': ' : ' (' . g:ledger_default_commodity . '): '))
  " call inputrestore()
  call ledger#reconcile(a:file, a:account, str2float(l:amount))
endf "}}}

" Commands {{{1
command! -buffer -nargs=? -complete=customlist,s:autocomplete_account_or_payee
      \ Balance call ledger#show_balance(g:ledger_main, <q-args>)

command! -buffer -nargs=+ -complete=customlist,s:autocomplete_account_or_payee
      \ Ledger call ledger#output(ledger#report(g:ledger_main, <q-args>))

command! -buffer -range LedgerAlign <line1>,<line2>call ledger#align_commodity()

command! -buffer -nargs=1 -complete=customlist,s:autocomplete_account_or_payee
      \ Reconcile call <sid>reconcile(g:ledger_main, <q-args>)

command! -buffer -complete=customlist,s:autocomplete_account_or_payee -nargs=*
      \ Register call ledger#register(g:ledger_main, <q-args>)
" }}}

