"=============================================================================
" File    : autoload/unite/source/outline/modules/util.vim
" Author  : h1mesuke <himesuke@gmail.com>
" Updated : 2012-01-11
" Version : 0.5.1
" License : MIT license {{{
"
"   Permission is hereby granted, free of charge, to any person obtaining
"   a copy of this software and associated documentation files (the
"   "Software"), to deal in the Software without restriction, including
"   without limitation the rights to use, copy, modify, merge, publish,
"   distribute, sublicense, and/or sell copies of the Software, and to
"   permit persons to whom the Software is furnished to do so, subject to
"   the following conditions:
"   
"   The above copyright notice and this permission notice shall be included
"   in all copies or substantial portions of the Software.
"   
"   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
"   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
"   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
"   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
"   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
"   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
"   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
" }}}
"=============================================================================

let s:save_cpo = &cpo
set cpo&vim

function! unite#sources#outline#modules#util#import() abort
  return s:Util
endfunction

"-----------------------------------------------------------------------------

function! s:get_SID() abort
  return matchstr(expand('<sfile>'), '<SNR>\d\+_')
endfunction
let s:SID = s:get_SID()
delfunction s:get_SID

let s:Util = unite#sources#outline#modules#base#new('Util', s:SID)

"-----------------------------------------------------------------------------
" Heading

function! s:Util_get_indent_level(context, lnum) abort
  let line = a:context.lines[a:lnum]
  let sw = a:context.buffer.sw
  let ts = a:context.buffer.ts
  let indent = substitute(matchstr(line, '^\s*'), '\t', repeat(' ', ts), 'g')
  return strlen(indent) / sw + 1
endfunction
call s:Util.function('get_indent_level')

function! s:Util_get_comment_heading_level(context, lnum, ...) abort
  let line = a:context.lines[a:lnum]
  if line =~ '^\s'
    let level =  (a:0 ? a:1 : s:Util_get_indent_level(a:context, a:lnum) + 3)
  else
    let level = (strlen(substitute(line, '\s*', '', 'g')) > 40 ? 2 : 3)
    let level -= (line =~ '=')
  endif
  return level
endfunction
call s:Util.function('get_comment_heading_level')

"-----------------------------------------------------------------------------
" Matching

" join_to( {context}, {lnum}, {pattern} [, {limit}])
function! s:Util_join_to(context, lnum, pattern, ...) abort
  let lines = a:context.lines
  let limit = (a:0 ? a:1 : 3)
  if limit < 0
    return s:join_to_backward(lines, a:lnum, a:pattern, limit * -1)
  endif
  let lnum = a:lnum
  let limit = min([a:lnum + limit, len(lines) - 1])
  while lnum <= limit
    let line = lines[lnum]
    if line =~# a:pattern
      break
    endif
    let lnum += 1
  endwhile
  return join(lines[a:lnum : lnum], "\n")
endfunction
call s:Util.function('join_to')

function! s:join_to_backward(context, lnum, pattern, limit) abort
  let lines = a:context.lines
  let lnum = max([1, a:lnum - a:limit])
  while lnum > 0
    let line = lines[lnum]
    if line =~# a:pattern
      break
    endif
    let lnum -= 1
  endwhile
  return join(lines[lnum : a:lnum], "\n")
endfunction

function! s:Util_join_to_rparen(context, lnum, ...) abort
  let limit = (a:0 ? a:1 : 3)
  let line = s:Util_join_to(a:context, a:lnum, ')', limit)
  let line = substitute(line, "\\s*\n\\s*", ' ', 'g')
  let line = substitute(line, ')\zs.*$', '', '')
  return line
endfunction
call s:Util.function('join_to_rparen')

" neighbor_match( {context}, {lnum}, {pattern} [, {range} [, {exclusive}])
function! s:Util_neighbor_match(context, lnum, pattern, ...) abort
  let lines = a:context.lines
  let range = get(a:000, 0, 1)
  let exclusive = !!get(a:000, 1, 0)
  if type(range) == type([])
    let [prev, next] = range
  else
    let [prev, next] = [range, range]
  endif
  let [bwd_range, fwd_range] = s:neighbor_ranges(a:context, a:lnum, prev, next, exclusive)
  for lnum in bwd_range
    if lines[lnum] =~# a:pattern
      return 1
    endif
  endfor
  for lnum in fwd_range
    if lines[lnum] =~# a:pattern
      return 1
    endif
  endfor
  return 0
endfunction
call s:Util.function('neighbor_match')

function! s:neighbor_ranges(context, lnum, prev, next, exclusive) abort
  let max_lnum = len(a:context.lines) - 1
  let bwd_range = range(max([1, a:lnum - a:prev]), max([1, a:lnum - a:exclusive]))
  let fwd_range = range(min([a:lnum + a:exclusive, max_lnum]), min([a:lnum + a:next, max_lnum]))
  return [bwd_range, fwd_range]
endfunction

