" plugin to handle the TaskPaper to-do list format
" Language:     Taskpaper (http://hogbaysoftware.com/projects/taskpaper)
" Maintainer:   David O'Callaghan <david.ocallaghan@cs.tcd.ie>
" URL:          https://github.com/davidoc/taskpaper.vim
" Last Change:  2012-03-07

let s:save_cpo = &cpo
set cpo&vim

function! s:add_delete_tag(tag, value, add)
    let cur_line = getline(".")

    let tag = " @" . a:tag
    if a:value != ''
        let tag .= "(" . a:value . ")"
    endif

    " Add tag
    if a:add
        let new_line = cur_line . tag
        call setline(".", new_line)
        return 1
    endif

    " Delete tag
    if cur_line =~# '\V' . tag
        if a:value != ''
            let new_line = substitute(cur_line, '\V' . tag, "", "g")
        else
            let new_line = substitute(cur_line, '\V' . tag . '\v(\([^)]*\))?',
            \                         "", "g")
        endif

        call setline(".", new_line)
        return 1
    endif
    return 0
endfunction

function! taskpaper#add_tag(tag, ...)
    let value = a:0 > 0 ? a:1 : input('Value: ')
    return s:add_delete_tag(a:tag, value, 1)
endfunction

function! taskpaper#delete_tag(tag, ...)
    let value = a:0 > 0 ? a:1 : ''
    return s:add_delete_tag(a:tag, value, 0)
endfunction

function! taskpaper#swap_tag(oldtag, newtag)
    call taskpaper#delete_tag(a:oldtag)
    call taskpaper#add_tag(a:newtag, '')
endfunction

function! taskpaper#swap_tags(oldtags, newtags)
    for oldtag in a:oldtags
        call taskpaper#delete_tag(oldtag)
    endfor
    for newtag in a:newtags
        call taskpaper#add_tag(newtag, '')
    endfor
endfunction

function! taskpaper#toggle_tag(tag, ...)
    if !taskpaper#delete_tag(a:tag, '')
        let args = a:0 > 0 ? [a:tag, a:1] : [a:tag]
        call call("taskpaper#add_tag", args)
    endif
endfunction

function! taskpaper#has_tag(tag)
    let cur_line = getline(".")
    let m = matchstr(cur_line, '@'.a:tag)
    if m != ''
        return 1
    else
        return 0
endfunction

function! taskpaper#cycle_tags(...)
    let tags_index = 0
    let tag_list = a:000
    let tag_added = 0
    for tag_name in tag_list
        let tags_index = tags_index + 1
        if tags_index == len(tag_list)
            let tags_index = 0
        endif
        let has_tag = taskpaper#has_tag(tag_name)
        if has_tag == 1
            let tag_added = 1
            call taskpaper#delete_tag(tag_name)
            let new_tag = tag_list[tags_index]
            if new_tag != ''
                call taskpaper#add_tag(new_tag, '')
            endif
            break
        endif
    endfor
    if tag_added == 0
        call taskpaper#add_tag(tag_list[0], '')
    endif
endfunction

function! taskpaper#update_tag(tag, ...)
    call taskpaper#delete_tag(a:tag, '')
    let args = a:0 > 0 ? [a:tag, a:1] : [a:tag]
    call call("taskpaper#add_tag", args)
endfunction

function! taskpaper#date()
    return strftime(g:task_paper_date_format, localtime())
endfunction

function! taskpaper#complete_project(lead, cmdline, pos)
    let lnum = 1
    let list = []
    let stack = ['']
    let depth = 0

    while lnum <= line('$')
        let line = getline(lnum)
        let ml = matchlist(line, '\v\C^\t*(.+):(\s+\@[^ \t(]+(\([^)]*\))?)*$')

        if !empty(ml)
            let d = len(matchstr(line, '^\t*'))

            while d < depth
                call remove(stack, -1)
                let depth -= 1
            endwhile

            while d > depth
                call add(stack, '')
                let depth += 1
            endwhile

            let stack[d] = ml[1]

            let candidate = join(stack, ':')
            if candidate =~ '^' . a:lead
                call add(list, join(stack, ':'))
            endif
        endif

        let lnum += 1
    endwhile

    return list
endfunction

function! taskpaper#go_to_project()
    let res = input('Project: ', '', 'customlist,taskpaper#complete_project')

    if res != ''
        call taskpaper#search_project(split(res, ':'))
    endif
endfunction

function! taskpaper#next_project()
    return search('^\t*\zs.\+:\(\s\+@[^\s(]\+\(([^)]*)\)\?\)*$', 'w')
endfunction

function! taskpaper#previous_project()
    return search('^\t*\zs.\+:\(\s\+@[^\s(]\+\(([^)]*)\)\?\)*$', 'bw')
endfunction

function! s:search_project(project, depth, begin, end)
    call cursor(a:begin, 1)
    return search('\v^\t{' . a:depth . '}\V' . a:project . ':', 'c', a:end)
endfunction

