"
" Template system for Vim
"
" Copyright (C) 2012 Adrian Perez de Castro <aperez@igalia.com>
" Copyright (C) 2005 Adrian Perez de Castro <the.lightman@gmail.com>
"
" Distributed under terms of the MIT license.
"

if exists("g:templates_plugin_loaded")
	finish
endif
let g:templates_plugin_loaded = 1

if !exists('g:templates_name_prefix')
	let g:templates_name_prefix = ".vim-template:"
endif

if !exists('g:templates_global_name_prefix')
	let g:templates_global_name_prefix = "=template="
endif

if !exists('g:templates_debug')
	let g:templates_debug = 0
endif

if !exists('g:templates_tr_in')
	let g:templates_tr_in = [ '.', '*', '?' ]
endif

if !exists('g:templates_tr_out')
	let g:templates_tr_out = [ '\.', '.*', '\?' ]
endif

if !exists('g:templates_fuzzy_start')
	let g:templates_fuzzy_start = 1
endif

if !exists('g:templates_search_height')
	" First try to find the deprecated option
	if exists('g:template_max_depth')
		echom('g:template_max_depth is deprecated in favor of g:templates_search_height')
		let g:templates_search_height = g:template_max_depth != 0 ? g:template_max_depth : -1
	endif

	if(!exists('g:templates_search_height'))
		let g:templates_search_height = -1
	endif
endif

if !exists('g:templates_directory')
	let g:templates_directory = []
elseif type(g:templates_directory) == type('')
	" Convert string value to a list with one element.
	let s:tmp = g:templates_directory
	unlet g:templates_directory
	let g:templates_directory = [ s:tmp ]
	unlet s:tmp
endif

if !exists('g:templates_no_builtin_templates')
	let g:templates_no_builtin_templates = 0
endif

if !exists('g:templates_user_variables')
	let g:templates_user_variables = []
endif

" Put template system autocommands in their own group. {{{1
if !exists('g:templates_no_autocmd')
	let g:templates_no_autocmd = 0
endif

if !g:templates_no_autocmd
	augroup Templating
		autocmd!
		autocmd BufNewFile * call <SID>TLoad()
	augroup END
endif

function <SID>Debug(mesg)
	if g:templates_debug
		echom(a:mesg)
	endif
endfunction

" normalize the path
" replace the windows path sep \ with /
function <SID>NormalizePath(path)
	return substitute(a:path, "\\", "/", "g")
endfunction

" Template searching. {{{1
" Returns a string containing the path of the parent directory of the given
" path. Works like dirname(3). It also simplifies the given path.
function <SID>DirName(path)
	let l:tmp = <SID>NormalizePath(a:path)
	return substitute(l:tmp, "[^/][^/]*/*$", "", "")
endfunction

" Default templates directory
let s:default_template_dir = <SID>DirName(<SID>DirName(expand("<sfile>"))) . "templates"

" Find the target template in windows
"
" In windows while we clone the symbol link from github
" it will turn to normal file, so we use this function
" to figure out the destination file
function <SID>TFindLink(path, template)
	if !filereadable(a:path . a:template)
		return a:template
	endif

	let l:content = readfile(a:path . a:template, "b")
	if len(l:content) != 1
		return a:template
	endif

	if filereadable(a:path . l:content[0])
		return <SID>TFindLink(a:path, l:content[0])
	else
		return a:template
	endif
endfunction

" Translate a template file name into a regular expression to test for matching
" against a given filename. As of writing this behavior is something like this:
" (with a g:templates_name_prefix set as 'template.')
"
" template.py -> ^.*py$
"
" template.test.py -> ^.*test.py$
"
function <SID>TemplateToRegex(template, prefix)
	let l:template_base_name = fnamemodify(a:template,":t")
	let l:template_glob = strpart(l:template_base_name, len(a:prefix))

	" Translate the template's glob into a normal regular expression
	let l:in_escape_mode = 0
	let l:template_regex = ""
	for l:c in split(l:template_glob, '\zs')
		if l:in_escape_mode == 1
			if l:c == '\'
				let l:template_regex = l:template_regex . '\\'
			else
				let l:template_regex = l:template_regex . l:c
			endif

			let l:in_escape_mode = 0
		else
			if l:c == '\'
				let l:in_escape_mode = 1
			else
				let l:tr_index = index(g:templates_tr_in, l:c)
				if l:tr_index != -1
					let l:template_regex = l:template_regex . g:templates_tr_out[l:tr_index]
				else
					let l:template_regex = l:template_regex . l:c
				endif
			endif
		endif
	endfor

	if g:templates_fuzzy_start
		return l:template_regex . '$'
	else
		return '^' . l:template_regex . '$'
	endif