" neighbor_matchstr( {context}, {lnum}, {pattern} [, {range} [, {exclusive}])
function! s:Util_neighbor_matchstr(context, lnum, pattern, ...) abort
  let lines = a:context.lines
  let range = get(a:000, 0, 1)
  let exclusive = !!get(a:000, 1, 0)
  if type(range) == type([])
    let [prev, next] = range
  else
    let [prev, next] = [range, range]
  endif
  let [bwd_range, fwd_range] = s:neighbor_ranges(a:context, a:lnum, prev, next, exclusive)
  for lnum in bwd_range
    let matched = matchstr(lines[lnum], a:pattern)
    if !empty(matched)
      return matched
    endif
  endfor
  for lnum in fwd_range
    let matched = matchstr(lines[lnum], a:pattern)
    if !empty(matched)
      return matched
    endif
  endfor
  return ""
endfunction
call s:Util.function('neighbor_matchstr')

let s:SHARED_PATTERNS = {
      \ '*': {
      \   'parameter_list': '\S\s*\zs([^)]*)',
      \   'parameter_list_and_after': '\S\s*\zs([^)]*).*$',
      \   'after_lbrace'  : '{.*$',
      \   'after_lbracket': '[.*$',
      \   'after_lparen'  : '(.*$',
      \   'after_colon'   : ':.*$',
      \ },
      \ 'c': {
      \   'heading-1': '^\s*\/\*\s*[-=*]\{10,}\s*$',
      \   'header'   : ['^/\*', '\*/\s*$'],
      \ },
      \ 'cpp': {
      \   'heading-1': '^\s*/[/*]\s*[-=/*]\{10,}\s*$',
      \   'header'   : {
      \     'leading': '^//',
      \     'block'  : ['^/\*', '\*/\s*$'],
      \   },
      \ },
      \ 'sh': {
      \   'heading-1': '^\s*#\s*[-=#]\{10,}\s*$',
      \   'header'   : '^#',
      \ },
      \}

function! s:Util_shared_pattern(filetype, which) abort
  return s:SHARED_PATTERNS[a:filetype][a:which]
endfunction
call s:Util.function('shared_pattern')

"-----------------------------------------------------------------------------
" List

let s:List = unite#sources#outline#modules#base#new('List', s:SID)
let s:Util.List = s:List

function! s:List_sort_by_lnum(dicts) abort
  return sort(a:dicts, 's:compare_by_lnum')
endfunction
function! s:compare_by_lnum(d1, d2) abort
  let n1 = a:d1.lnum
  let n2 = a:d2.lnum
  return n1 == n2 ? 0 : n1 > n2 ? 1 : -1
endfunction
call s:List.function('sort_by_lnum')

unlet s:List

"-----------------------------------------------------------------------------
" Path

let s:Path = unite#sources#outline#modules#base#new('Path', s:SID)
let s:Util.Path = s:Path

" Path.normalize( {path} [, {mods}])
function! s:Path_normalize(path, ...) abort
  let path = a:path
  if a:0
    let mods = a:0
    let path = fnamemodify(path, mods)
  endif
  let path = substitute(path, '[/\\]', '/', 'g')
  return path
endfunction
call s:Path.function('normalize')

unlet s:Path

"-----------------------------------------------------------------------------
" String

let s:String = unite#sources#outline#modules#base#new('String', s:SID)
let s:Util.String = s:String

" String.capitalize( {str} [, {flag}])
function! s:String_capitalize(str, ...) abort
  let flag = (a:0 ? a:1 : '')
  return substitute(a:str, '\<\(\h\)\(\w\+\)\>', '\u\1\L\2', flag)
endfunction
call s:String.function('capitalize')

" Ported from:
" Sample code from Programing Ruby, page 145
"
function! s:String_nr2roman(nr) abort
  if a:nr <= 0 || 4999 < a:nr
    return string(a:nr)
  endif
  let factors = [
        \ ["M", 1000], ["CM", 900], ["D",  500], ["CD", 400],
        \ ["C",  100], ["XC",  90], ["L",   50], ["XL",  40],
        \ ["X",   10], ["IX",   9], ["V",    5], ["IV",   4],
        \ ["I",    1],
        \]
  let nr = a:nr
  let roman = ""
  for [code, factor] in factors
    let cnt = nr / factor
    let nr  = nr % factor
    if cnt > 0
      let roman .= repeat(code, cnt)
    endif
  endfor
  return roman
endfunction
call s:String.function('nr2roman')

function! s:String_shellescape(str) abort
  if &shell =~? '^\%(cmd\%(\.exe\)\=\|command\.com\)\%(\s\|$\)' || unite#util#is_windows()
    return '"' . substitute(a:str, '"', '""', 'g') . '"'
  else
    return "'" . substitute(a:str, "'", "'\\\\''", 'g') . "'"
  endif
endfunction
call s:String.function('shellescape')

unlet s:String

"-----------------------------------------------------------------------------
" Misc

function! s:Util_print_debug(which, msg) abort
  if get(g:, 'unite_source_outline_' . a:which . '_debug', 0)
    echomsg "unite-outline: " . a:msg
  endif
endfunction
call s:Util.function('print_debug')

function! s:Util_print_progress(msg) abort
  echon a:msg
  redraw
endfunction
call s:Util.function('print_progress')

function! s:Util__cpp_is_in_comment(heading_line, matched_line) abort
  return ((a:matched_line =~ '^\s*//'  && a:heading_line =~ '^\s*//') ||
        \ (a:matched_line =~ '^\s*/\*' && a:matched_line !~ '\*/\s*$'))
endfunction
call s:Util.function('_cpp_is_in_comment')

let &cpo = s:save_cpo
unlet s:save_cpo