*! version 2.9.0 28mar2017
/*
MS_COMPILE_MATA: Compile a Mata library (.mlib), if needed

USAGE:

1) In xyz.ado:

	-------------------------------------------------------------------------
	*! version 1.2.3 31dec2017

	program xyz
		...
		ms_get_version xyz
		ms_compile_mata, package(xyz) version(`package_version')
		...
	end
	-------------------------------------------------------------------------


2) In xyz.mata:

	-------------------------------------------------------------------------
	ms_get_version xyz // stores version of the ado in local `package_version'
	assert("`package_version'" != "")
	mata: string scalar xyz_version() return("`package_version'")
	mata: string scalar xyz_stata_version() return("`c(stata_version)'")
	...
	-------------------------------------------------------------------------

ADVANCED SYNTAX:

	ms_compile_mata, PACKage(...) VERsion(...) [FUNctions(...) VERBOSE FORCE DEBUG]

		functions:	list of Mata functions from xyz.mata that will be added
					to the .mlib. Default is all functions: *()

		force:		always compile the package and create a new .mlib
					By default, this only happens if the mlib doesn't exist
					or if the package versions and Stata versions disagree
					with what the .mlib has stored


NOTE:	the names of the .mata and .ado files can be different
		(in fact, ms_compile_mata doesn't know the name of the .ado!)

ACKNOWLEDGEMENT: based on code from David Roodman's -boottest-

*/

program ms_compile_mata
	syntax, PACKage(string) VERsion(string) [FUNctions(string)] [VERBOSE] [FORCE] [DEBUG]
	loc force = ("`force'" != "")

	if (!`force') {
		Check, package(`package') version(`version') `verbose'
		loc force = s(needs_compile)
	}

	if (`force') {
		Compile, package(`package') version(`version') functions(`functions') `verbose' `debug'
	}
end


program Check, sclass
	syntax, PACKage(string) VERSion(string) [VERBOSE]
	loc verbose = ("`verbose'" != "")

	loc package_version = "`version'"
	loc stata_version = c(stata_version)
	loc joint_version = "`package_version'|`stata_version'"
	
	loc mlib_package_version = "???"
	loc mlib_stata_version = "???"
	loc mlib_joint_version = "???"


	// Jointly check if the package and Stata versions are the same

	cap mata: mata drop `package'_joint_version()
	cap mata: st_local("mlib_joint_version", `package'_joint_version())
	_assert inlist(`c(rc)', 0, 3499), msg("`package' check: unexpected error")

	if ("`mlib_joint_version'" == "`joint_version'") {
		sreturn local needs_compile = 0
		exit
	}

	 // Does the MLIB has the same version as the one stated in the ADO?

	cap mata: mata drop `package'_version()
	cap mata: st_local("mlib_stata_version", `package'_stata_version())
	_assert inlist(`c(rc)', 0, 3499), msg("`package' check: unexpected error")

	if ("`mlib_stata_version'" != "`stata_version'") {
		if (`verbose') di as text "(existing l`package'.mlib compiled with Stata `mlib_stata_version'; need to recompile for Stata `stata_version')"
		sreturn local needs_compile = 1
		exit
	}

	 // Was the MLIB compiled with the current version of Stata?

	cap mata: mata drop `package'_stata_version()
	cap mata: st_local("mlib_package_version", `package'_version())
	_assert inlist(`c(rc)', 0, 3499), msg("`package' check: unexpected error")

	if ("`mlib_package_version'" != "`package_version'") {
		if (`verbose') di as text `"(existing l`package'.mlib is version "`mlib_package_version'"; need to recompile for "`package_version'")"'
		sreturn local needs_compile = 1
		exit
	}
end


program Compile
	syntax, PACKage(string) VERSion(string) [FUNctions(string)] [VERBOSE] [DEBUG]
	loc verbose = ("`verbose'" != "")
	loc debug = ("`debug'" != "")
	if ("`functions'"=="") loc functions "*()"

	loc stata_version = c(stata_version)

	mata: mata clear
	
	* Delete any preexisting .mlib
	loc mlib "l`package'.mlib"
	cap findfile "`mlib'"
	while (c(rc)!=601) {
	        * Try to delete file
	        cap erase "`r(fn)'"
	        
	        * Catch exception when file is read-only
	        if c(rc)==608 {
	        	di as error "(warning: file `r(fn)' is read-only; skipping delete)"
	        	continue, break
	        }
	        * Abort in case of other errors
	        else if c(rc) {
	        	di as error "Cannot delete `r(fn)'; error `c(rc)'; aborting"
	        	error `c(rc)'
	        }

	        * Check if the mlib file still persists somewhere (error 601: file not found)
	        cap findfile "`mlib'"
	}

	* Run the .mata
	if (`verbose') di as text "(compiling l`package'.mlib for Stata `stata_version')"
	qui findfile "`package'.mata"
	loc fn "`r(fn)'"
	run "`fn'"

	if (`debug') di as error "Functions available for indexing:"
	if (`debug') mata: mata desc
	
	* Find out where can I save the .mlib
	* Try directories in order specified by S_ADO, skipping BASE/SITE/OLDPLACE/"."
	* If all fail, then try current working directory (".")
	tokenize `"$S_ADO"', parse(";")
	local ok 0
	while (!`ok') {

		local path `"`1'"'
		if `"`path'"'=="PLUS" local path `"`c(sysdir_plus)'"'
		else if `"`path'"'=="PERSONAL" local path `"`c(sysdir_personal)'"'
		
		* Skip directories in S_ADO that do no exist or are not accessible
		mata : st_local("dir_ok", strofreal(direxists(`"`path'"')))
		if `dir_ok'==0 & `"`1'"' != ""{
			macro shift
			continue
		}
		
		if !inlist(`"`path'"',".","") TrySave `"`path'"' "`1'" "`package'" "`functions'" `debug' `verbose'
		
		* Final effort after reaching end of S_ADO: try installing to current directory
		if(`"`1'"' == "" & !`ok') {
			TrySave "." "current path" "`package'" "`functions'" `debug' `verbose'
			
			if (!`ok') {
				di as error "Could not compile file; ftools will not work correctly"
				error 123
			}			
		}
		macro shift
	}

end


program TrySave
	args path name package functions debug verbose
	assert "`package'"!=""
	loc random_file = "`=int(runiform()*1e8)'"
	cap conf new file `"`path'/`random_file'"'
	if (c(rc)) {
		di as error `"cannot save compiled Mata file in `name' (`path')"'
		c_local ok 0
		exit
	}
	else {
		loc path "`path'/l/"
		cap conf new file "`path'`random_file'"
		if (c(rc)) {
			mkdir "`path'"
		}

		cap conf new file "`path'l`package'.mlib"

		* Create .mlib
		cap mata: mata mlib create l`package'  , dir("`path'") replace
		if c(rc)==608 {
			c_local ok 0
			exit
		}
		else if c(rc) {
			di as error "could not compile mlib file"
			error 608
		}
		qui mata: mata mlib add l`package' `functions', dir("`path'") complete
		//qui mata: mata mlib add l`package' HDFE() , dir("`path'") complete
		
		* Verify file exists and works correctly
		qui findfile l`package'.mlib
		loc fn `r(fn)'
		if (`verbose') di as text `"(library saved in `fn')"'
		qui mata: mata mlib index

		if (`debug') di as error "Functions indexed:"
		if (`debug') mata: mata describe using l`package'

		c_local ok 1
	}
end