" Snippet definition parsing code

function! s:sfile() abort
    return expand('<sfile>')
endfunction

let s:parser_proto = {}
let s:special_chars = "$`\n"

function! s:new_parser(text) abort
    let ret = copy(s:parser_proto)
    let ret.input = a:text
    let ret.len = strlen(ret.input)
    let ret.pos = -1
    let ret.indent = 0
    let ret.value = []
    let ret.vars = {}
    let ret.stored_lines = []
    call ret.advance()
    return ret
endfunction

function! s:parser_advance(...) dict abort
    let self.pos += a:0 ? a:1 : 1
    let self.next = self.input[self.pos]
endfunction

function! s:parser_same(tok) dict abort
    if self.next == a:tok
        call self.advance()
        return 1
    else
        return 0
    endif
endfunction

function! s:parser_id() dict abort
    if self.input[(self.pos):(self.pos+5)] == 'VISUAL'
        call self.advance(6)
        return 'VISUAL'
    elseif self.next =~ '\d'
        let end = matchend(self.input, '\d\+', self.pos)
        let res = strpart(self.input, self.pos, end - self.pos)
        call self.advance(end - self.pos)
        return +res " force conversion to Number
    endif
    return -1
endfunction

function! s:parser_add_var(var) dict abort
    let id = a:var[0]
    if !has_key(self.vars, id)
        let self.vars[id] = { 'instances' : [] }
    endif
    call add(self.vars[id].instances, a:var)
endfunction

function! s:parser_var() dict abort
    let ret = []
    if self.same('{')
        let id = self.id()
        if id >= 0
            call add(ret, id)
            call extend(ret, self.varend())
        endif
    else
        let id = self.id()
        if id >= 0
            call add(ret, id)
        endif
    endif
    return ret
endfunction

function! s:parser_varend() dict abort
    let ret = []
    if self.same(':')
        call extend(ret, self.placeholder())
    elseif self.same('/')
        call add(ret, self.subst())
    endif
    call self.same('}')
    return ret
endfunction

function! s:parser_placeholder() dict abort
    let ret = self.text('}')
    return empty(ret) ? [''] : ret
endfunction

function! s:parser_subst() dict abort
    let ret = {}
    let ret.pat = self.pat()
    if self.same('/')
        let ret.sub = self.pat(1)
    endif
    if self.same('/')
        let ret.flags = self.pat(1)
    endif
    return ret
endfunction

