"=============================================================================
" What Is This:
" File: metatodo.vim
" Author: Vincent Berthoux <twinside@gmail.com>
" Last Change: 2009 june 28
" Version: 1.1
"
" ChangeLog:
"     * 1.1.1:- Changed loading of files using globpath()
"     * 1.1  :- Taking into account "Documents and Settings" folder...
"             - Adding icons source from $VIM or $VIMRUNTIME
"             - Checking the nocompatible option (the only one required)
"     * 1.0  :- Original version
if exists("g:__CUTETODO_VIM__")
    finish
endif
let g:__CUTETODO_VIM__ = 1

"======================================================================
"           Configuration checking
"======================================================================
if &compatible
    echom 'cuteTodoList require the nocompatible option, loading aborted'
    echom "To fix it add 'set nocompatible' in your .vimrc file"
    finish
endif

if has("win32")
    let s:ext = '.ico'
else
    let s:ext = '.png'
endif

let s:path = globpath( &rtp, '/signs/priol1' . s:ext )
if s:path == ''
    echom "Cute Todo list can't find icons, plugin not loaded."
    finish
endif

"======================================================================
"           General Options
"======================================================================
if !exists("g:todo_list_buff_name")
    let g:todo_list_buff_name = 'Todo\ List'
endif

if !exists("g:todo_generate_auto")
    let g:todo_generate_auto = 1
endif

if !exists("g:todo_list_filename")
    let g:todo_list_filename = '.todo.txt'
endif

if !exists("g:todo_list_globfilename")
    let g:todo_list_globfilename = '.global.todo.txt'
endif

if !exists("g:todo_global_path")
    let g:todo_global_path = expand('~') . '/'
elseif g:todo_global_path =~ '[/\\]$'
    let g:todo_global_path = substitute( g:todo_global_path, '[/\\]\+$', '', '')
endif

"======================================================================
"           Plugin data
"======================================================================
" The todo list is stored with the following type (haskell
" notation) :
" DisplayName, Filename, Text :: String
" Priority, SortKind :: Int
" s:todo_list :: [(DisplayName, Filename, SortKind, [(Priority, Text)])]
" But as viml doesn't provide tuples, they are implemented
" as lists.
let s:todo_list = []
" When only part of data is needed, prefer accessing it
" using following indices, to prevent breaking if data structure
" change.
let s:todo_sort_index = 2   " Index of the SortKind thingy
let s:todo_list_index = 3   " Index of the [(Prio, Text)] thingy

let s:signId = 99000
let s:signCount = 0
let s:todoMinPrio = 0
let s:todoMaxPrio = 4
let s:default_priority = 2
let s:defaultSortKind = 0

