" cache.vim
" @Author:      Tom Link (micathom AT gmail com?subject=[vim])
" @Website:     http://www.vim.org/account/profile.php?user_id=4037
" @License:     GPL (see http://www.gnu.org/licenses/gpl.txt)
" @Created:     2007-06-30.
" @Last Change: 2015-11-26.
" @Revision:    35.1.243


" The cache directory. If empty, use |tlib#dir#MyRuntime|.'/cache'.
" You might want to delete old files from this directory from time to 
" time with a command like: >
"   find ~/vimfiles/cache/ -atime +31 -type f -print -delete
TLet g:tlib_cache = ''

" |tlib#cache#Purge()|: Remove cache files older than N days.
TLet g:tlib#cache#purge_days = 31

" Purge the cache every N days. Disable automatic purging by setting 
" this value to a negative value.
TLet g:tlib#cache#purge_every_days = 31

" The encoding used for the purge-cache script.
" Default: 'enc'
TLet g:tlib#cache#script_encoding = &enc

" Whether to run the directory removal script:
"    0 ... No
"    1 ... Query user
"    2 ... Yes
TLet g:tlib#cache#run_script = 1

" Verbosity level:
"     0 ... Be quiet
"     1 ... Display informative message
"     2 ... Display detailed messages
TLet g:tlib#cache#verbosity = 1

" A list of regexps that are matched against partial filenames of the 
" cached files. If a regexp matches, the file won't be removed by 
" |tlib#cache#Purge()|.
TLet g:tlib#cache#dont_purge = ['[\/]\.last_purge$']

" If the cache filename is longer than N characters, use 
" |pathshorten()|.
TLet g:tlib#cache#max_filename = 200

let s:cache = {}


" :display: tlib#cache#Dir(?mode = 'bg')
" The default cache directory.
function! tlib#cache#Dir(...) "{{{3
    TVarArg ['mode', 'bg']
    let dir = tlib#var#Get('tlib_cache', mode)
    if empty(dir)
        let dir = tlib#file#Join([tlib#dir#MyRuntime(), 'cache'])
    endif
    return dir
endf


" :def: function! tlib#cache#Filename(type, ?file=%, ?mkdir=0, ?dir='')
function! tlib#cache#Filename(type, ...) "{{{3
    " TLogDBG 'bufname='. bufname('.')
    let dir0 = a:0 >= 3 && !empty(a:3) ? a:3 : tlib#cache#Dir()
    let dir = dir0
    if a:0 >= 1 && !empty(a:1)
        let file  = a:1
    else
        if empty(expand('%:t'))
            return ''
        endif
        let file  = expand('%:p')
        let file  = tlib#file#Relative(file, tlib#file#Join([dir, '..']))
    endif
    " TLogVAR file, dir
    let mkdir = a:0 >= 2 ? a:2 : 0
    let file  = substitute(file, '\.\.\|[:&<>]\|//\+\|\\\\\+', '_', 'g')
    let dirs  = [dir, a:type]
    let dirf  = fnamemodify(file, ':h')
    if dirf != '.'
        call add(dirs, dirf)
    endif
    let dir   = tlib#file#Join(dirs)
    " TLogVAR dir
    let dir   = tlib#dir#PlainName(dir)
    " TLogVAR dir
    let file  = fnamemodify(file, ':t')
    " TLogVAR file, dir, mkdir
    let cache_file = tlib#file#Join([dir, file])
    if len(cache_file) > g:tlib#cache#max_filename
        if v:version >= 704
            let shortfilename = pathshorten(file) .'_'. sha256(file)
        else
            let shortfilename = pathshorten(file) .'_'. tlib#hash#Adler32(file)
        endif
        let cache_file = tlib#cache#Filename(a:type, shortfilename, mkdir, dir0)
    else
        if mkdir && !isdirectory(dir)
            try
                call mkdir(dir, 'p')
            catch /^Vim\%((\a\+)\)\=:E739:/
                if filereadable(dir) && !isdirectory(dir)
                    echoerr 'TLib: Cannot create directory for cache file because a file with the same name exists (please delete it):' dir
                    " call delete(dir)
                    " call mkdir(dir, 'p')
                endif
            endtry
        endif
    endif
    " TLogVAR cache_file
    return cache_file
endf


let s:timestamps = {}


