"=============================================================================
" File    : autoload/unite/source/outline/_cache.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#file_cache#import(dir) abort
  let s:FileCache.DIR = a:dir
  return s:FileCache
endfunction

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

let s:Util = unite#sources#outline#import('Util')

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

" FileCache module provides functions to access and manage the cache data
" stored in files on the local filesystem.
"
let s:FileCache = unite#sources#outline#modules#base#new('FileCache', s:SID)

if get(g:, 'unite_source_outline_debug', 0)
  let s:FileCache.CLEANUP_FILE_COUNT = 10
  let s:FileCache.CLEANUP_RATE = 1
  let s:FileCache.EXPIRES = 60
else
  let s:FileCache.CLEANUP_FILE_COUNT = 300
  let s:FileCache.CLEANUP_RATE = 10
  let s:FileCache.EXPIRES = 60 * 60 * 24 * 30
endif

" Returns True if the cached data associated with buffer {bufnr} is available.
"
function! s:FileCache_has(bufnr) dict
  let path = s:get_buffer_path(a:bufnr)
  return s:cache_file_exists(path)
endfunction
call s:FileCache.function('has')

" Returns True if the cache file associated with file {path} exists.
"
function! s:cache_file_exists(path) abort
  return (s:cache_dir_exists() && filereadable(s:get_cache_file_path(a:path)))
endfunction

" Returns True if the cache directory exists.
"
function! s:cache_dir_exists() abort
  if isdirectory(s:FileCache.DIR)
    return 1
  elseif unite#util#is_sudo()
    call unite#util#print_error("unite-outline: Couldn't create the cache directory.")
    return isdirectory(s:FileCache.DIR)
  else
    try
      call mkdir(iconv(s:FileCache.DIR, &encoding, &termencoding), 'p')
    catch
      call unite#util#print_error("unite-outline: Couldn't create the cache directory.")
    endtry
    return isdirectory(s:FileCache.DIR)
  endif
endfunction

" Returns a full pathname of the file opened at the buffer {bufnr}.
"
function! s:get_buffer_path(bufnr) abort
  return fnamemodify(bufname(a:bufnr), ':p')
endfunction

" Returns a full pathname of the cache file for the file {path}.
"
function! s:get_cache_file_path(path) abort
  return s:FileCache.DIR . '/' . s:encode_file_path(a:path)
endfunction

" Encodes a full pathname to a basename.
"
" Original source from Shougo's neocomplcache
" https://github.com/Shougo/neocomplcache
"
function! s:encode_file_path(path) abort
  if len(s:FileCache.DIR) + len(a:path) < 150
    " Encode {path} to a basename.
    return substitute(substitute(a:path, ':', '=-', 'g'), '[/\\]', '=+', 'g')
  else
    " Calculate a simple hash.
    let sum = 0
    for idx in range(len(a:path))
      let sum += char2nr(a:path[idx]) * (idx + 1)
    endfor
    return printf('%X', sum)
  endif
endfunction

" Returns the cached data associated with buffer {bufnr}.
"
function! s:FileCache_get(bufnr) dict
  let path = s:get_buffer_path(a:bufnr)
  let data = s:load_cache_file(path)
  return data
endfunction
call s:FileCache.function('get')

function! s:load_cache_file(path) abort
  let cache_file = s:get_cache_file_path(a:path)
  let lines = readfile(cache_file)
  if !empty(lines)
    let dumped_data = lines[0]
    call s:print_debug("[LOADED] cache file: " . cache_file)
  else
    throw "unite-outline: Couldn't load the cache file: " . cache_file
  endif
  " Touch; Update the timestamp.
  if !unite#util#is_sudo() && writefile([dumped_data], cache_file) == 0
    call s:print_debug("[TOUCHED] cache file: " . cache_file)
  endif
  sandbox let data = eval(dumped_data)
  return data
endfunction

" Saves {data} to the cache file.
"
function! s:FileCache_set(bufnr, data) dict
  let path = s:get_buffer_path(a:bufnr)
  try
    if s:cache_dir_exists()
      call s:save_cache_file(path, a:data)
    elseif s:cache_file_exists(path)
      call s:remove_file(s:get_cache_file_path(path))
    endif
  catch /^unite-outline:/
    call unite#util#print_error(v:exception)
  endtry
  call s:cleanup_cache_files()
endfunction
call s:FileCache.function('set')

function! s:save_cache_file(path, data) abort
  let cache_file = s:get_cache_file_path(a:path)
  let dumped_data = string(a:data)
  if !unite#util#is_sudo() && writefile([dumped_data], cache_file) == 0
    call s:print_debug("[SAVED] cache file: " . cache_file)
  else
    throw "unite-outline: Couldn't save the cache to: " . cache_file
  endif
endfunction

" Remove the cached data associated with buffer {bufnr}.
"
function! s:FileCache_remove(bufnr) dict
  let path = s:get_buffer_path(a:bufnr)
  if s:cache_file_exists(path)
    try
      call s:remove_file(s:get_cache_file_path(path))
    catch /^unite-outline:/
      call unite#util#print_error(v:exception)
    endtry
  endif
endfunction
call s:FileCache.function('remove')

function! s:remove_file(path) abort
    if delete(a:path) == 0
      call s:print_debug("[DELETED] cache file: " . a:path)
    else
      throw "unite-outline: Couldn't delete the cache file: " . a:path
    endif
endfunction

" Remove all cache files.
"
function! s:FileCache_clear() abort
  if s:cache_dir_exists()
    call s:cleanup_all_cache_files()
    echomsg "unite-outline: Deleted all cache files."
  else
    call unite#util#print_error("unite-outline: Cache directory doesn't exist.")
  endif
endfunction
call s:FileCache.function('clear')

function! s:cleanup_all_cache_files() abort
  call s:cleanup_cache_files(1)
endfunction

" Remove old cache files.
"
function! s:cleanup_cache_files(...) abort
  let delete_all = (a:0 ? a:1 : 0)
  let cache_files = split(globpath(s:FileCache.DIR, '*'), "\<NL>")
  let dlt_files = []

  if delete_all
    let dlt_files = cache_files
  elseif len(cache_files) > s:FileCache.CLEANUP_FILE_COUNT
    let now = localtime()
    if now % s:FileCache.CLEANUP_RATE == 0
      for path in cache_files
        if now - getftime(path) > s:FileCache.EXPIRES
          call add(dlt_files, path)
        endif
      endfor
    endif
  endif
  for path in dlt_files
    try
      call s:remove_file(path)
    catch /^unite-outline:/
      call unite#util#print_error(v:exception)
    endtry
  endfor
endfunction

function! s:print_debug(msg) abort
  call s:Util.print_debug('cache', a:msg)
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo