# Copyright (c) Monetra Technologies LLC
# SPDX-License-Identifier: MIT

# EnableWarnings.cmake
#
# Checks for and turns on a large number of warning C flags.
#
# Adds the following helper functions:
#
#	remove_warnings(... list of warnings ...)
#		Turn off given list of individual warnings for all targets and subdirectories added after this.
#
#   remove_all_warnings()
#		Remove all warning flags, add -w to suppress built-in warnings.
#
#   remove_all_warnings_from_targets(... list of targets ...)
#       Suppress warnings for the given targets only.
#
#   push_warnings()
#		Save current warning flags by pushing them onto an internal stack. Note that modifications to the internal
#		stack are only visible in the current CMakeLists.txt file and its children.
#
#       Note: changing warning flags multiple times in the same directory only affects add_subdirectory() calls.
#             Targets in the directory will always use the warning flags in effect at the end of the CMakeLists.txt
#             file - this is due to really weird and annoying legacy behavior of CMAKE_C_FLAGS.
#
#   pop_warnings()
#       Restore the last set of flags that were saved with push_warnings(). Note that modifications to the internal
#		stack are only visible in the current CMakeLists.txt file and its children.
#

if (_internal_enable_warnings_already_run)
	return()
endif ()
set(_internal_enable_warnings_already_run TRUE)

include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)

get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)

# internal helper: _int_enable_warnings_set_flags_ex(langs_var configs_var [warnings flags])
function(_int_enable_warnings_set_flags_ex langs_var configs_var)
	if (NOT ARGN)
		return()
	endif ()

	if (NOT ${configs_var})
		set(${configs_var} "NONE")
	endif ()
	string(TOUPPER "${${configs_var}}" ${configs_var})

	foreach(_flag ${ARGN})
		string(MAKE_C_IDENTIFIER "HAVE_${_flag}" varname)

		if ("C" IN_LIST ${langs_var})
			check_c_compiler_flag(${_flag} ${varname})
			if (${varname})
				foreach (config IN LISTS ${configs_var})
					if (config STREQUAL "NONE")
						set(config)
					else ()
						set(config "_${config}")
					endif ()
					string(APPEND CMAKE_C_FLAGS${config} " ${_flag}")
				endforeach ()
			endif ()
		endif ()

		if ("CXX" IN_LIST ${langs_var})
			string(APPEND varname "_CXX")
			check_cxx_compiler_flag(${_flag} ${varname})
			if (${varname})
				foreach (config IN LISTS ${configs_var})
					if (config STREQUAL "NONE")
						set(config)
					else ()
						set(config "_${config}")
					endif ()
					string(APPEND CMAKE_CXX_FLAGS${config} " ${_flag}")
				endforeach ()
			endif ()
		endif ()
	endforeach()

	foreach(lang C CXX)
		foreach (config IN LISTS ${configs_var})
			string(TOUPPER "${config}" config)
			if (config STREQUAL "NONE")
				set(config)
			else ()
				set(config "_${config}")
			endif ()
			string(STRIP "${CMAKE_${lang}_FLAGS${config}}" CMAKE_${lang}_FLAGS${config})
			set(CMAKE_${lang}_FLAGS${config} "${CMAKE_${lang}_FLAGS${config}}" PARENT_SCOPE)
		endforeach ()
	endforeach()
endfunction()

# internal helper: _int_enable_warnings_set_flags(langs_var [warnings flags])
macro(_int_enable_warnings_set_flags langs_var)
	set(configs "NONE")
	_int_enable_warnings_set_flags_ex(${langs_var} configs ${ARGN})
endmacro()

set(_flags_C)
set(_flags_CXX)
set(_debug_flags_C)
set(_debug_flags_CXX)

if (MSVC)
	# Visual Studio uses a completely different nomenclature for warnings than gcc/mingw/clang, so none of the
	# "-W[name]" warnings will work.

	# W4 would be better but it produces unnecessary warnings like:
	# *  warning C4706: assignment within conditional expression
	#     Triggered when doing "while(1)"
	# * warning C4115: 'timeval' : named type definition in parentheses
	# * warning C4201: nonstandard extension used : nameless struct/union
	#     Triggered by system includes (commctrl.h, shtypes.h, Shlobj.h)
	set(_flags
		/W3
		/we4013 # Treat "function undefined, assuming extern returning int" warning as an error. https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4013
	)

	# Disable some warnings to reduce noise level on visual studio.
	if (NOT WIN32_STRICT_WARNINGS)
		list(APPEND _flags
			/wd4018 # Disable signed/unsigned mismatch warnings. https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4018
			/wd4068 # Disable unknown pragma warning. https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4068
			/wd4244 # Disable integer type conversion warnings. https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-levels-3-and-4-c4244
			/wd4267 # Disable warnings about converting size_t to a smaller type. https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-3-c4267
		)
	endif ()
	list(APPEND _flags_C   ${_flags})
	list(APPEND _flags_CXX ${_flags})