endfunction

" Given a template and filename, return a score on how well the template matches
" the given filename.  If the template does not match the file name at all,
" return 0
function <SID>TemplateBaseNameTest(template, prefix, filename)
	let l:tregex = <SID>TemplateToRegex(a:template, a:prefix)

	" Ensure that we got a valid regex
	if l:tregex == ""
		return 0
	endif

	" For now only use the base of the filename.. this may change later
	" *Note* we also have to be careful because a:filename may also be the passed
	" in text from TLoadCmd...
	let l:filename_chopped = fnamemodify(a:filename,":t")

	" Check for a match
	let l:regex_result = match(l:filename_chopped,l:tregex)
	if l:regex_result != -1
		" For a match return a score based on the regex length
		return len(l:tregex)
	else
		" No match
		return 0
	endif

endfunction

" Returns the most specific / highest scored template file found in the given
" path.  Template files are found by using a glob operation on the current path
" and the setting of g:templates_name_prefix. If no template is found in the
" given directory, return an empty string
function <SID>TDirectorySearch(path, template_prefix, file_name)
	let l:picked_template = ""
	let l:picked_template_score = 0

	" Use find if possible as it will also get hidden files on nix systems. Use
	" builtin glob as a fallback
	if executable("find") && !has("win32") && !has("win64")
		let l:find_cmd = '`find -L ' . shellescape(a:path) . ' -maxdepth 1 -type f -name ' . shellescape(a:template_prefix . '*' ) . '`'
		call <SID>Debug("Executing " . l:find_cmd)
		let l:glob_results = glob(l:find_cmd)
		if v:shell_error != 0
			call <SID>Debug("Could not execute find command")
			unlet l:glob_results
		endif
	endif
	if !exists("l:glob_results")
		call <SID>Debug("Using fallback glob")
		let l:glob_results = glob(a:path . a:template_prefix . "*")
	endif
	let l:templates = split(l:glob_results, "\n")
	for template in l:templates
		" Make sure the template is readable
		if filereadable(template)
			let l:current_score = 
						\<SID>TemplateBaseNameTest(template, a:template_prefix, a:file_name)
			call <SID>Debug("template: " . template . " got scored: " . l:current_score)

			" Pick that template only if it beats the currently picked template
			" (here we make the assumption that template name length ~= template
			" specifity / score)
			if l:current_score > l:picked_template_score
				let l:picked_template = template
				let l:picked_template_score = l:current_score
			endif
		endif
	endfor

	if l:picked_template != ""
		call <SID>Debug("Picked template: " . l:picked_template)
	else
		call <SID>Debug("No template found")
	endif

	return l:picked_template
endfunction

" Searches for a [template] in a given [path].
"
" If [height] is [-1] the template is searched for in the given directory and
" all parents in its directory structure
"
" If [height] is [0] no searching is done in the given directory or any
" parents
"
" If [height] is [1] only the given directory is searched
"
" If [height] is greater than one, n parents and the given directory will be
" searched where n is equal to height - 1
"
" If no template is found an empty string is returned.
"
function <SID>TSearch(path, template_prefix, file_name, height)
	if (a:height != 0)

		" pick a template from the current path
		let l:picked_template = <SID>TDirectorySearch(a:path, a:template_prefix, a:file_name)
		if l:picked_template != ""
			return l:picked_template
		else
			let l:pathUp = <SID>DirName(a:path)
			if l:pathUp != a:path
				let l:new_height = a:height >= 0 ? a:height - 1 : a:height
				return <SID>TSearch(l:pathUp, a:template_prefix, a:file_name, l:new_height)
			endif
		endif
	endif

	" Ooops, either we cannot go up in the path or [height] reached 0
	return ""
endfunction


