"" Guard
if exists('g:autoloaded_unobtrusive_folds') || &compatible
  finish
endif
let g:autoloaded_unobtrusive_folds = 1

"" Configuration

""
" @section Configuration, config
" Since this method of folding is more involved than the built-in ones it can
" be slow for very big files. Options are provided to mitigate this.

""" g:unobtrusive_fold_max_lines
if !exists('g:unobtrusive_fold_max_lines')
  ""
  " Set to non-zero value to make the commands below do nothing if the number
  " of lines in the current buffer is higher than the given value. Defaults to
  " 0.
  let g:unobtrusive_fold_max_lines = 0
endif

""" g:unobtrusive_fold_max_prevlines
if !exists('g:unobtrusive_fold_max_prevlines')
  ""
  " Set to non-zero value to look at at most this many previous lines when
  " calculating folds. Defaults to 0.
  let g:unobtrusive_fold_max_prevlines = 0
endif

"" Constants

let s:pattern_prefix = '\V\c\^\s\*'

"" Functions

""" #text()

""
" @public
" Set 'foldtext' to this function to get an improved version of
" `getline(v:foldstart)`. Leading tabs are expanded to 'tabstop' spaces. If
" 'fillchars' `fold:` is not space, the number of lines included in the fold is
" printed, padded with `fold:` so that it lines up with one column short of the
" right edge of the window, or 'textwidth' (if not 0), whichever is smaller.
" The empty column at the far right is nice for people who set 'colorcolumn' to
" `+1`. The rest of the line is cleared (i.e. not filled automatically by Vim
" with 'fillchars' `fold:`). A good value for `fold:` might be the box drawing
" character `─`.
function! unobtrusive_fold#text() abort
  let line = getline(v:foldstart)
  let line = substitute(line, '\t', repeat(' ', &l:tabstop), 'g')
  let fillchar = matchstr(&fillchars, '.*\(^\|,\)fold:\zs.\ze')
  if empty(fillchar)
    let fillchar = '-'
  endif
  if fillchar !=# ' '
    let foldlines = v:foldend - v:foldstart + 1
    let signs = len(split(execute('sign place buffer=' . bufnr()), '\n')) - 1
    let numbers = &l:number ? line('$') : &l:relativenumber ? winheight(0) : 0
    let winwidth = winwidth(0)
    \ - &l:foldcolumn
    \ - (signs && &l:signcolumn ==# 'auto' || &l:signcolumn ==# 'yes' ? 2 : 0)
    \ - (numbers ? max([&l:numberwidth, strlen(numbers) + 1]) : 0)
    let width = winwidth - 1
    if &l:textwidth
      let width = min([width, &l:textwidth])
    endif
    let fill = repeat(fillchar, width - strlen(line) - strlen(foldlines) - 2)
    let pad = repeat(' ', winwidth - width)
    let line = line . ' ' . fill . ' ' . foldlines . pad
  endif
  return line
endfunction

""" #comment()