function! s:parser_pat(...) dict abort
    let val = ''

    while self.pos < self.len
        if self.same('\')
            if self.next == '/'
                let val .= '/'
                call self.advance()
            elseif a:0 && self.next == '}'
                let val .= '}'
                call self.advance()
            else
                let val .= '\'
            endif
        elseif self.next == '/' || a:0 && self.next == '}'
            break
        else
            let val .= self.next
            call self.advance()
        endif
    endwhile

    return val
endfunction

function! s:parser_expr() dict abort
    let str = self.string('`')
    call self.same('`')
    return snipmate#util#eval(str)
endfunction

function! s:parser_string(till, ...) dict abort
    let val = ''
    let till = '\V\[' . escape(a:till, '\') . ']'

    while self.pos < self.len
        if self.same('\')
            if self.next != "\n"
                let val .= self.next
            endif
            call self.advance()
        elseif self.next =~# till
            break
        elseif self.next == "\t"
            let self.indent += 1
            let val .= s:indent(1)
            call self.advance()
        else
            let val .= self.next
            call self.advance()
        endif
    endwhile

    return val
endfunction

function! s:join_consecutive_strings(list) abort
    let list = a:list
    let pos = 0
    while pos + 1 < len(list)
        if type(list[pos]) == type('') && type(list[pos+1]) == type('')
            let list[pos] .= list[pos+1]
            call remove(list, pos + 1)
        else
            let pos += 1
        endif
    endwhile
endfunction

function! s:parser_text(till) dict abort
    let ret = []
    let target = ret

    while self.pos < self.len
        let lines = []

        if self.same('$')
            let var = self.var()
            if !empty(var)
                if var[0] is# 'VISUAL'
                    let lines = s:visual_placeholder(var, self.indent)
                    " Remove trailing newline. See #245
                    if lines[-1] == '' && self.next == "\n"
                        call remove(lines, -1)
                    endif
                elseif var[0] >= 0
                    call add(target, var)
                    call self.add_var(var)
                endif
            endif
        elseif self.same('`')
            let lines = split(self.expr(), "\n", 1)
        else
            let lines = [self.string(a:till . s:special_chars)]
        endif

        if !empty(lines)
            call add(target, lines[0])
            call extend(self.stored_lines, lines[1:-2])
            " Don't change targets if there's only one line
            if exists("lines[1]")
                let target = [lines[-1]]
            endif
        endif

        " Empty lines are ignored if this is tested at the start of an iteration
        if self.next ==# a:till
            break
        endif
    endwhile

    call s:join_consecutive_strings(ret)
    if target isnot ret
        call s:join_consecutive_strings(target)
        call extend(self.stored_lines, target)
    endif

    return ret
endfunction

function! s:parser_line() dict abort
    let ret = []
    if !empty(self.stored_lines)
        call add(ret, remove(self.stored_lines, 0))
    else
        call extend(ret, self.text("\n"))
        call self.same("\n")
    endif
    let self.indent = 0
    return ret
endfunction

function! s:parser_parse() dict abort
    while self.pos < self.len || !empty(self.stored_lines)
        let line = self.line()
        call add(self.value, line)
    endwhile
endfunction

function! s:indent(count) abort
    if &expandtab
        let shift = repeat(' ', snipmate#util#tabwidth())
    else
        let shift = "\t"
    endif
    return repeat(shift, a:count)
endfunction

function! s:visual_placeholder(var, indent) abort
    let arg = get(a:var, 1, {})
    if type(arg) == type({})
        let pat = get(arg, 'pat', '')
        let sub = get(arg, 'sub', '')
        let flags = get(arg, 'flags', '')
        let content = split(substitute(get(b:, 'snipmate_visual', ''), pat, sub, flags), "\n", 1)
    else
        let content = split(get(b:, 'snipmate_visual', arg), "\n", 1)
    endif

    let indent = s:indent(a:indent)
    call map(content, '(v:key != 0) ? indent . v:val : v:val')

    return content
endfunction

function! s:parser_create_stubs() dict abort

    for [id, dict] in items(self.vars)
        for i in dict.instances
            if len(i) > 1 && type(i[1]) != type({})
                if !has_key(dict, 'placeholder')
                    let dict.placeholder = i[1:]
                    call add(i, dict)
                else
                    unlet i[1:]
                    call s:create_mirror_stub(i, dict)
                endif
            else
                call s:create_mirror_stub(i, dict)
            endif
        endfor
        if !has_key(dict, 'placeholder')
            let dict.placeholder = []
            let j = 0
            while len(dict.instances[j]) > 2
                let j += 1
            endwhile
            let oldstub = remove(dict.instances[j], 1, -1)[-1]
            call add(dict.instances[j], '')
            call add(dict.instances[j], dict)
            call filter(dict.mirrors, 'v:val isnot oldstub')
        endif
        unlet dict.instances
    endfor

endfunction

function! s:create_mirror_stub(mirror, dict)
    let mirror = a:mirror
    let dict = a:dict
    let stub = get(mirror, 1, {})
    call add(mirror, stub)
    let dict.mirrors = get(dict, 'mirrors', [])
    call add(dict.mirrors, stub)
endfunction

function! snipmate#parse#snippet(text, ...) abort
    let parser = s:new_parser(a:text)
    call parser.parse()
    if !(a:0 && a:1)
        call parser.create_stubs()
    endif
    unlet! b:snipmate_visual
    return [parser.value, parser.vars]
endfunction

call extend(s:parser_proto, snipmate#util#add_methods(s:sfile(), 'parser',
            \ [ 'advance', 'same', 'id', 'add_var', 'var', 'varend',
            \   'line', 'string', 'create_stubs', 'pat',
            \   'placeholder', 'subst', 'expr', 'text', 'parse',
            \ ]), 'error')
