#
#  Copyright (c) 2018 Anderson Toshiyuki Sasaki <ansasaki@redhat.com>
#
#  Redistribution and use is allowed according to the terms of the New
#  BSD license.
#  For details see the accompanying COPYING-CMAKE-SCRIPTS file.
#

#.rst:
# FindABIMap
# ----------
#
# This file provides functions to generate the symbol version script. It uses
# the ``abimap`` tool to generate and update the linker script file. It can be
# installed by calling::
#
#   $ pip install abimap
#
# The ``function generate_map_file`` generates a symbol version script
# containing the provided symbols. It defines a custom command which sets
# ``target_name`` as its ``OUTPUT``.
#
# The experimental function ``extract_symbols()`` is provided as a simple
# parser to extract the symbols from C header files. It simply extracts symbols
# followed by an opening '``(``'. It is recommended to use a filter pattern to
# select the lines to be considered. It defines a custom command which sets
# ``target_name`` as its output.
#
# The helper function ``get_files_list()`` is provided to find files given a
# name pattern. It defines a custom command which sets ``target_name`` as its
# output.
#
# Functions provided
# ------------------
#
# ::
#
#   generate_map_file(target_name
#                     RELEASE_NAME_VERSION release_name
#                     SYMBOLS symbols_target
#                     [CURRENT_MAP cur_map]
#                     [FINAL]
#                     [BREAK_ABI]
#                     [COPY_TO output]
#                    )
#
# ``target_name``:
#   Required, expects the name of the file to receive the generated symbol
#   version script. It should be added as a dependency for the library. Use the
#   linker option ``--version-script filename`` to add the version information
#   to the symbols when building the library.
#
# ``RELEASE_NAME_VERSION``:
#   Required, expects a string containing the name and version information to be
#   added to the symbols in the format ``lib_name_1_2_3``.
#
# ``SYMBOLS``:
#   Required, expects a target with the property ``LIST_FILE`` containing a path
#   to a file containing the list of symbols to be added to the symbol version
#   script.
#
# ``CURRENT_MAP``:
#   Optional. If given, the new set of symbols will be checked against the
#   ones contained in the ``cur_map`` file and updated properly. If an
#   incompatible change is detected and ``BREAK_ABI`` is not defined, the build
#   will fail.
#
# ``FINAL``:
#   Optional. If given, will provide the ``--final`` option to ``abimap`` tool,
#   which will mark the modified release in the symbol version script with a
#   special comment, preventing later changes. This option should be set when
#   creating a library release and the resulting map file should be stored with
#   the source code.
#
# ``BREAK_ABI``:
#   Optional. If provided, will use ``abimap`` ``--allow-abi-break`` option, which
#   accepts incompatible changes to the set of symbols. This is necessary if any
#   previously existing symbol were removed.
#
# ``COPY_TO``:
#   Optional, expects a string containing the path to where the generated
#   map file will be copied.
#
# Example:
#
# .. code-block:: cmake
#
#   find_package(ABIMap)
#   generate_map_file("lib.map"
#                     RELEASE_NAME_VERSION "lib_1_0_0"
#                     SYMBOLS symbols
#                    )
#
# Where the target ``symbols`` has its property ``LIST_FILE`` set to the path to
# a file containing::
#
#  ``symbol1``
#  ``symbol2``
#
# This example would result in the symbol version script to be created in
# ``${CMAKE_CURRENT_BINARY_DIR}/lib.map`` containing the provided symbols.
#
# ::
#
#   get_files_list(target_name
#                   DIRECTORIES dir1 [dir2 ...]
#                   FILES_PATTERNS exp1 [exp2 ...]
#                   [COPY_TO output]
#                  )
#
# ``target_name``:
#   Required, expects the name of the target to be created. A file named as
#   ``${target_name}.list`` will be created in
#   ``${CMAKE_CURRENT_BINARY_DIR}`` to receive the list of files found.
#
# ``DIRECTORIES``:
#   Required, expects a list of directories paths. Only absolute paths are
#   supported.
#
# ``FILES_PATTERN``:
#   Required, expects a list of matching expressions to find the files to be
#   considered in the directories.
#
# ``COPY_TO``:
#   Optional, expects a string containing the path to where the file containing
#   the list of files will be copied.
#
# This command searches the directories provided in ``DIRECTORIES`` for files
# matching any of the patterns provided in ``FILES_PATTERNS``. The obtained list
# is written to the path specified by ``output``. A target named ``target_name``
# will be created and its property ``LIST_FILE`` will be set to contain
# ``${CMAKE_CURRENT_BINARY_DIR}/${target_name}.list``
#
# Example:
#
# .. code-block:: cmake
#
#   find_package(ABIMap)
#   get_files_list(target
#     DIRECTORIES "/include/mylib"
#     FILES_PATTERNS "*.h"
#     COPY_TO "my_list.txt"
#   )
#
# Consider that ``/include/mylib`` contains 3 files, ``h1.h``, ``h2.h``, and
# ``h3.hpp``
#
# Will result in a file ``my_list.txt`` containing::
#
#   ``h1.h;h2.h``
#
# And the target ``target`` will have its property ``LIST_FILE`` set to contain
# ``${CMAKE_CURRENT_BINARY_DIR}/target.list``
#
# ::
#
#   extract_symbols(target_name
#                   HEADERS_LIST headers_list_target
#                   [FILTER_PATTERN pattern]
#                   [COPY_TO output]
#                  )
#
# ``target_name``:
#   Required, expects the name of the target to be created. A file named after
#   the string given in ``target_name`` will be created in
#   ``${CMAKE_CURRENT_BINARY_DIR}`` to receive the list of symbols.
#
# ``HEADERS_LIST``:
#   Required, expects a target with the property ``LIST_FILE`` set, containing a
#   file path. Such file must contain a list of files paths.
#
# ``FILTER_PATTERN``:
#   Optional, expects a string. Only the lines containing the filter pattern
#   will be considered.
#
# ``COPY_TO``:
#   Optional, expects a string containing the path to where the file containing
#   the found symbols will be copied.
#
# This command extracts the symbols from the files listed in
# ``headers_list`` and write them on the ``output`` file. If ``pattern``
# is provided, then only the lines containing the string given in ``pattern``
# will be considered. It is recommended to provide a ``FILTER_PATTERN`` to mark
# the lines containing exported function declaration, since this function is
# experimental and can return wrong symbols when parsing the header files. A
# target named ``target_name`` will be created with the property ``LIST_FILE``
# set to contain ``${CMAKE_CURRENT_BINARY_DIR}/${target_name}.list``.
#
# Example:
#
# .. code-block:: cmake
#
#   find_package(ABIMap)
#   extract_symbols("lib.symbols"
#     HEADERS_LIST "headers_target"
#     FILTER_PATTERN "API_FUNCTION"
#   )
#
# Where ``LIST_FILE`` property in ``headers_target`` points to a file
# containing::
#
#   header1.h;header2.h
#
# Where ``header1.h`` contains::
#
#   API_FUNCTION int exported_func1(int a, int b);
#
# ``header2.h`` contains::
#
#   API_FUNCTION int exported_func2(int a);
#
#   int private_func2(int b);
#
# Will result in a file ``lib.symbols.list`` in ``${CMAKE_CURRENT_BINARY_DIR}``
# containing::
#
#   ``exported_func1``
#   ``exported_func2``
#