" Tries to find valid templates using the global g:templates_name_prefix as a glob
" matcher for template files. The search is done as follows:
"   1. The [path] passed to the function, [upwards] times up.
"   2. The g:templates_directory directory, if it exists.
"   3. Built-in templates from s:default_template_dir.
" Returns an empty string if no template is found.
"
function <SID>TFind(path, name, up)
	let l:tmpl = <SID>TSearch(a:path, g:templates_name_prefix, a:name, a:up)
	if l:tmpl != ''
		return l:tmpl
	endif

	for l:directory in g:templates_directory
		let l:directory = <SID>NormalizePath(expand(l:directory) . '/')
		if isdirectory(l:directory)
			let l:tmpl = <SID>TSearch(l:directory, g:templates_global_name_prefix, a:name, 1)
			if l:tmpl != ''
				return l:tmpl
			endif
		endif
	endfor

	if g:templates_no_builtin_templates
		return ''
	endif

	return <SID>TSearch(<SID>NormalizePath(expand(s:default_template_dir) . '/'), g:templates_global_name_prefix, a:name, 1)
endfunction

" Escapes a string for use in a regex expression where the regex uses / as the
" delimiter. Must be used with Magic Mode off /V
"
function <SID>EscapeRegex(raw)
	return escape(a:raw, '/')
endfunction

" Template variable expansion. {{{1

" Makes a single [variable] expansion, using [value] as replacement.
"
function <SID>TExpand(variable, value)
	silent! execute "%s/\\V%" . <SID>EscapeRegex(a:variable) . "%/" .  <SID>EscapeRegex(a:value) . "/g"
endfunction

" Performs variable expansion in a template once it was loaded {{{2
"
function <SID>TExpandVars()
	" Date/time values
	let l:day        = strftime("%d")
	let l:year       = strftime("%Y")
	let l:month      = strftime("%m")
	let l:monshort   = strftime("%b")
	let l:monfull    = strftime("%B")
	let l:time       = strftime("%H:%M")
	let l:date       = exists("g:dateformat") ? strftime(g:dateformat) :
				     \ (l:year . "-" . l:month . "-" . l:day)
	let l:fdate      = l:date . " " . l:time
	let l:filen      = expand("%:t:r:r:r")
	let l:filex      = expand("%:e")
	let l:filec      = expand("%:t")
	let l:fdir       = expand("%:p:h:t")
	let l:hostn      = hostname()
	let l:user       = exists("g:username") ? g:username :
				     \ (exists("g:user") ? g:user : $USER)
	let l:email      = exists("g:email") ? g:email : (l:user . "@" . l:hostn)
	let l:guard      = toupper(substitute(l:filec, "[^a-zA-Z0-9]", "_", "g"))
	let l:class      = substitute(l:filen, "\\([a-zA-Z]\\+\\)", "\\u\\1\\e", "g")
	let l:macroclass = toupper(l:class)
	let l:camelclass = substitute(l:class, "_", "", "g")

	" Finally, perform expansions
	call <SID>TExpand("DAY",   l:day)
	call <SID>TExpand("YEAR",  l:year)
	call <SID>TExpand("DATE",  l:date)
	call <SID>TExpand("TIME",  l:time)
	call <SID>TExpand("USER",  l:user)
	call <SID>TExpand("FDATE", l:fdate)
	call <SID>TExpand("MONTH", l:month)
	call <SID>TExpand("MONTHSHORT", l:monshort)
	call <SID>TExpand("MONTHFULL",  l:monfull)
	call <SID>TExpand("FILE",  l:filen)
	call <SID>TExpand("FFILE", l:filec)
	call <SID>TExpand("FDIR",  l:fdir)
	call <SID>TExpand("EXT",   l:filex)
	call <SID>TExpand("MAIL",  l:email)
	call <SID>TExpand("HOST",  l:hostn)
	call <SID>TExpand("GUARD", l:guard)
	call <SID>TExpand("CLASS", l:class)
	call <SID>TExpand("MACROCLASS", l:macroclass)
	call <SID>TExpand("CAMELCLASS", l:camelclass)
	call <SID>TExpand("LICENSE", exists("g:license") ? g:license : "MIT")

	" Perform expansions for user-defined variables
	for [l:varname, l:funcname] in g:templates_user_variables
		let l:value = function(funcname)()
		call <SID>TExpand(l:varname, l:value)
	endfor
endfunction

" }}}2

" Puts the cursor either at the first line of the file or in the place of
" the template where the %HERE% string is found, removing %HERE% from the
" template.
"
function <SID>TPutCursor()
	0  " Go to first line before searching
	if search("%HERE%", "W")
		let l:column = col(".")
		let l:lineno = line(".")
		s/%HERE%//
		call cursor(l:lineno, l:column)
	endif
