# This file contains utilities used by both leatherman and consuming
# projects.

# Save the directory containing other cmake script files.
# If we're top-level, this file is generated and dropped
# in a different directory from the other script files.
if(LEATHERMAN_TOPLEVEL)
    set(LEATHERMAN_CMAKE_DIR ${CMAKE_SOURCE_DIR}/cmake)
else()
    set(LEATHERMAN_CMAKE_DIR ${CMAKE_CURRENT_LIST_DIR})
endif()

# Usage: leatherman_logging_namespace("namespace")
#
# Sets the LEATHERMAN_LOGGING_NAMESPACE preprocessor definition to the
# value passed as "namespace".
macro(leatherman_logging_namespace namespace)
    add_definitions("-DLEATHERMAN_LOGGING_NAMESPACE=\"${namespace}\"")
endmacro()

# Usage: leatherman_logging_line_numbers()
#
# Sets the LEATHERMAN_LOGGING_LINE_NUMBERS preprocessor definition.
macro(leatherman_logging_line_numbers)
    add_definitions("-DLEATHERMAN_LOGGING_LINE_NUMBERS")
endmacro()

# Usage: debug("Something cool is happening")
#
# Print message if LEATHERMAN_DEBUG is set. Used to introspect macro
# logic.
macro(debug str)
    if (LEATHERMAN_DEBUG)
        message(STATUS ${str})
    endif()
endmacro(debug)

# Usage: export_var("foobar")
#
# Sets variable "foobar" in the parent scope to the same value as
# "foobar" in the invoking scope. Remember that a macro does not
# create a new scope, but a function does.
macro(export_var varname)
    if (NOT "${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_SOURCE_DIR}")
        debug("Exporting ${varname}")
        set(${varname} ${${varname}} PARENT_SCOPE)
    else()
        debug("Skipping export of ${varname} because I'm top-level")
    endif()
    debug("It's value is: ${${varname}}")
endmacro(export_var)

# Usage: defoption(VARNAME "Documentation String" ${DEFAULT_VALUE}")
#
# Define an option that will only be set to DEFAULT_VALUE if it does
# not already exist in this scope. If the variable is available in the
# scope, the option will keep the current value. This works around a
# weird CMake behavior where set(OPTION_VAR TRUE) does not cause
# option() to ignore its default.
macro(defoption name doc default)
    if(DEFINED ${name})
        debug("${name} is already set, using it")
        set(enabled ${${name}})
    else()
        debug("${name} unset, using default")
        set(enabled ${default})
    endif()
    option(${name} ${doc} ${enabled})
endmacro()

# Usage: leatherman_install(TARGETS)
#
# Installs targets using common cross-platform configuration.
# On Windows shared libraries go in bin, import and archive libraries
# go in lib. On Linux shared libraries go in lib. Binaries go in bin.
#
# Also always drop the prefix; give the target its expected name.
# We often have binaries and related dynamic libraries, and this
# simplifies giving them different but related names, such as
# `facter` and `libfacter`.
macro(leatherman_install)
    install(TARGETS ${ARGV}
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib${LIB_SUFFIX}
        ARCHIVE DESTINATION lib${LIB_SUFFIX})
    foreach(ARG ${ARGV})
        if (TARGET ${ARG})
            set_target_properties(${ARG} PROPERTIES PREFIX "" IMPORT_PREFIX "")
        endif()
    endforeach()
endmacro()

# Usage: add_cppcheck_dirs(dir1 dir2)
#
# Add the listed directories to the set that cppcheck will be run
# against
macro(add_cppcheck_dirs)
    list(APPEND CPPCHECK_DIRS ${ARGV})
    export_var(CPPCHECK_DIRS)
endmacro()

# Usage: add_cpplint_files(file1 file2)
#
# Add the listed files to the set that cpplint will be run against
macro(add_cpplint_files)
    list(APPEND CPPLINT_FILES ${ARGV})
    export_var(CPPLINT_FILES)
endmacro()

# Usage: enable_cppcheck()
#
# Create the cppcheck custom target with all the directories specified
# in previous calls to `add_cppcheck_dirs`
macro(enable_cppcheck)
    add_custom_target(cppcheck COMMAND cppcheck --enable=warning,performance --error-exitcode=2 --quiet --inline-suppr ${CPPCHECK_DIRS})