# Search for python which is required
if (ABIMap_FIND_REQURIED)
    find_package(PythonInterp REQUIRED)
else()
    find_package(PythonInterp)
endif()


if (PYTHONINTERP_FOUND)
    # Search for abimap tool used to generate the map files
    find_program(ABIMAP_EXECUTABLE NAMES abimap DOC "path to the abimap executable")
    mark_as_advanced(ABIMAP_EXECUTABLE)

    if (NOT ABIMAP_EXECUTABLE AND UNIX)
        message(STATUS "Could not find `abimap` in PATH."
                       " It can be found in PyPI as `abimap`"
                       " (try `pip install abimap`)")
    endif ()

    if (ABIMAP_EXECUTABLE)
        # Get the abimap version
        execute_process(COMMAND ${ABIMAP_EXECUTABLE} version
                        OUTPUT_VARIABLE ABIMAP_VERSION_STRING
                        OUTPUT_STRIP_TRAILING_WHITESPACE)

        # If the version string starts with abimap-, strip it
        if ("abimap" STRLESS_EQUAL ${ABIMAP_VERSION_STRING})
            string(REGEX REPLACE "abimap-" "" ABIMAP_VERSION_STRING "${ABIMAP_VERSION_STRING}")
        endif()
    endif()

    include(FindPackageHandleStandardArgs)
    find_package_handle_standard_args(ABIMap
                                      REQUIRED_VARS ABIMAP_EXECUTABLE
                                      VERSION_VAR ABIMAP_VERSION_STRING)