elseif (CMAKE_C_COMPILER_ID MATCHES "Intel")
	# Intel's compiler warning flags are more like Visual Studio than GCC, though the numbers aren't the same.
	set(_flags
		# Use warning level 3, quite wordy.
		-w3
		# Disable warnings we don't care about (add more as they are encountered).
		-wd383    # Spammy warning about initializing from a temporary object in C++ (which is done all the time ...).
		-wd11074  # Diagnostic related to inlining.
		-wd11076  # Diagnostic related to inlining.
	)

	list(APPEND _flags_C   ${_flags})
	list(APPEND _flags_CXX ${_flags})

elseif (CMAKE_C_COMPILER_ID MATCHES "XL")
	set (_flags
		-qwarn64
		-qformat=all
		-qflag=i:i
	)
	list(APPEND _flags_C   ${_flags})
	list(APPEND _flags_CXX ${_flags})

else ()
	# If we're compiling with GCC / Clang / MinGW (or anything else besides Visual Studio or Intel):
	# C Flags:
	list(APPEND _flags_C
		-Wall
		-Wextra

		# Enable additional warnings not covered by Wall and Wextra.
		-Wcast-align
		-Wconversion
		-Wdeclaration-after-statement
		-Wdouble-promotion
		-Wfloat-equal
		-Wformat-security
		-Winit-self
		-Wjump-misses-init
		-Wlogical-op
		-Wmissing-braces
		-Wmissing-declarations
		-Wmissing-format-attribute
		-Wmissing-include-dirs
		-Wmissing-prototypes
		-Wnested-externs
		-Wno-coverage-mismatch
		-Wold-style-definition
		-Wpacked
		-Wpointer-arith
		-Wredundant-decls
		-Wshadow
		-Wsign-conversion
		-Wstrict-overflow
		-Wstrict-prototypes
		-Wtrampolines
		-Wundef
		-Wunused
		-Wvariadic-macros
		-Wvla
		-Wwrite-strings

		# On Windows MinGW I think implicit fallthrough enabled by -Wextra must not default to 3
		-Wimplicit-fallthrough=3

		# Treat implicit variable typing and implicit function declarations as errors.
		-Werror=implicit-int
		-Werror=implicit-function-declaration

		# Make MacOSX honor -mmacosx-version-min
		-Werror=partial-availability

		# Some clang versions might warn if an argument like "-I/path/to/headers" is unused,
		# silence these.
		-Qunused-arguments
	)

	# C++ flags:
	list(APPEND _flags_CXX
		-Wall
		-Wextra

		# Enable additional warnings not covered by Wall and Wextra.
		-Wcast-align
		-Wformat-security
		-Wmissing-declarations
		-Wmissing-format-attribute
		-Wpacked-bitfield-compat
		-Wredundant-decls
		-Wvla

		# Turn off unused parameter warnings with C++ (they happen often in C++ and Qt).
		-Wno-unused-parameter

		# Some clang versions might warn if an argument like "-I/path/to/headers" is unused,
		# silence these.
		-Qunused-arguments
	)

	# Note: when cross-compiling to Windows from Cygwin, the Qt Mingw packages have a bunch of
	#       noisy type-conversion warnings in headers. So, only enable those warnings if we're
	#       not building that configuration.
	if (NOT (WIN32 AND (CMAKE_HOST_SYSTEM_NAME MATCHES "CYGWIN")))
		list(APPEND _flags_CXX
			-Wconversion
			-Wfloat-equal
			-Wsign-conversion
		)
	endif ()

	# Add flags to force colored output even when output is redirected via pipe.
	if (CMAKE_GENERATOR MATCHES "Ninja")
		set(color_default TRUE)
	else ()
		set(color_default FALSE)
	endif ()
	option(FORCE_COLOR "Force compiler to always colorize, even when output is redirected." ${color_default})
	mark_as_advanced(FORCE FORCE_COLOR)
	if (FORCE_COLOR)
		set(_flags
			-fdiagnostics-color=always # GCC
			-fcolor-diagnostics        # Clang
		)
		list(APPEND _flags_C   ${_flags})
		list(APPEND _flags_CXX ${_flags})
	endif ()

	# Add -fno-omit-frame-pointer (and optionally -fno-inline) to make debugging and stack dumps nicer.
	set(_flags
		-fno-omit-frame-pointer
	)
	option(M_NO_INLINE "Disable function inlining for RelWithDebInfo and Debug configurations?" FALSE)
	if (M_NO_INLINE)
		list(APPEND _flags
			-fno-inline
		)
	endif ()
	list(APPEND _debug_flags_C   ${_flags})
	list(APPEND _debug_flags_CXX ${_flags})
endif ()