endmacro()

# We set this here so that enable_cpplint() can find it
set(LEATHERMAN_CPPLINT_PATH "${LEATHERMAN_CMAKE_DIR}/../scripts/cpplint.py")

# Usage: enable_cpplint()
#
# Create the cpplint custom target with all the specified in previous
# calls to `add_cpplint_files`
macro(enable_cpplint)
    include(FindPythonInterp)
    if (NOT PYTHONINTERP_FOUND)
        message(STATUS "Python not found; 'cpplint' target will not be available")
    else()
        set(CPPLINT_FILTER
            "-build/c++11"            # <thread>, <condvar>, etc...
            "-whitespace/indent"      # We use 4 space indentation
            "-build/include"          # Why?
            "-build/namespaces"       # What's a namespace to do
            "-legal/copyright"        # Not yet
            "-runtime/references"     # Not sure about this religion
            "-readability/streams"    # What?
            "-readability/namespace"  # Ignore nested namespace comment formatting
            "-whitespace/braces"      # Is there a k&r setting?
            "-whitespace/line_length" # Well yeah, but ... not just now
            "-runtime/arrays"         # Sizing an array with a 'const int' doesn't make it variable sized
            "-readability/todo"       # Seriously? todo comments need to identify an owner? pffft
            "-whitespace/empty_loop_body" # Can't handle do { ... } while(expr);
            "-runtime/int"            # Some C types are needed for library interop
            "-runtime/explicit"       # Using implicit conversion from string to regex for regex calls.
            "-build/header_guard"     # Disable header guards (cpplint doesn't yet support enforcing #pragma once)
            "-runtime/indentation_namespace" # Our namespace indentation is not consistent
            "-readability/inheritance" # virtual/override sometimes used together
            "-whitespace/operators"   # Expects spaces around perfect forwarding (&&)
        )

        set(CPPLINT_ARGS "--extensions=cc,cpp,hpp,h")
        if (CPPLINT_FILTER)
            string(REPLACE ";" "," CPPLINT_FILTER "${CPPLINT_FILTER}")
            set(CPPLINT_ARGS "${CPPLINT_ARGS};--filter=${CPPLINT_FILTER}")
        endif()
        if (MSVC)
            set(CPPLINT_ARGS "${CPPLINT_ARGS};--output=vs7")
        endif()

        add_custom_target(cpplint
            COMMAND ${PYTHON_EXECUTABLE} ${LEATHERMAN_CPPLINT_PATH} ${CPPLINT_ARGS} ${CPPLINT_FILES}
            VERBATIM
        )
    endif()
endmacro()