exec 'sign define todopriop2 text=!! icon=' . escape( globpath( &rtp, 'signs/priop2' . s:ext ), ' \' )
exec 'sign define todopriop1 text=!1 icon=' . escape( globpath( &rtp, 'signs/priop1' . s:ext ), ' \' )
exec 'sign define todopriol1 text=_1 icon=' . escape( globpath( &rtp, 'signs/priol1' . s:ext ), ' \' )
exec 'sign define todopriol2 text=__ icon=' . escape( globpath( &rtp, 'signs/priol2' . s:ext ), ' \' )

let s:todo_inner_help_appeal = ['" Press <F1> for help'
                              \,' '
                              \]
let s:todo_inner_help_text = [
    \ '" Help : <F1> to close'
    \,'"  +   : Increase task priority.'
    \,'"  -   : Decrease task priority.'
    \,'"  ^   : Move the task up.'
    \,'"  v   : Move the task down.'
    \,'"  m   : Modify the task.'
    \,'"  s   : Sort the todos.'
    \,'"  u   : Update todo from buffers.'
    \,'"  o   : Jump to generated todo.'
    \,'"<S-D> : Delete a task.'
    \,' '
    \]

"=============================================================================
"                Buffer support functions
"=============================================================================
fun! s:TodoRedraw( editLine ) "{{{
    " Remove everything
    setl modifiable
    call s:TodoRemoveSign()

    " avoid overwriting user's buffers
    let tempz = getreg( 'z' )
    normal ggVG"zd
    call setreg( 'z', tempz )

    " re-render
    call s:TodoWriteFile( s:todo_list )
    " Goto previous line in previous buffer...
    " faking a non-moving cursor
    execute 'normal ' . a:editLine . 'gg'
    " Hmm, resize test, not that good :-/
    "execute 'resize ' . line( '$' )
    call s:SaveTodoFile()
endfunction "}}}

fun! s:ComputeIndices() "{{{
    let editLine = line( '.' )
    let j = 0

    if b:todo_inner_help == 0
        " -1 for title
        " -1 for help line
        let linenb = editLine - 1 - len( s:todo_inner_help_appeal )
    else
        let linenb = editLine - 1 - len( s:todo_inner_help_text )
    endif

    for [disp, f, sortKind, todos] in s:todo_list
        if linenb <= len( todos )
            let index = linenb - 1
            return [editLine, j, index]
        endif
        let linenb = linenb - len( todos ) - 2
        let j = j + 1
    endfor
    return [editLine, -1, -1]
endfunction "}}}

" return -1 if no windows was open
"        >= 0 if cursor is now in the window
fun! s:TodoGotoWin() "{{{
    let bufnum = bufnr( g:todo_list_buff_name )
    if bufnum >= 0
        let win_num = bufwinnr( bufnum )
        " We must get a real window number, or
        " else the buffer would have been deleted
        " already
        exe win_num . "wincmd w"
        return 0
    endif
    return -1
endfunction "}}}

" Remove all the priority mark on the side of the buffer.
fun! s:TodoRemoveSign() "{{{
    let s:signCount = s:signCount - 1
    while s:signCount >= 0
        let unplacer = 'sign unplace ' . (s:signCount + s:signId)
        exec unplacer
        let s:signCount = s:signCount - 1
    endwhile
endfunction "}}}

" Save the local and global todos.
fun! s:SaveTodoFile() "{{{
    for [disp, filename, sortKind, todoList] in s:todo_list
        " The generated todolist has no filename.
        if filename == ''
            return
        endif

        let toWrite = []

        for [prio,line] in todoList
            call add( toWrite, prio . ':' . line )
        endfor

        call writefile( toWrite , filename )
    endfor
endfunction "}}}

let s:classNames = ['todopriol2', 'todopriol1', '', 'todopriop1', 'todopriop2']
fun! s:AttribSign( buffId, prio, line) "{{{
    let prioClass = get( s:classNames, a:prio )

    if prioClass != ''
        let id = s:signId + s:signCount
        let s:signCount = s:signCount + 1
        let toPlace = 'sign place ' . id
                    \ . ' line=' . a:line
                    \ . ' name=' . prioClass
                    \ . ' buffer=' . a:buffId
        execute toPlace
    endif
endfunction "}}}

fun! s:TodoWriteFile( todol ) "{{{
    let i = 0
    " Prefixes used to highlight differently one task on two
    let prefixes = [' ', "\t"]
    let buffId = bufnr( '%' )
    setlocal modifiable

    if b:todo_inner_help == 0
        call append( i, s:todo_inner_help_appeal )
        let i = i + len( s:todo_inner_help_appeal )
    else
        call append( i, s:todo_inner_help_text )
        let i = i + len( s:todo_inner_help_text )
    endif

    for [disp, file, sortKind, todos] in a:todol
        let partBegin = i
        call append( i, '====  ' . disp )
        let i = i + 1

        for [prio,text] in todos
            call append( i, prefixes[i % 2] . text )
            let i = i + 1
            call s:AttribSign( buffId, prio, i )
        endfor

        " We create a fold manually here
        if partBegin > 0
            let partBegin = partBegin + 1
        endif

        exec partBegin . ',' . i . 'fold'
        call append( i, ' ' )
        let i = i + 1
    endfor

    " Remove last two lines, and avoid overwritin
    " user's registers
    let tempz = getreg( 'z' )
    normal G"zdd"zdd
    call setreg( 'z', tempz )
    " Open all the folds
    normal zR
    setlocal nomodifiable
endfunction "}}}

fun! s:TodoParseTodo(txt) "{{{
    if a:txt =~ '^\d:.*'
        let prio = substitute( a:txt, '^\(\d\):.*', '\1', '' )
        let text = substitute( a:txt, '^\d:\(.*\)', '\1', '' )
        return [prio, text]
    elseif a:txt =~ '^\d\+\s\+.*'
        let prio = substitute( a:txt, '^\(\d\+\).*', '\1', '' )
        let text = substitute( a:txt, '^\d\+\s\+\(.*\)', '\1', '' )
        return [prio, ' ' . text]
    else
        return [s:default_priority, a:txt]
    endif
endfunction "}}}

" Used by TodoGatherInFiles to store result in a list :]
fun! s:TodoGathering() "{{{
    let text = substitute( getline('.'), '.*TODO\s\+\(.*\)', '\1', '' )
    let [prio, txt] = s:TodoParseTodo( text )
    call add( s:spareTodos, [prio, expand('%') . '|' . line('.') . '|' .txt ] )
endfunction "}}}

fun! s:TodoGatherInFiles() "{{{
    if !g:todo_generate_auto 
        return
    endif

    let curBuf = bufnr( '%' )
    " avoid buffer deletion during bufdo
    setlocal bufhidden=hide
    " Ok we remove all we have, to avoid
    " getting a feedback loop and getting todos
    " from the todo window...
    setlocal modifiable
    let tempz = getreg( 'z' )
    normal ggVG"zd
    call setreg( 'z', tempz )

    let s:spareTodos = []
    " Big regexp to catch TODO and avoid it in words
    bufdo 1,$g/^TODO\|\ATODO\A\|\ATODO$/call s:TodoGathering()
    execute 'buffer ' . curBuf
    setlocal bufhidden=wipe
    if len(s:todo_list) < 3
        call add( s:todo_list, ['Generated', '', s:defaultSortKind, s:spareTodos])
    else
        let s:todo_list[2] = ['Generated', '', s:defaultSortKind, s:spareTodos]
    endif
endfunction "}}}

fun! LoadTodoFile( displayName, fileName ) "{{{
    if !filereadable( a:fileName )
        return [a:displayName, a:fileName, s:defaultSortKind, []]
    endif
    let retLst = []

    for line in readfile( a:fileName )
        call add( retLst, s:TodoParseTodo( line ) )
    endfor
    return [a:displayName, a:fileName, s:defaultSortKind, retLst]
endfunction "}}}

"=============================================================================
"                Buffer definition
"=============================================================================
" Set all the option for the todo buffer, avoid
" modification and put hooks to manage it.
function! s:PrepareTodoBuffer()
    set ft=NONE
    mapclear <buffer>
    setlocal buftype=nofile
    " make sure buffer is deleted when view is closed
    setlocal bufhidden=wipe
    setlocal noswapfile
    setlocal nobuflisted
    setlocal nonumber
    setlocal linebreak
    setlocal foldcolumn=1
    setlocal foldmethod=manual
    setlocal noexpandtab
    setlocal softtabstop
    setlocal tabstop=1
    
    exe 'file ' . g:todo_list_buff_name
    setlocal statusline=%F
    nnoremap <silent> <buffer> + :call <SID>TodoAddToPriority(1)<CR>
    nnoremap <silent> <buffer> - :call <SID>TodoAddToPriority(-1)<CR>
    nnoremap <silent> <buffer> ^ :call <SID>TodoSwap(-1)<CR>
    nnoremap <silent> <buffer> v :call <SID>TodoSwap(1)<CR>
    nnoremap <silent> <buffer> s :call <SID>TodoSort()<CR>
    nnoremap <silent> <buffer> m :call <SID>TodoModify()<CR>
    nnoremap <silent> <buffer> u :call <SID>UpdateTodos()<CR>
    nnoremap <silent> <buffer> o :call <SID>TodoJumpTo()<CR>
    nnoremap <silent> <buffer> <S-D> :call <SID>TodoRemoveTask()<CR>
    nnoremap <silent> <buffer> <F1> :call <SID>TodoToggleHelp()<CR>

    syntax match Title /^===.*/
    syntax match DiffChange /^\t.*/
    syntax match Comment /^".*/

    let b:todo_inner_help = 0
endfunction

fun! s:TodoToggleHelp() "{{{
    if b:todo_inner_help != 0
        let b:todo_inner_help = 0
    else
        let b:todo_inner_help = 1
    endif
    let editLine = line('.')
    call s:TodoRedraw( editLine )
endfunction "}}}

"=============================================================================
"                Sorting functions
"=============================================================================
fun! s:TodoCompareByPrio(obj1, obj2) "{{{
    let [i1, ignoredText1] = a:obj1
    let [i2, ignoredText2] = a:obj2
    return i1 == i2 ? 0 : i1 > i2 ? 1 : -1
endfunction "}}}

fun! s:TodoCompareByInvPrio(o1, o2) "{{{
    return - s:TodoCompareByPrio(a:o1, a:o2)
endfunction "}}}

fun! s:TodoCompareByText(obj1, obj2) "{{{
    let [ignoredPrio1, i1] = a:obj1
    let [ignoredPrio2, i2] = a:obj2
    return i1 == i2 ? 0 : i1 > i2 ? 1 : -1
endfunction "}}}

fun! s:TodoCompareByInvText(o1, o2) "{{{
    return - s:TodoCompareByText( a:o1, a:o2 )
endfunction "}}}

let s:sortFunctions = ["<SID>TodoCompareByPrio"
                     \,"<SID>TodoCompareByInvPrio"
                     \,"<SID>TodoCompareByText"
                     \,"<SID>TodoCompareByInvText"
                     \]
fun! s:TodoSort() "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()
    if fileNumber != -1
        let sortIdx = (s:todo_list[fileNumber][s:todo_sort_index] + 1) % len( s:sortFunctions )
        call sort( s:todo_list[fileNumber][s:todo_list_index], s:sortFunctions[sortIdx] )
        let s:todo_list[fileNumber][s:todo_sort_index] = sortIdx
        call s:TodoRedraw( editLine )
    endif
endfunction "}}}

"=============================================================================
"                Buffer actions
"=============================================================================
" Function called to trigger the todo gathering in files
fun! s:UpdateTodos() "{{{
    let editLine = line('.')
    let s:spareTodos = []
    call s:TodoLoadFiles()
    call s:TodoGatherInFiles()
    call s:PrepareTodoBuffer()
    call s:TodoRedraw( editLine )
endfunction "}}}

fun! s:TodoRemoveTask() "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()

    if fileNumber != -1
        let todos = s:todo_list[fileNumber][s:todo_list_index]
        call remove( todos, index )
        call s:TodoRedraw( editLine )
    endif
endfunction "}}}

" Swap two tasks, to simulate a move up or move down
" in function of the a:swapRel variable.
fun! s:TodoSwap(swapRel) "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()

    if fileNumber != -1
        let todos = s:todo_list[fileNumber][s:todo_list_index]
        let atIndex = get( todos, index )

        " Avoid swapping out of bounds todos
        if (a:swapRel < 0 && index == 0) || (a:swapRel > 0 && index == len(todos) - 1)
            return
        endif

        call remove( todos, index )
        call insert( todos, atIndex, index + a:swapRel )
        if a:swapRel > 0
            let editLine = editLine + 1
        elseif a:swapRel < 0
            let editLine = editLine - 1
        endif

        call s:TodoRedraw( editLine )
    endif
endfunction "}}}

" Add count to the priority
fun! s:TodoAddToPriority( count ) "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()

    if fileNumber != -1
        let todos = s:todo_list[fileNumber][s:todo_list_index]
        let [prio, text] = todos[index]
        let newprio = prio + a:count

        if newprio < s:todoMinPrio || newprio > s:todoMaxPrio
            return
        endif

        call insert( todos, [newprio, text], index )
        call remove( todos, index + 1 )

        call s:TodoRedraw( editLine )
    endif
endfunction "}}}

fun! s:TodoModify() "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()

    if fileNumber != -1
        let [dispName, filename, sortKind, todos] = s:todo_list[fileNumber]
        " Can't modify the generated
        if filename == ''
            return
        endif

        let newText = input( 'Todo> ', todos[index][1] )
        let todos[index][1] = newText

        call s:TodoRedraw( editLine )
    endif
endfunction "}}}

fun! s:TodoJumpTo() "{{{
    let [editLine, fileNumber, index] = s:ComputeIndices()

    if fileNumber != 2
        return
    endif

    let txt = s:todo_list[fileNumber][s:todo_list_index][index][1]
    let filename = substitute( txt, '^\([^|]\+\).*', '\1', '' )
    let lineNum = substitute( txt, '^[^|]\+|\(\d\+\).*', '\1', '')

    let bufnum = bufnr( filename )
    if bufnum >= 0
        let win_num = bufwinnr( bufnum )
        " If it's already shown
        if win_num > 0
            " We must get a real window number, or
            " else the buffer would have been deleted
            " already
            exe win_num . "wincmd w"
        else " Heuristic... go left and change the buffer...
            wincmd l
            execute 'buffer ' . bufnum
        endif

        exe 'normal ' . lineNum . 'gg'
    endif
endfunction "}}}

"=============================================================================
"                Ex Commands implementation
"=============================================================================
fun! TodoCloseWindow() "{{{
    let last_buffer = bufnr("%")
    if s:TodoGotoWin() >= 0
        close
    endif
    let win_num = bufwinnr( last_buffer )
    " We must get a real window number, or
    " else the buffer would have been deleted
    " already
    exe win_num . "wincmd w"
endfunction "}}}

fun! TodoWindowOpen() "{{{
    " Ok, previous buffer to jump to it at final
    let last_buffer = bufnr("%")

    if s:TodoGotoWin() < 0
        new
        call s:PrepareTodoBuffer()
        setl modifiable
    else
        call s:TodoRemoveSign()
        setl modifiable
        normal ggVGd<ESC>
    endif

    call s:TodoWriteFile( s:todo_list )

    let win_num = bufwinnr( last_buffer )
    " We must get a real window number, or
    " else the buffer would have been deleted
    " already
    exe win_num . "wincmd w"
endfunction "}}}

fun! TodoAdd(level) "{{{
    let text = input( 'Todo> ' )
    let prio = s:default_priority

    " If the text begin by digits
    if text =~ '^\d\+\s\+.*'
        " then extract the said digit and transform it to int
        let mayPrio = substitute(text,'^\(\d\+\)\s\+.*','\1', '') + 0
        " If it's in priority bounds...
        if s:todoMinPrio <= mayPrio && mayPrio <= s:todoMaxPrio
            " Take it as task priority...
            let text = substitute( text, '^\d\+\s\+\(.*\)', '\1', '' )
            let prio = mayPrio
        endif
    endif

    " Now add it to the good level... (local or global)
    for [disp, f, sortKind, todos] in s:todo_list
        if disp == a:level
            call add( todos, [prio, text] )
        endif
    endfor

    call TodoWindowOpen()
    call s:SaveTodoFile()
endfunction "}}}

fun! s:TodoLoadFiles() "{{{
    let currDir = expand( '$PWD' )
    let globDir = g:todo_global_path

    let s:todo_list = [ LoadTodoFile( 'Global', globDir . '/' . g:todo_list_globfilename )
                    \ , LoadTodoFile( 'Local', g:todo_list_filename )
                    \ ]
endfunction "}}}

command! TodoClose call TodoCloseWindow()
command! TodoOpen call TodoWindowOpen()
command! Todo call TodoAdd( 'Local' )
command! Todog call TodoAdd( 'Global' )

let s:spareTodos = []
call s:TodoLoadFiles()