function! s:SetTimestamp(cfile, type) "{{{3
    if !has_key(s:timestamps, a:cfile)
        let s:timestamps[a:cfile] = {}
    endif
    let s:timestamps[a:cfile].atime = getftime(a:cfile)
    let s:timestamps[a:cfile][a:type] = s:timestamps[a:cfile].atime
endf


function! tlib#cache#Save(cfile, dictionary, ...) "{{{3
    TVarArg ['options', {}]
    let in_memory = get(options, 'in_memory', 0)
    if in_memory
        " TLogVAR in_memory, a:cfile, localtime()
        let s:cache[a:cfile] = {'mtime': localtime(), 'data': a:dictionary}
    elseif !empty(a:cfile)
        " TLogVAR a:dictionary
        call writefile([string(a:dictionary)], a:cfile, 'b')
        call s:SetTimestamp(a:cfile, 'write')
    endif
endf


function! tlib#cache#MTime(cfile) "{{{3
    let mtime = {'mtime': getftime(a:cfile)}
    let mtime = extend(mtime, get(s:timestamps, a:cfile, {}))
    return mtime
endf


function! tlib#cache#Get(cfile, ...) "{{{3
    TVarArg ['default', {}], ['options', {}]
    let in_memory = get(options, 'in_memory', 0)
    if in_memory
        " TLogVAR in_memory, a:cfile
        return get(get(s:cache, a:cfile, {}), 'data', default)
    else
        call tlib#cache#MaybePurge()
        if !empty(a:cfile) && filereadable(a:cfile)
            let val = readfile(a:cfile, 'b')
            call s:SetTimestamp(a:cfile, 'read')
            return eval(join(val, "\n"))
        else
            return default
        endif
    endif
endf


" :display: tlib#cache#Value(cfile, generator, ftime, ?generator_args=[], ?options={})
" Get a cached value from cfile. If it is outdated (compared to ftime) 
" or does not exist, create it calling a generator function.
function! tlib#cache#Value(cfile, generator, ftime, ...) "{{{3
    TVarArg ['args', []], ['options', {}]
    let in_memory = get(options, 'in_memory', 0)
    if in_memory
        let not_found = !has_key(s:cache, a:cfile)
        let cftime = not_found ? -1 : s:cache[a:cfile].mtime
    else
        let cftime = getftime(a:cfile)
    endif
    " TLogVAR in_memory, cftime
    if cftime == -1 || (a:ftime != 0 && cftime < a:ftime)
        " TLogVAR a:generator, args
        let val = call(a:generator, args)
        " TLogVAR val
        let cval = {'val': val}
        " TLogVAR cval
        call tlib#cache#Save(a:cfile, cval, options)
        return val
    else
        let val = tlib#cache#Get(a:cfile, {}, options)
        if !has_key(val, 'val')
            throw 'tlib#cache#Value: Internal error: '. a:cfile
        else
            return val.val
        endif
    endif
endf


" Call |tlib#cache#Purge()| if the last purge was done before 
" |g:tlib#cache#purge_every_days|.
function! tlib#cache#MaybePurge() "{{{3
    if g:tlib#cache#purge_every_days < 0
        return
    endif
    let dir = tlib#cache#Dir('g')
    let last_purge = tlib#file#Join([dir, '.last_purge'])
    let last_purge_exists = filereadable(last_purge)
    if last_purge_exists
        let threshold = localtime() - g:tlib#cache#purge_every_days * g:tlib#date#dayshift
        let should_purge = getftime(last_purge) < threshold
    else
        let should_purge = 0 " should ignore empty dirs, like the tmru one: !empty(glob(tlib#file#Join([dir, '**'])))
    endif
    if should_purge
        if last_purge_exists
            let yn = 'y'
        else
            let txt = "TLib: The cache directory '". dir ."' should be purged of old files.\nDelete files older than ". g:tlib#cache#purge_days ." days now?"
            let yn = tlib#input#Dialog(txt, ['yes', 'no'], 'no')
        endif
        if yn =~ '^y\%[es]$'
            call tlib#cache#Purge()
        else
            let g:tlib#cache#purge_every_days = -1
            if !last_purge_exists
                call s:PurgeTimestamp(dir)
            endif
            echohl WarningMsg
            echom "TLib: Please run :call tlib#cache#Purge() to clean up ". dir
            echohl NONE
        endif
    elseif !last_purge_exists
        call s:PurgeTimestamp(dir)
    endif