# Usage: gettext_templates(dir)
#
# Create templates for gettext in `dir` from the source files specified as additional arguments.
# Creates a custom target `translation`.
macro(gettext_templates dir)
    # Don't even try to find gettext on AIX or Solaris, we don't want it.
    if (LEATHERMAN_USE_LOCALES AND LEATHERMAN_GETTEXT)
        find_program(XGETTEXT_EXE xgettext)
    endif()

    if (XGETTEXT_EXE)
        set(TRANSLATION_DIR "${dir}")
        file(MAKE_DIRECTORY ${TRANSLATION_DIR})

        set(ALL_PROJECT_SOURCES ${ARGN})
        set(lang_template ${TRANSLATION_DIR}/${PROJECT_NAME}.pot)
        add_custom_command(OUTPUT ${lang_template}
            COMMAND ${XGETTEXT_EXE}
                --sort-by-file
                --copyright-holder "Puppet \\<docs@puppet.com\\>"
                --package-name=${PROJECT_NAME} --package-version=${PROJECT_VERSION}
                --msgid-bugs-address "docs@puppet.com"
                -d ${PROJECT_NAME} -o ${lang_template}
                --keyword=LOG_DEBUG:1,\\"debug\\"
                --keyword=LOG_INFO:1,\\"info\\"
                --keyword=LOG_WARNING:1,\\"warning\\"
                --keyword=LOG_ERROR:1,\\"error\\"
                --keyword=LOG_FATAL:1,\\"fatal\\"
                --keyword=log:2,\\"log\\"
                --keyword=translate:1
                --keyword=translate_n:1,2
                --keyword=translate_p:1c,2
                --keyword=translate_np:1c,2,3
                --keyword=format:1
                --keyword=format_n:1,2
                --keyword=format_p:1c,2
                --keyword=format_np:1c,2,3
                --keyword=_:1
                --keyword=n_:1,2
                --keyword=p_:1c,2
                --keyword=np_:1c,2,3
                --add-location=file
                --add-comments=LOCALE
                ${ALL_PROJECT_SOURCES}
            COMMAND ${CMAKE_COMMAND}
                -DPOT_FILE=${lang_template}
                -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
                -P ${LEATHERMAN_CMAKE_DIR}/normalize_pot.cmake
            DEPENDS ${ALL_PROJECT_SOURCES})

        add_custom_target(${PROJECT_NAME}.pot ALL DEPENDS ${lang_template})

        find_program(MSGINIT_EXE msginit)
        find_program(MSGMERGE_EXE msgmerge)
        if (MSGINIT_EXE AND MSGMERGE_EXE)
            foreach(lang ${LEATHERMAN_LOCALES})
                set(lang_file ${TRANSLATION_DIR}/${lang}.po)
                add_custom_command(OUTPUT ${lang_file}
                    COMMAND ${CMAKE_COMMAND}
                        -DPOT_FILE=${lang_template}
                        -DLANG_FILE=${lang_file}
                        -DLANG=${lang}
                        -DMSGMERGE_EXE=${MSGMERGE_EXE}
                        -DMSGINIT_EXE=${MSGINIT_EXE}
                        -P ${LEATHERMAN_CMAKE_DIR}/generate_translations.cmake
                    DEPENDS ${lang_template})
                add_custom_target(${PROJECT_NAME}-${lang}.pot ALL DEPENDS ${lang_file})
            endforeach()
        endif()
    else()
        message(STATUS "Could not find gettext executables, skipping gettext_templates.")
    endif()
endmacro()