endfunction

" File name utils
"
" Ensures that the given file name is safe to be opened and will not be shell
" expanded
function <SID>NeuterFileName(filename)
	let l:neutered = fnameescape(a:filename)
	call <SID>Debug("Neutered " . a:filename . " to " . l:neutered)
	return l:neutered
endfunction


" Template application. {{{1

" Loads a template for the current buffer, substitutes variables and puts
" cursor at %HERE%. Used to implement the BufNewFile autocommand.
"
function <SID>TLoad()
	if !line2byte( line( '$' ) + 1 ) == -1
		return
	endif

	let l:file_name = expand("%:p")
	let l:file_dir = <SID>DirName(l:file_name)
	let l:depth = g:templates_search_height
	let l:tFile = <SID>TFind(l:file_dir, l:file_name, l:depth)
	call <SID>TLoadTemplate(l:tFile, 0)
endfunction


" Like the previous one, TLoad(), but intended to be called with an argument
" that either is a filename (so the file is loaded as a template) or
" a template suffix (and the template is searched as usual). Of course this
" makes variable expansion and cursor positioning.
"
function <SID>TLoadCmd(template, position)
	if filereadable(a:template)
		let l:tFile = a:template
	else
		let l:height = g:templates_search_height
		let l:tName = g:templates_global_name_prefix . a:template
		let l:file_name = expand("%:p")
		let l:file_dir = <SID>DirName(l:file_name)

		let l:tFile = <SID>TFind(l:file_dir, a:template, l:height)
	endif
	call <SID>TLoadTemplate(l:tFile, a:position)
endfunction

" Load the given file as a template
function <SID>TLoadTemplate(template, position)
	if a:template != ""
		let l:deleteLastLine = 0
		if line('$') == 1 && getline(1) == ''
			let l:deleteLastLine = 1
		endif

		" Read template file and expand variables in it.
		let l:safeFileName = <SID>NeuterFileName(a:template)
		if a:position == 0 || l:deleteLastLine == 1
			execute "keepalt 0r " . l:safeFileName
		else
			execute "keepalt r " . l:safeFileName
		endif
		call <SID>TExpandVars()

		if l:deleteLastLine == 1
			" Loading a template into an empty buffer leaves an extra blank line at the bottom, delete it
			execute line('$') . "d _"
		endif

		call <SID>TPutCursor()
		setlocal nomodified
	endif
endfunction

" Commands {{{1

" Just calls the above function, pass either a filename or a template
" suffix, as explained before =)
"
fun ListTemplateSuffixes(A,P,L)
  let l:templates = split(globpath(s:default_template_dir, g:templates_global_name_prefix . a:A . "*"), "\n")
  let l:res = []
  for t in templates
    let l:suffix = substitute(t, ".*\\.", "", "")
    call add(l:res, l:suffix)
  endfor

  return l:res
endfun
command -nargs=1 -complete=customlist,ListTemplateSuffixes Template call <SID>TLoadCmd("<args>", 0)
command -nargs=1 -complete=customlist,ListTemplateSuffixes TemplateHere call <SID>TLoadCmd("<args>", 1)

" Syntax autocommands {{{1
"
" Enable the vim-template syntax for template files
" Usually we'd put this in the ftdetect folder, but because
" g:templates_name_prefix doesn't get defined early enough we have to add the
" template detection from the plugin itself
execute "au BufNewFile,BufRead " . g:templates_name_prefix . "* "
			\. "let b:vim_template_subtype = &filetype | "
			\. "set ft=vim-template"

if !g:templates_no_builtin_templates
	execute "au BufNewFile,BufRead "
				\. s:default_template_dir . "/" . g:templates_global_name_prefix . "* "
				\. "let b:vim_template_subtype = &filetype | "
				\. "set ft=vim-template"
endif

for s:directory in g:templates_directory
	let s:directory = <SID>NormalizePath(expand(s:directory) . '/')
	if isdirectory(s:directory)
		execute "au BufNewFile,BufRead "
					\. s:directory . "/" . g:templates_global_name_prefix . "* "
					\. "let b:vim_template_subtype = &filetype | "
					\. "set ft=vim-template"
	endif
	unlet s:directory
endfor

" vim: fdm=marker