function! taskpaper#search_project(projects)
    if empty(a:projects)
        return 0
    endif

    let save_pos = getpos('.')

    let begin = 1
    let end = line('$')
    let depth = 0

    for project in a:projects
        if !s:search_project(project, depth, begin, end)
            call setpos('.', save_pos)
            return 0
        endif

        let begin = line('.')
        let end = taskpaper#search_end_of_item(begin)
        let depth += 1
    endfor

    call cursor(begin, 1)
    normal! ^

    return begin
endfunction

function! taskpaper#search_end_of_item(...)
    let lnum = a:0 > 0 ? a:1 : line('.')
    let flags = a:0 > 1 ? a:2 : ''

    let depth = len(matchstr(getline(lnum), '^\t*'))

    let end = lnum
    let lnum += 1
    while lnum <= line('$')
        let line = getline(lnum)

        if line =~ '^\s*$'
            " Do nothing
        elseif depth < len(matchstr(line, '^\t*'))
            let end = lnum
        else
            break
        endif

        let lnum += 1
    endwhile

    if flags !~# 'n'
        call cursor(end, 0)
        normal! ^
    endif

    return end
endfunction

function! taskpaper#delete(...)
    let start = a:0 > 0 ? a:1 : line('.')
    let reg = a:0 > 1 ? a:2 : '"'
    let kill_indent = a:0 > 2 ? a:3 : 0

    let reg_save = ''
    if kill_indent && reg =~# '\u'
        let reg = tolower(reg)
        let reg_save = getreg(reg)
    endif

    let save_fen = &l:foldenable
    setlocal nofoldenable

    let depth = len(matchstr(getline(start), '^\t*'))

    let end = taskpaper#search_end_of_item(start)
    silent execute start . ',' . end . 'delete ' . reg

    let &l:foldenable = save_fen

    if kill_indent
        let pat = '\(^\|\n\)\t\{' . depth . '\}'
        let content = substitute(getreg(reg), pat, '\1', 'g')
        if reg_save != ''
            let content = reg_save . content
        endif
        call setreg(reg, content)
    endif

    return end - start + 1
endfunction

function! taskpaper#put(...)
    let projects = a:0 > 0 ? a:1 : []
    let reg = a:0 > 1 ? a:2 : '"'
    let indent = a:0 > 2 ? a:3 : 0

    let save_fen = &l:foldenable
    setlocal nofoldenable

    if !empty(projects) && !taskpaper#search_project(projects)
        let &l:foldenable = save_fen
        return 0
    endif

    if indent > 0
        let project_depth = len(matchstr(getline('.'), '^\t*'))
        let tabs = repeat("\t", project_depth + indent)
    else
        let tabs = ''
    endif

    execute 'put' reg
    silent execute "'[,']" . 's/^\ze./' . tabs

    let &l:foldenable = save_fen

    return line("']") - line("'[") + 1
endfunction

function! taskpaper#move(projects, ...)
    let lnum = a:0 > 0 ? a:1 : line('.')

    let save_fen = &l:foldenable
    setlocal nofoldenable

    if !taskpaper#search_project(a:projects)
        let &l:foldenable = save_fen
        return 0
    endif

    let reg = 'a'
    let save_reg = [getreg(reg), getregtype(reg)]

    let nlines = taskpaper#delete(lnum, reg, 1)
    call taskpaper#put(a:projects, reg, 1)

    let &l:foldenable = save_fen
    call setreg(reg, save_reg[0], save_reg[1])
        if g:task_paper_follow_move == 0
            execute lnum
        endif
    return nlines
endfunction

function! taskpaper#move_to_project()
    let res = input('Project: ', '', 'customlist,taskpaper#complete_project')
    call taskpaper#move(split(res, ':'))
endfunction

function! taskpaper#update_project()
    let indent = matchstr(getline("."), '^\t*')
    let depth = len(indent)

    let projects = []

    for linenr in range(line('.'), 1, -1)
        let line = getline(linenr)
        let ml = matchlist(line, '\v^\t{0,' . depth . '}([^\t:]+):')
        if empty(ml)
            continue
        endif

        let project = ml[1]
        if project != ""
            call add(projects, project)

            let indent = matchstr(line, '^\t*')
            let depth = len(indent) - 1

            if depth < 0
                break
            endif
        endif
    endfor

    call taskpaper#update_tag('project', join(reverse(projects), ' / '))
endfunction

function! taskpaper#archive_done()
    let archive_start = search('^' . g:task_paper_archive_project . ':', 'cw')
    if archive_start == 0
        call append('$', g:task_paper_archive_project . ':')
        let archive_start = line('$')
        let archive_end = 0
    else
        let archive_end = search('^\S\+:', 'W')
    endif

    let save_fen = &l:foldenable
    let save_reg = [getreg('a'), getregtype('a')]
    setlocal nofoldenable
    call setreg('a', '')

    call cursor(1, 1)
    let deleted = 0

    while 1
        let lnum = search('@done', 'W', archive_start - deleted)
        if lnum == 0
            break
        endif

        call taskpaper#update_project()
        let deleted += taskpaper#delete(lnum, 'A', 1)
    endwhile

    if archive_end != 0
        call cursor(archive_end, 1)

        while 1
            let lnum = search('@done', 'W')
            if lnum == 0
                break
            endif

            call taskpaper#update_project()
            let deleted += taskpaper#delete(lnum, 'A', 1)
        endwhile
    endif

    if deleted != 0
        call taskpaper#put([g:task_paper_archive_project], 'a', 1)
    else
        echo 'No done items.'
    endif

    let &l:foldenable = save_fen
    call setreg('a', save_reg[0], save_reg[1])

    return deleted