# Usage: gettext_compile(dir inst)
#
# Compile gettext .po files into .mo files and configure installing to inst
# Creates a custom target `translations`.
#
# Does nothing if msgfmt (part of gettext) isn't found. Sets GETTEXT_ENABLED
# to ON if we can compile .mo files, otherwise sets to OFF. This variable can
# be used to disable functionality (such as testing) that requires gettext
# translation files.
macro(gettext_compile dir inst)
    # Don't even try to find gettext on AIX or Solaris, we don't want it.
    if (LEATHERMAN_USE_LOCALES AND LEATHERMAN_GETTEXT)
        find_program(MSGFMT_EXE msgfmt)
    endif()

    if (MSGFMT_EXE)
        file(GLOB TRANSLATIONS ${dir}/*.po)
        if (NOT TARGET translations)
            add_custom_target(translations ALL)
        endif()

        # Add LEATHERMAN_LOCALES, as they may not have been generated yet.
        foreach(locale ${LEATHERMAN_LOCALES})
            set(fpath ${dir}/${locale}.po)
            list(FIND TRANSLATIONS ${fpath} FOUND)
            if (${FOUND} EQUAL -1)
                list(APPEND TRANSLATIONS ${fpath})
            endif()
        endforeach()

        foreach(fpath ${TRANSLATIONS})
            get_filename_component(lang ${fpath} NAME_WE)
            set(mo ${CMAKE_BINARY_DIR}/${lang}/LC_MESSAGES/${PROJECT_NAME}.mo)
            add_custom_command(OUTPUT ${mo}
                COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/${lang}/LC_MESSAGES
                COMMAND ${MSGFMT_EXE} -c -v -o ${mo} ${fpath} 2>&1
                DEPENDS ${fpath})
            add_custom_target(${lang}-${PROJECT_NAME} DEPENDS ${mo})
            add_dependencies(translations ${lang}-${PROJECT_NAME})
            if(LEATHERMAN_LOCALE_INSTALL)
                install(FILES ${mo} DESTINATION "@CMAKE_INSTALL_PREFIX@/${LEATHERMAN_LOCALE_INSTALL}/${lang}/LC_MESSAGES")
            else()
                install(FILES ${mo} DESTINATION "@CMAKE_INSTALL_PREFIX@/share/locale/${lang}/LC_MESSAGES")
            endif()
        endforeach()
        set(GETTEXT_ENABLED ON)
    else()
        message(STATUS "Could not find gettext executables, skipping gettext_compile.")
        set(GETTEXT_ENABLED OFF)
    endif()
endmacro()

include(GetGitRevisionDescription)
# Usage: get_commit_string(VARNAME)
#
# Sets VARNAME to the git commit revision string, i.e. (commit SHA1)
function(get_commit_string varname)
    get_git_head_revision(GIT_REFSPEC GIT_SHA1)
    debug("Git SHA1 is ${GIT_SHA1}")
    if ("${GIT_SHA1}" STREQUAL "" OR "${GIT_SHA1}" STREQUAL "GITDIR-NOTFOUND")
        set(${varname} "" PARENT_SCOPE)
    else()
        set(${varname} " (commit ${GIT_SHA1})" PARENT_SCOPE)
    endif()
endfunction()

include(GenerateExportHeader)
# Usage: symbol_exports(TARGET HEADER)
#
# Generate the export header for restricting symbols exported from the library,
# and configure the compiler. Restricting symbols has several advantages, noted
# at https://gcc.gnu.org/wiki/Visibility.
macro(symbol_exports target header)
    generate_export_header(${target} EXPORT_FILE_NAME "${header}")
    # Export on Apple resulted in issues finding symbols from library dependencies
    # that we haven't solved. For now avoid the problem.
    # AIX doesn't support inline headers, and CMake warns if you try to apply the
    # option on static libraries.
    get_target_property(target_type ${target} TYPE)
    if ((NOT APPLE) AND (NOT CMAKE_SYSTEM_NAME MATCHES "AIX") AND
        (${target_type} STREQUAL SHARED_LIBRARY))
        set_target_properties(${target} PROPERTIES VISIBILITY_INLINES_HIDDEN ON)
    endif()
    # If the target name is not a C identifier, generate_export_header will
    # convert it to one. Fix the define to do the same.
    string(MAKE_C_IDENTIFIER ${target} target_c_name)
    string(TOLOWER ${target_c_name} target_name_lower)
    target_compile_definitions(${target} PRIVATE "-D${target_name_lower}_EXPORTS")
endmacro()

# Usage: append_new(VARNAME VAR1 VAR2 ...)
#
# Append ARGN items to VARNAME list if not already present. Accounts for
# optimized/debug flags.
function(append_new varname)
    set(prefix "")
    foreach(DEP ${ARGN})
        if ((${DEP} STREQUAL optimized) OR (${DEP} STREQUAL debug))
            set(prefix ${DEP})
        else()
            list(FIND ${varname} ${DEP} found)
            if (${found} EQUAL -1)
                if (prefix)
                    list(APPEND ${varname} ${prefix})
                endif()
                list(APPEND ${varname} ${DEP})
            endif()
            set(prefix "")
        endif()
    endforeach()
    set(${varname} ${${varname}} PARENT_SCOPE)
endfunction()

# Usage: unpack_vendored("pkg.zip" "pkg" SOURCE_DIR)
#
# Unpacks a compressed pkg.zip in the vendor directory to
# ${PROJECT_BINARY_DIR}/src/pkg and saves the unpacked location to a variable.
macro(unpack_vendored pkg extracted_dir dir)
    set(pkgfile ${PROJECT_SOURCE_DIR}/vendor/${pkg})
    set(${dir} ${PROJECT_BINARY_DIR}/src/${extracted_dir})

    message(STATUS "Unpacking ${pkgfile} into ${${dir}}")
    file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/src)
    execute_process(
        COMMAND ${CMAKE_COMMAND} -E tar xzf ${PROJECT_SOURCE_DIR}/vendor/${pkg}
        WORKING_DIRECTORY ${PROJECT_BINARY_DIR}/src
        )
endmacro(unpack_vendored)