
# Copyright (C) 2011-2020 Daniel Scharrer
#
# This software is provided 'as-is', without any express or implied
# warranty.  In no event will the author(s) be held liable for any damages
# arising from the use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
# 1. The origin of this software must not be misrepresented; you must not
#    claim that you wrote the original software. If you use this software
#    in a product, an acknowledgment in the product documentation would be
#    appreciated but is not required.
# 2. Altered source versions must be plainly marked as such, and must not be
#    misrepresented as being the original software.
# 3. This notice may not be removed or altered from any source distribution.

get_filename_component(VERSION_SCRIPT_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(VERSION_STRING_SCRIPT "${VERSION_SCRIPT_DIR}/VersionScript.cmake")

# Create a rule to generate a version string at compile time.
#
# An optional fifth argument can be used to add additional cmake defines.
#
# SRC is processed using the configure_file() cmake command
# at build to produce DST with the following variable available:
#
# VERSION_SOURCES:
#  List of (${var} ${file}) pairs.
#
# for each variable ${var}
# - ${var}: The contents of the associated file
# - ${var}_COUNT: Number of lines in the associated file
# - For each line ${i}:
#   - ${var}_${i}: The ${i}-th line of the associated file
#    - ${var}_${i}_PREFIX: The first component in the line
#    - ${var}_${i}_LINE: Everything except the first component of the line
#    - ${var}_${i}_NAME: Everything except the last component of the line
#    - ${var}_${i}_STRING: The last component (excluding optional suffix) of the line
#    - ${var}_${i}_SUFFIX: Suffix (separated by " + ") of the line
#    - ${var}_${i}_MAJOR: First version component in ${var}_${i}_STRING
#    - ${var}_${i}_MINOR: Second version component in ${var}_${i}_STRING
#    - ${var}_${i}_PATCH: Third version component in ${var}_${i}_STRING
#    - ${var}_${i}_BUILD: Fourth version component in ${var}_${i}_STRING
#    - ${var}_${i}_NUMBER: Reassembled verion components
#    - ${var}_${i}_PRERELEASE: If the version indicates a prerelease build
#    - ${var}_${i}_PRIVATE: If the version indicates a private build
# - ${var}_HEAD: The first paragraph of the associated file
# - ${var}_TAIL: The remaining paragraphs of the associated file
#
# - GIT_COMMIT: The current git commit. (not defined if there is no GIT_DIR directory)
# - GIT_COMMIT_PREFIX_${i}: The first ${i} characters of GIT_COMMIT (i=0..39)
# For the exact syntax of SRC see the documentation of the configure_file() cmake command.
# The version file is regenerated whenever VERSION_FILE or the current commit changes.
function(version_file SRC DST VERSION_SOURCES GIT_DIR)
	
	set(MODE_VARIABLE 0)
	set(MODE_FILE 1)
	
	set(mode ${MODE_VARIABLE})
	
	set(args)
	set(dependencies "${VERSION_STRING_SCRIPT}")
	
	foreach(arg IN LISTS VERSION_SOURCES)
		
		if(mode EQUAL MODE_VARIABLE)
			set(mode ${MODE_FILE})
		else()
			get_filename_component(arg "${arg}" ABSOLUTE)
			list(APPEND dependencies ${arg})
			set(mode ${MODE_VARIABLE})
		endif()
		
		list(APPEND args ${arg})
		
	endforeach()
	
	get_filename_component(abs_src "${SRC}" ABSOLUTE)
	get_filename_component(abs_dst "${DST}" ABSOLUTE)
	get_filename_component(abs_git_dir "${GIT_DIR}" ABSOLUTE)
	
	set(defines)
	if(ARGC GREATER 4)
		set(defines ${ARGV4})
	endif()
	
	if(EXISTS "${abs_git_dir}")
		find_program(GIT_COMMAND git)
		if(GIT_COMMAND)
			list(APPEND dependencies "${GIT_COMMAND}")
		endif()
		if(EXISTS "${abs_git_dir}/HEAD")
			list(APPEND dependencies "${abs_git_dir}/HEAD")
		endif()
		if(EXISTS "${abs_git_dir}/packed-refs")
			list(APPEND dependencies "${abs_git_dir}/packed-refs")
		endif()
		if(EXISTS "${abs_git_dir}/logs/HEAD")
			list(APPEND dependencies "${abs_git_dir}/logs/HEAD")
		endif()
	else()
		set(abs_git_dir "")
	endif()
	
	add_custom_command(
		OUTPUT
			"${abs_dst}"
		COMMAND
			${CMAKE_COMMAND}
			"-DINPUT=${abs_src}"
			"-DOUTPUT=${abs_dst}"
			"-DVERSION_SOURCES=${args}"
			"-DGIT_DIR=${abs_git_dir}"
			"-DGIT_COMMAND=${GIT_COMMAND}"
			${defines}
			-P "${VERSION_STRING_SCRIPT}"
		COMMAND
			${CMAKE_COMMAND} -E touch "${abs_dst}"
		MAIN_DEPENDENCY
			"${abs_src}"
		DEPENDS
			${dependencies}
		COMMENT ""
		VERBATIM
	)
	
endfunction()

macro(_version_escape var string)
	set(${var} "${string}")
endmacro()

macro(_define_version_var_nostrip suffix contents)
	_version_escape(${var}_${suffix} "${${contents}}")
	list(APPEND variables ${var}_${suffix})
endmacro()

macro(_define_version_var suffix contents)
	string(STRIP "${${contents}}" tmp)
	_define_version_var_nostrip(${suffix} tmp)
endmacro()

macro(_define_version_line_var_nostrip suffix contents)
	_define_version_var_nostrip(${i}_${suffix} ${contents})
	if(line_name)
		set(${line_name}_${suffix} "${${var}_${i}_${suffix}}")
		list(APPEND variables ${line_name}_${suffix})
	endif()
endmacro()

macro(_define_version_line_var suffix contents)
	string(STRIP "${${contents}}" tmp)
	_define_version_line_var_nostrip(${suffix} tmp)
endmacro()

function(parse_version_file names file)
	
	list(GET names 0 var)
	
	list(LENGTH names names_count)
	
	set(variables)
	
	file(READ "${file}" contents)
	string(STRIP "${contents}" contents)
	string(REGEX REPLACE "\r\n" "\n" contents "${contents}")
	string(REGEX REPLACE "\r" "\n" contents "${contents}")
	_version_escape(${var} "${contents}")
	list(APPEND variables ${var})
	
	# Split the version file into lines.
	string(REGEX MATCHALL "[^\r\n]+" lines "${contents}")
	set(i 0)
	foreach(line IN LISTS lines)
		
		if(i LESS names_count)
			list(GET names ${i} line_name)
		else()
			set(line_name)
		endif()
		
		_define_version_var(${i} line)
		
		if(line MATCHES "^([^ ]*) (.*)$")
			set(prefix "${CMAKE_MATCH_1}")
			set(notprefix "${CMAKE_MATCH_2}")
			_define_version_line_var(PREFIX prefix)
			_define_version_line_var(LINE notprefix)
		else()
			_define_version_line_var(PREFIX line)
		endif()
		
		if(line MATCHES "^(.*[^+] )?([^ ]+)( \\+ [^ ]+)?$")
			
			set(name "${CMAKE_MATCH_1}")
			set(string "${CMAKE_MATCH_2}")
			set(suffix "${CMAKE_MATCH_3}")
			
			_define_version_line_var(NAME name)
			_define_version_line_var(STRING string)
			_define_version_line_var_nostrip(SUFFIX suffix)
			
			if(i GREATER 0 AND line_name)
				set(${line_name} "${${var}_${i}_STRING}")
				list(APPEND variables ${line_name})
			endif()
			
			if(string MATCHES "^([0-9]+)(\\.([0-9]+)(\\.([0-9]+)(\\.([0-9]+))?)?)?(.*)?$")
				
				set(major "${CMAKE_MATCH_1}")
				set(minor "${CMAKE_MATCH_3}")
				set(patch "${CMAKE_MATCH_5}")
				set(build "${CMAKE_MATCH_7}")
				set(release "${CMAKE_MATCH_8}")
				
				set(error 0)
				set(newpatch)
				set(newbuild)
				set(prerelease 0)
				set(private 0)
				if(release MATCHES "^\\-dev\\-([2-9][0-9][0-9][0-9]+)\\-([0-9][0-9])\\-([0-9][0-9])$")
					set(prerelease 1)
					set(newpatch "${CMAKE_MATCH_1}")
					set(newbuild "${CMAKE_MATCH_2}${CMAKE_MATCH_3}")
				elseif(release MATCHES "^\\-rc([0-9]+)$")
					set(prerelease 1)
					set(newpatch "9999")
					set(newbuild "${CMAKE_MATCH_1}")
				elseif(release MATCHES "^\\-r([0-9]+)$")
					if(build STREQUAL "")
						set(build "${CMAKE_MATCH_1}")
					else()
						set(error 1)
					endif()
				elseif(release OR suffix)
					set(prerelease 1)
					set(private 1)
					set(newpatch 9999)
					set(newbuild 9999)
				endif()
				
				foreach(component IN ITEMS major minor patch build newpatch newbuild)
					string(REGEX REPLACE "^0+" "" ${component} "${${component}}")
					if(NOT ${component})
						set(${component} 0)
					endif()
				endforeach()
				
				if(newpatch)
					set(prerelease 1)
					if(build)
						set(error 1)
					else()
						if(patch)
							if(newpatch EQUAL 9999)
								math(EXPR patch "${patch} - 1")
								if(newbuild EQUAL 9999)
									set(build 9999)
								else()
									math(EXPR build "${newbuild} + 1000")
								endif()
							else()
								set(error 1)
							endif()
						else()
							if(minor)
								math(EXPR minor "${minor} - 1")
							else()
								if(major)
									math(EXPR major "${major} - 1")
								else()
									set(error 1)
								endif()
								set(minor 9999)
							endif()
							set(patch "${newpatch}")
							set(build "${newbuild}")
						endif()
					endif()
				endif()
				
				set(number "${major}")
				if(minor OR patch OR build)
					set(number "${number}.${minor}")
					if(patch OR build)
						set(number "${number}.${patch}")
						if(build)
							set(number "${number}.${build}")
						endif()
					endif()
				endif()
				
				_define_version_line_var(MAJOR major)
				_define_version_line_var(MINOR minor)
				_define_version_line_var(PATCH patch)
				_define_version_line_var(BUILD build)
				_define_version_line_var(PRERELEASE prerelease)
				_define_version_line_var(PRIVATE private)
				_define_version_line_var(NUMBER number)
				_define_version_line_var(ERROR error)
				
			endif()
			
		endif()
		
		math(EXPR i "${i} + 1")
	endforeach()
	
	set(${var}_COUNT ${i} PARENT_SCOPE)
	
	string(REGEX REPLACE "\n\n.*$" "" head "${contents}")
	_define_version_var(HEAD head)
	
	string(REGEX MATCH "\n\n.*" tail "${contents}")
	_define_version_var(TAIL tail)
	
	foreach(var IN LISTS variables)
		set(${var} "${${var}}" PARENT_SCOPE)
	endforeach()
	
endfunction()