endif()


if (ABIMAP_FOUND)

# Define helper scripts
set(_EXTRACT_SYMBOLS_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/ExtractSymbols.cmake)
set(_GENERATE_MAP_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/GenerateMap.cmake)
set(_GET_FILES_LIST_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/GetFilesList.cmake)

function(get_file_list _TARGET_NAME)

    set(one_value_arguments
        COPY_TO
    )

    set(multi_value_arguments
        DIRECTORIES
        FILES_PATTERNS
    )

    cmake_parse_arguments(_get_files_list
        ""
        "${one_value_arguments}"
        "${multi_value_arguments}"
        ${ARGN}
    )

    # The DIRS argument is required
    if (NOT DEFINED _get_files_list_DIRECTORIES)
        message(FATAL_ERROR "No directories paths provided. Provide a list of"
                            " directories paths containing header files.")
    endif()

    # The FILES_PATTERNS argument is required
    if (NOT DEFINED _get_files_list_FILES_PATTERNS)
        message(FATAL_ERROR "No matching expressions provided. Provide a list"
                            " of matching patterns for the header files.")
    endif()

    set(_FILES_LIST_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}/${_TARGET_NAME}.list)

    get_filename_component(_get_files_list_OUTPUT_PATH
                           "${_FILES_LIST_OUTPUT_PATH}"
                           ABSOLUTE)

    add_custom_target(
        ${_TARGET_NAME}_int ALL
        COMMAND ${CMAKE_COMMAND}
          -DOUTPUT_PATH=${_get_files_list_OUTPUT_PATH}
          -DDIRECTORIES=${_get_files_list_DIRECTORIES}
          -DFILES_PATTERNS=${_get_files_list_FILES_PATTERNS}
          -P ${_GET_FILES_LIST_SCRIPT}
        COMMENT
          "Searching for files"
        VERBATIM
    )

    if (DEFINED _get_files_list_COPY_TO)
        # Copy the generated file back to the COPY_TO
        add_custom_target(${_TARGET_NAME} ALL
            COMMAND
              ${CMAKE_COMMAND} -E copy_if_different
              ${_FILES_LIST_OUTPUT_PATH} ${_get_files_list_COPY_TO}
            DEPENDS ${_TARGET_NAME}_int
            COMMENT "Copying ${_TARGET_NAME} to ${_get_files_list_COPY_TO}"
            VERBATIM
        )
    else()
        add_custom_target(${_TARGET_NAME} ALL
            DEPENDS ${_TARGET_NAME}_int
        )
    endif()

    set_target_properties(${_TARGET_NAME}
        PROPERTIES LIST_FILE ${_FILES_LIST_OUTPUT_PATH}
    )

endfunction()

function(extract_symbols _TARGET_NAME)

    set(one_value_arguments
        FILTER_PATTERN
        HEADERS_LIST
        COPY_TO
    )

    set(multi_value_arguments
    )

    cmake_parse_arguments(_extract_symbols
        ""
        "${one_value_arguments}"
        "${multi_value_arguments}"
        ${ARGN}
    )

    # The HEADERS_LIST_FILE argument is required
    if (NOT DEFINED _extract_symbols_HEADERS_LIST)
        message(FATAL_ERROR "No target provided in HEADERS_LIST. Provide a"
                            " target with the property LIST_FILE set as the"
                            " path to the file containing the list of headers.")
    endif()

    get_filename_component(_SYMBOLS_OUTPUT_PATH
                           "${CMAKE_CURRENT_BINARY_DIR}/${_TARGET_NAME}.list"
                           ABSOLUTE
    )

    get_target_property(_HEADERS_LIST_FILE
        ${_extract_symbols_HEADERS_LIST}
        LIST_FILE
    )

    add_custom_target(
        ${_TARGET_NAME}_int ALL
        COMMAND ${CMAKE_COMMAND}
          -DOUTPUT_PATH=${_SYMBOLS_OUTPUT_PATH}
          -DHEADERS_LIST_FILE=${_HEADERS_LIST_FILE}
          -DFILTER_PATTERN=${_extract_symbols_FILTER_PATTERN}
          -P ${_EXTRACT_SYMBOLS_SCRIPT}
        DEPENDS ${_extract_symbols_HEADERS_LIST}
        COMMENT "Extracting symbols from headers"
        VERBATIM
    )

    if (DEFINED _extract_symbols_COPY_TO)
        # Copy the generated file back to the COPY_TO
        add_custom_target(${_TARGET_NAME} ALL
            COMMAND
              ${CMAKE_COMMAND} -E copy_if_different
              ${_SYMBOLS_OUTPUT_PATH} ${_extract_symbols_COPY_TO}
            DEPENDS ${_TARGET_NAME}_int
            COMMENT "Copying ${_TARGET_NAME} to ${_extract_symbols_COPY_TO}"
            VERBATIM
        )
    else()
        add_custom_target(${_TARGET_NAME} ALL
            DEPENDS ${_TARGET_NAME}_int
        )
    endif()

    set_target_properties(${_TARGET_NAME}
        PROPERTIES LIST_FILE ${_SYMBOLS_OUTPUT_PATH}
    )