# Check and set compiler flags.
set(_debug_configs
	RelWithDebInfo
	Debug
)
foreach(_lang ${languages})
	_int_enable_warnings_set_flags(_lang ${_flags_${_lang}})
	_int_enable_warnings_set_flags_ex(_lang _debug_configs ${_debug_flags_${_lang}})

	# Ensure pure Debug builds are NOT optimized (not possible on Visual Studio).
	# Any optimization of a Debug build will prevent debuggers like lldb from
	# fully displaying backtraces and stepping.
	if (NOT MSVC)
		set(_config Debug)
		_int_enable_warnings_set_flags_ex(_lang _config -O0)
	endif ()
endforeach()



# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Helper functions


# This function can be called in subdirectories, to prune out warnings that they don't want.
#  vararg: warning flags to remove from list of enabled warnings. All "no" flags after EXPLICIT_DISABLE
#          will be added to C flags.
#
# Ex.: remove_warnings(-Wall -Wdouble-promotion -Wcomment) prunes those warnings flags from the compile command.
function(remove_warnings)
	get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
	set(langs C)
	if ("CXX" IN_LIST languages)
		list(APPEND langs CXX)
	endif ()

	foreach(lang ${langs})
		set(toadd)
		set(in_explicit_disable FALSE)
		foreach (flag ${ARGN})
			if (flag STREQUAL "EXPLICIT_DISABLE")
				set(in_explicit_disable TRUE)
			elseif (in_explicit_disable)
				list(APPEND toadd "${flag}")
			else ()
				string(REGEX REPLACE "${flag}([ \t]+|$)" "" CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}")
			endif ()
		endforeach ()
		_int_enable_warnings_set_flags(lang ${toadd})
		string(STRIP "${CMAKE_${lang}_FLAGS}" CMAKE_${lang}_FLAGS)
		set(CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}" PARENT_SCOPE)
	endforeach()
endfunction()


# Explicitly suppress all warnings. As long as this flag is the last warning flag, warnings will be
# suppressed even if earlier flags enabled warnings.
function(remove_all_warnings)
	get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
	set(langs C)
	if ("CXX" IN_LIST languages)
		list(APPEND langs CXX)
	endif ()

	foreach(lang ${langs})
		string(REGEX REPLACE "[-/][Ww][^ \t]*([ \t]+|$)" "" CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}")
		if (MSVC)
			string(APPEND CMAKE_${lang}_FLAGS " /w")
		else ()
			string(APPEND CMAKE_${lang}_FLAGS " -w")
		endif ()
		string(STRIP "${CMAKE_${lang}_FLAGS}" CMAKE_${lang}_FLAGS)
		set(CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}" PARENT_SCOPE)
	endforeach()
endfunction()


function(remove_all_warnings_from_targets)
	foreach (target ${ARGN})
		if (MSVC)
			target_compile_options(${target} PRIVATE "/w")
		else ()
			target_compile_options(${target} PRIVATE "-w")
		endif ()
	endforeach()
endfunction()


# Save the current warning settings to an internal variable.
function(push_warnings)
	get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
	set(langs C)
	if ("CXX" IN_LIST languages)
		list(APPEND langs CXX)
	endif ()

	foreach(lang ${langs})
		if (CMAKE_${lang}_FLAGS MATCHES ";")
			message(AUTHOR_WARNING "Cannot push warnings for ${lang}, CMAKE_${lang}_FLAGS contains semicolons")
			continue()
		endif ()
		# Add current flags to end of internal list.
		list(APPEND _enable_warnings_internal_${lang}_flags_stack "${CMAKE_${lang}_FLAGS}")
		# Propagate results up to caller's scope.
		set(_enable_warnings_internal_${lang}_flags_stack "${_enable_warnings_internal_${lang}_flags_stack}" PARENT_SCOPE)
	endforeach()
endfunction()


# Restore the current warning settings from an internal variable.
function(pop_warnings)
	get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
	set(langs C)
	if ("CXX" IN_LIST languages)
		list(APPEND langs CXX)
	endif ()

	foreach(lang ${langs})
		if (NOT _enable_warnings_internal_${lang}_flags_stack)
			continue()
		endif ()
		# Pop flags off of end of list, overwrite current flags with whatever we popped off.
		list(GET _enable_warnings_internal_${lang}_flags_stack -1 CMAKE_${lang}_FLAGS)
		list(REMOVE_AT _enable_warnings_internal_${lang}_flags_stack -1)
		# Propagate results up to caller's scope.
		set(_enable_warnings_internal_${lang}_flags_stack "${_enable_warnings_internal_${lang}_flags_stack}" PARENT_SCOPE)
		string(STRIP "${CMAKE_${lang}_FLAGS}" CMAKE_${lang}_FLAGS)
		set(CMAKE_${lang}_FLAGS "${CMAKE_${lang}_FLAGS}" PARENT_SCOPE)
	endforeach()
endfunction()