endfunction

function! taskpaper#fold(lnum, pat, ipat)
    let line = getline(a:lnum)
    let level = foldlevel(a:lnum)

    if line =~? a:pat && (a:ipat == '' || line !~? a:ipat)
        return 0
    elseif synIDattr(synID(a:lnum, 1, 1), "name") != 'taskpaperProject'
        return 1
    elseif level != -1
        return level
    endif

    let depth = len(matchstr(getline(a:lnum), '^\t*'))

    for lnum in range(a:lnum + 1, line('$'))
        let line = getline(lnum)

        if depth >= len(matchstr(line, '^\t*'))
            break
        endif

        if line =~? a:pat && (a:ipat == '' || line !~? a:ipat)
            return 0
        endif
    endfor
    return 1
endfunction

function! taskpaper#search(...)
    let pat = a:0 > 0 ? a:1 : input('Search: ')
    let ipat = a:0 > 1 ? a:2 : ''
    if pat == ''
        return
    endif

    setlocal foldexpr=taskpaper#fold(v:lnum,pat,ipat)
    setlocal foldminlines=0 foldtext=''
    setlocal foldmethod=expr foldlevel=0 foldenable
endfunction

function! taskpaper#fold_except_range(lnum, begin, end)
    if a:lnum > a:end
        return 1
    elseif a:lnum >= a:begin
        return 0
    elseif synIDattr(synID(a:lnum, 1, 1), "name") != 'taskpaperProject'
        return 1
    elseif level != -1
        return level
    endif

    if a:end <= taskpaper#search_end_of_item(a:lnum, 'n')
        return 0
    endif

    return 1
endfunction

function! taskpaper#focus_project()
    let pos = getpos('.')

    normal! $
    let begin = taskpaper#previous_project()
    if begin == 0
        call setpos('.', pos)
        return
    endif

    let end = taskpaper#search_end_of_item(begin, 'n')

    " Go to the top level project
    while taskpaper#previous_project()
        if getline('.') =~ '^[^\t]'
            break
        endif
    endwhile

    setlocal foldexpr=taskpaper#fold_except_range(v:lnum,begin,end)
    setlocal foldminlines=0 foldtext=''
    setlocal foldmethod=expr foldlevel=0 foldenable
endfunction

function! taskpaper#search_tag(...)
    if a:0 > 0
        let tag = a:1
    else
        let cword = expand('<cword>')
        let tag = input('Tag: ', cword =~ '@\k\+' ? cword[1:] : '')
    endif

    if tag != ''
        let ipat = (g:task_paper_search_hide_done == 1)?'\<@done\>':''
        call taskpaper#search('\<@' . tag . '\>', ipat)
    endif
endfunction

function! taskpaper#_fold_projects(lnum)
    if synIDattr(synID(a:lnum, 1, 1), "name") != 'taskpaperProject'
        return '='
    endif

    let line = getline(a:lnum)
    let depth = len(matchstr(line, '^\t*'))
    return '>' . (depth + 1)
endfunction

function! taskpaper#fold_projects()
    setlocal foldexpr=taskpaper#_fold_projects(v:lnum)
    setlocal foldminlines=0 foldtext=foldtext()
    setlocal foldmethod=expr foldlevel=0 foldenable
endfunction

function! taskpaper#newline()
    let lnum = line('.')
    let line = getline('.')

    if lnum == 1 || line !~ '^\s*$' ||
    \  synIDattr(synID(lnum - 1, 1, 1), "name") != 'taskpaperProject'
        return ''
    endif

    let pline = getline(lnum - 1)
    let depth = len(matchstr(pline, '^\t*'))
    call setline(lnum, repeat("\t", depth + 1) . '- ')

    return "\<End>"
endfunction


function! taskpaper#tag_style(...)
    if a:0 > 0
        let tag_name = a:1
    endif

    if a:0 > 1
        let tag_style = a:2
        let tag_style_name = 'taskpaperAutoStyle_' . tag_name
        execute 'syn match' tag_style_name  '/\s\zs@'.tag_name.'\(([^)]*)\)\?/'
        execute 'hi' tag_style_name tag_style
        if version < 508
            execute 'hi link'  tag_style_name tag_style_name
        else
            execute 'hi def link' tag_style_name tag_style_name
        endif
    else
        echo "No style specified."
        return ''
    endif
endfunction

function! taskpaper#tag_style_dict(tsd)
    for tag_name in keys(a:tsd)
        call taskpaper#tag_style(tag_name,a:tsd[tag_name])
    endfor
endfunction

let &cpo = s:save_cpo