endfunction()

function(generate_map_file _TARGET_NAME)

    set(options
        FINAL
        BREAK_ABI
    )

    set(one_value_arguments
        RELEASE_NAME_VERSION
        SYMBOLS
        CURRENT_MAP
        COPY_TO
    )

    set(multi_value_arguments
    )

    cmake_parse_arguments(_generate_map_file
        "${options}"
        "${one_value_arguments}"
        "${multi_value_arguments}"
        ${ARGN}
    )

    if (NOT DEFINED _generate_map_file_SYMBOLS)
        message(FATAL_ERROR "No target provided in SYMBOLS. Provide a target"
                            " with the property LIST_FILE set as the path to"
                            " the file containing the list of symbols.")
    endif()

    if (NOT DEFINED _generate_map_file_RELEASE_NAME_VERSION)
        message(FATAL_ERROR "Release name and version not provided."
          " (e.g. libname_1_0_0)")
    endif()


    get_target_property(_SYMBOLS_FILE
        ${_generate_map_file_SYMBOLS}
        LIST_FILE
    )

    # Set generated map file path
    get_filename_component(_MAP_OUTPUT_PATH
                           "${CMAKE_CURRENT_BINARY_DIR}/${_TARGET_NAME}"
                           ABSOLUTE
    )

    add_custom_target(
        ${_TARGET_NAME}_int ALL
        COMMAND ${CMAKE_COMMAND}
          -DABIMAP_EXECUTABLE=${ABIMAP_EXECUTABLE}
          -DSYMBOLS=${_SYMBOLS_FILE}
          -DCURRENT_MAP=${_generate_map_file_CURRENT_MAP}
          -DOUTPUT_PATH=${_MAP_OUTPUT_PATH}
          -DFINAL=${_generate_map_file_FINAL}
          -DBREAK_ABI=${_generate_map_file_BREAK_ABI}
          -DRELEASE_NAME_VERSION=${_generate_map_file_RELEASE_NAME_VERSION}
          -P ${_GENERATE_MAP_SCRIPT}
        DEPENDS ${_generate_map_file_SYMBOLS}
        COMMENT "Generating the map ${_TARGET_NAME}"
        VERBATIM
    )

    # Add a custom command setting the map as OUTPUT to allow it to be added as
    # a generated source
    add_custom_command(
        OUTPUT ${_MAP_OUTPUT_PATH}
        DEPENDS ${_TARGET_NAME}_copy
    )

    if (DEFINED _generate_map_file_COPY_TO)
        # Copy the generated map back to the COPY_TO
        add_custom_target(${_TARGET_NAME}_copy ALL
            COMMAND
              ${CMAKE_COMMAND} -E copy_if_different ${_MAP_OUTPUT_PATH}
              ${_generate_map_file_COPY_TO}
            DEPENDS ${_TARGET_NAME}_int
            COMMENT "Copying ${_MAP_OUTPUT_PATH} to ${_generate_map_file_COPY_TO}"
            VERBATIM
        )
    else()
        add_custom_target(${_TARGET_NAME}_copy ALL
            DEPENDS ${_TARGET_NAME}_int
        )
    endif()
endfunction()

endif (ABIMAP_FOUND)