endf


" Delete old files.
function! tlib#cache#Purge() "{{{3
    let threshold = localtime() - g:tlib#cache#purge_days * g:tlib#date#dayshift
    let dir = tlib#cache#Dir('g')
    if g:tlib#cache#verbosity >= 1
        echohl WarningMsg
        echom "TLib: Delete files older than ". g:tlib#cache#purge_days ." days from ". dir
        echohl NONE
    endif
    let files = tlib#cache#ListFilesInCache()
    let deldir = []
    let newer = []
    let msg = []
    let more = &more
    set nomore
    try
        for file in files
            if isdirectory(file)
                if empty(filter(copy(newer), 'strpart(v:val, 0, len(file)) ==# file'))
                    call add(deldir, file)
                endif
            else
                if getftime(file) < threshold
                    if delete(file)
                        call add(msg, "TLib: Could not delete cache file: ". file)
                    elseif g:tlib#cache#verbosity >= 2
                        call add(msg, "TLib: Delete cache file: ". file)
                    endif
                else
                    call add(newer, file)
                endif
            endif
        endfor
    finally
        let &more = more
    endtry
    if !empty(msg) && g:tlib#cache#verbosity >= 1
        echo join(msg, "\n")
    endif
    if !empty(deldir)
        if &shell =~ 'sh\(\.exe\)\?$'
            let scriptfile = 'deldir.sh'
            let rmdir = 'rm -rf %s'
        else
            let scriptfile = 'deldir.bat'
            let rmdir = 'rmdir /S /Q %s'
        endif
        let enc = g:tlib#cache#script_encoding
        if has('multi_byte') && enc != &enc
            call map(deldir, 'iconv(v:val, &enc, enc)')
        endif
        let scriptfile = tlib#file#Join([dir, scriptfile])
        if filereadable(scriptfile)
            let script = readfile(scriptfile)
        else
            let script = []
        endif
        let script += map(copy(deldir), 'printf(rmdir, shellescape(v:val, 1))')
        let script = tlib#list#Uniq(script)
        call writefile(script, scriptfile)
        call inputsave()
        if g:tlib#cache#run_script == 0
            if g:tlib#cache#verbosity >= 1
                echohl WarningMsg
                if g:tlib#cache#verbosity >= 2
                    echom "TLib: Purged cache. Need to run script to delete directories"
                endif
                echom "TLib: Please review and execute: ". scriptfile
                echohl NONE
            endif
        else
            try
                let yn = g:tlib#cache#run_script == 2 ? 'y' : tlib#input#Dialog("TLib: About to delete directories by means of a shell script.\nDirectory removal script: ". scriptfile ."\nRun script to delete directories now?", ['yes', 'no', 'edit'], 'no')
                if yn =~ '^y\%[es]$'
                    exec 'silent cd '. fnameescape(dir)
                    exec '! ' &shell shellescape(scriptfile, 1)
                    exec 'silent cd -'
                    call delete(scriptfile)
                elseif yn =~ '^e\%[dit]$'
                    exec 'edit '. fnameescape(scriptfile)
                endif
            finally
                call inputrestore()
            endtry
        endif
    endif
    call s:PurgeTimestamp(dir)
endf


function! s:PurgeTimestamp(dir) "{{{3
    let last_purge = tlib#file#Join([a:dir, '.last_purge'])
    " TLogVAR last_purge
    call writefile([" "], last_purge)
endf

function! tlib#cache#ListFilesInCache(...) "{{{3
    let dir = a:0 >= 1 ? a:1 : tlib#cache#Dir('g')
    if v:version > 702 || (v:version == 702 && has('patch51'))
        let filess = glob(tlib#file#Join([dir, '**']), 1)
    else
        let filess = glob(tlib#file#Join([dir, '**']))
    endif
    let files = reverse(split(filess, '\n'))
    let pos0 = len(tlib#dir#CanonicName(dir))
    call filter(files, 's:ShouldPurge(strpart(v:val, pos0))')
    return files
endf


function! s:ShouldPurge(partial_filename) "{{{3
    " TLogVAR a:partial_filename
    for rx in g:tlib#cache#dont_purge
        if a:partial_filename =~ rx
            " TLogVAR a:partial_filename, rx
            return 0
        endif
    endfor
    return 1
endf