function! unobtrusive_fold#comment(bang) abort
  if empty(&l:commentstring)
    return
  endif
  let commentstart = escape(split(&l:commentstring, '\s*%s')[0], '\')
  let commentlast  = escape(commentstart[-1:],                   '\')
  call unobtrusive_fold#set(
  \   a:bang,
  \   commentstart . '\zs' . commentlast . '\+\ze',
  \   commentstart,
  \   []
  \ )
endfunction

""" #char()

function! unobtrusive_fold#char(bang, char) abort
  let char = escape(a:char, '\')
  call unobtrusive_fold#set(
  \   a:bang,
  \   '\zs' . char . '\+\ze',
  \   '',
  \   ['Comment', 'Code', 'Snippet']
  \ )
endfunction

""" #set()

function! unobtrusive_fold#set(bang, pattern, commentstart, ignore) abort
  if g:unobtrusive_fold_max_lines && line('$') > g:unobtrusive_fold_max_lines
    return
  endif
  let w:unobtrusive_fold_indent       = !empty(a:bang)
  let w:unobtrusive_fold_pattern      = a:pattern . '\s\*\S'
  let w:unobtrusive_fold_ignore       = a:ignore
  let w:unobtrusive_fold_commentstart = a:commentstart
  let &l:foldexpr   = 'unobtrusive_fold#expr()'
  let &l:foldmethod = 'expr'
endfunction

""" s:foldlevel()

function! s:foldlevel(lnum) abort
  let foldlevel = strlen(matchstr(
  \   getline(a:lnum),
  \   s:pattern_prefix . w:unobtrusive_fold_pattern
  \ ))
  if foldlevel && !empty(w:unobtrusive_fold_ignore)
    for syntax in map(synstack(a:lnum, 1), 'synIDattr(v:val, "name")')
      for ignore in w:unobtrusive_fold_ignore
        if syntax =~# '\v' . '(^|[a-z])' . ignore . '([A-Z]|$)'
          return 0
        endif
      endfor
    endfor
  endif
  return foldlevel
endfunction

""" s:indent()

function! s:indent(lnum, ...) abort
  if (a:lnum < 1 && line('$') < a:lnum) || (a:0 && s:foldlevel(a:lnum))
    return 0
  endif
  return !empty(getline(a:lnum)) + indent(a:lnum) / shiftwidth()
endfunction

""" s:prevfoldlevelbase()

function! s:prevfoldlevelbase(lnum) abort
  let stopline = g:unobtrusive_fold_max_prevlines
  \ ? max(1, a:lnum - g:unobtrusive_fold_max_prevlines)
  \ : 0
  let pattern_column = '\%<' . (indent(a:lnum) + 2) . 'v'
  let pos = getpos('.')
  call cursor(a:lnum, 1)
  let prev = search(
  \   s:pattern_prefix . pattern_column . w:unobtrusive_fold_pattern,
  \   'bW',
  \   stopline
  \ )
  call setpos('.', pos)
  if !prev
    return [0, 0]
  endif
  let foldlevel = s:foldlevel(prev)
  return [foldlevel, foldlevel - indent(prev) / shiftwidth()]
endfunction

""" s:nextnonblank()

function! s:nextnonblank(lnum)
  if empty(w:unobtrusive_fold_commentstart)
    return nextnonblank(a:lnum)
  endif
  let pos = getpos('.')
  call cursor(a:lnum, 1)
  let next = search(
  \   '\V\^\(' . w:unobtrusive_fold_commentstart . '\[[:punct:]]\*\$\)\@!\.',
  \   'Wc'
  \ )
  call setpos('.', pos)
  return next
endfunction

""" #expr()

function! unobtrusive_fold#expr() abort
  """" Empty line
  if empty(getline(v:lnum))
    return '='
  endif
  """" Marker...
  """" ...start
  let foldlevel = s:foldlevel(v:lnum)
  if foldlevel
    return '>' . foldlevel
  endif
  """" ...end...
  let next = s:nextnonblank(v:lnum + 1)
  """" ...foldlevel
  let nextfoldlevel = s:foldlevel(next)
  if nextfoldlevel
    let prevfoldlevel = s:prevfoldlevelbase(v:lnum)[0]
    return '<' . (min([nextfoldlevel, prevfoldlevel]) + 1)
  endif
  """" ...indent
  if indent(next) < indent(v:lnum)
    let prevfoldlevel = s:prevfoldlevelbase(v:lnum)[0]
    let nextfoldlevel = s:prevfoldlevelbase(next)[0]
    if nextfoldlevel < prevfoldlevel
      return '<' . (nextfoldlevel + 1)
    endif
  endif
  """" Indent...
  if w:unobtrusive_fold_indent
    let indent      = s:indent(v:lnum)
    let nextindent  = s:indent(next)
    let aboveindent = s:indent(v:lnum - 1, 1)
    let belowindent = s:indent(v:lnum + 1, 1)
    """" ...start
    if aboveindent < indent && (belowindent >= indent || nextindent > indent)
      let prevbase = s:prevfoldlevelbase(v:lnum)[1]
      return '>' . (prevbase + indent)
    endif
    """" ...end
    if belowindent < indent && (aboveindent || belowindent)
      let prevbase = s:prevfoldlevelbase(v:lnum)[1]
      return '<' . (prevbase + min([indent, nextindent + !!belowindent]))
    endif
  endif
  """" No change
  return '='
endfunction

""" #debug()

function! unobtrusive_fold#debug() abort
  let lines  = range(1, line('$'))
  let exprs  = map(copy(lines), 'execute("let v:lnum=v:val|echon ".&foldexpr)')
  for l in lines
    let e = exprs[l-1]
    if len(e) == 1
      let exprs[l-1] = e =~# '^[0-9]$' ? ' '.e : e.' '
    endif
  endfor
  let levels = map(copy(lines), 'foldlevel(v:val)')
  let debugs = map(copy(lines), '"[".exprs[v:val-1]."|".levels[v:val-1]."]"')

  let pos = getpos('.')
  call setreg('"', join(debugs, "\n"), 'b')
  normal! gg0""P
  call setpos('.', pos)
endfunction
