#[=======================================================================[.rst:
InstallPackage
--------------

.. _CPM: https://github.com/cpm-cmake/CPM.cmake

Provides a set of convenience functions for preparing a cmake project for
:guide:`importing-exporting`, :command:`install`, and :manual:`cpack(1)` with
sane, community-santioned defaults.

Cache Variables
^^^^^^^^^^^^^^^
.. variable:: ${PROJECT_NAME}_INSTALL

  Option to enable installing the current project. Defaults to ``ON`` if the
  current project is the top level project, ``OFF`` otherwise. Created upon
  including ``InstallPackage.cmake``.

.. variable:: ${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR

  Provides OS packaging authorities to override where this project's CONFIG
  cmake modules will be installed to. Defaults to ``${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}``.
  Typically, this will expand to ``/lib/cmake/${PROJECT_NAME}``. If you are not
  familiar with what this is or why it should change, you probably do not need
  to touch it.

Example Usages:

.. code-block:: cmake

  project(Sensor)
  include(InstallProject)
  add_library(Sensor sensor.c)
  target_include_directories(Sensor
    PUBLIC
      $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}
  )
  target_install_package(Sensor)
  project_install_package(Sensor)

Functions
^^^^^^^^^
#]=======================================================================]
# Set script compatibility
cmake_minimum_required(VERSION 3.13)

# Include required standard cmake modules
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)

# Inspect CMAKE_CURRENT_LIST_DIR at file scope; use within a function will not
# return the correct value
set(installProjectCMakeDir "${CMAKE_CURRENT_LIST_DIR}")

# Check if InstallPackage is being used from the top level project
set(isTopProject OFF)
if (CMAKE_PROJECT_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
  set(isTopProject ON)
endif()

# Create Cache Variables (see the above docstring for more info).
option(${PROJECT_NAME}_INSTALL "Install ${PROJECT_NAME}" ${isTopProject})
set(${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR
  "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}"
  CACHE PATH
  "Install prefix relative directory where ${PROJECT_NAME}Config.cmake installs to"
)

#[=======================================================================[.rst:
.. command:: find_package_register

  Makes the current project available to other projects in the same source tree
  via :command:`find_package`. This is done by generating a simple
  ``Find<ModuleName>.cmake`` file in :variable:`CMAKE_BINARY_DIR` ``/cmake/<ModuleName>``
  and inserting this directory to the start of :variable:`CMAKE_MODULE_PATH`,
  thereby ensuring precedence over installed packages. ``<ModuleName>_VERSION``
  must be defined before calling this function.

  :command:`find_package` is the universal means of finding libraries and
  validating their versions in cmake. Package managers and system packages are
  both expected to transparently use this mechanism. This function allows
  your project to also support this mechanism if included via :command:`add_subdirectory`.
  This will even allow some third party libraries and even tools such as CPM_
  to find the module via :command:`find_package`.

  Targets generated by your project should be aliased with the same names as
  their installed counterparts. This is also considered best practice. For
  instance, if your project is named ``Busses`` and your library is named
  ``Spi``, ``add_library(Busses::Spi ALIAS Spi)`` should be called after
  creating your library. :command:`target_install_package` (also provided by
  this module) will automatically alias your targets correctly.

  :command:`project_install_package` (also provided by this module) calls this
  function. If you are making an installable project, manually calling this
  function is unnecessary. If you do not support cmake install, then calling
  this function is highly recommended.

  Signatures::

    find_package_register(
      <module-name>
    )

  The options are:

  ``<module-name>``
  The name of the find module to generate.

  Example usage:

  .. code-block:: cmake

  # In a project:
  project(Busses VERSION 2.3)
  add_library(Uart uart.c uart.h)
  target_include_directories(Uart
    PUBLIC ${CMAKE_CURRENT_LIST_DIR}
  )
  add_library(Busses::Uart ALIAS Uart)
  # Busses::Uart can now be found via find_package
  find_package_register(Busses)

  # Meanwhile, in another project:
  project(Drivers)
  # Version checked correctly
  find_package(Busses 2.2 REQUIRED)
  add_library(Sensor sensor.c sensor.h)
  target_include_directories(Sensor
    PUBLIC ${CMAKE_CURRENT_LIST_DIR}
  )
  target_link_libraries(Sensor PUBLIC Busses::UART)

  # These could both be added via add_subdirectory, Busses could be installed as
  # a system package, or could even be provided by a package manager.

#]=======================================================================]
function(find_package_register MODULE_NAME)
  set(generatedModuleDir ${CMAKE_BINARY_DIR}/cmake/${MODULE_NAME})
  configure_file(
    ${installProjectCMakeDir}/FindModule.in.cmake
    ${generatedModuleDir}/Find${MODULE_NAME}.cmake
    @ONLY
  )
  set(CMAKE_MODULE_PATH ${generatedModuleDir} ${CMAKE_MODULE_PATH} PARENT_SCOPE)
endfunction()

#[=======================================================================[.rst:
.. command:: target_install_package

  Prepares a target for install if :prop_cache:`${PROJECT_NAME}_INSTALL` is
  ``ON``.

  Unconditionally adds an ``ALIAS`` target is generated in the form of
  ``${PROJECT_NAME}::<target>``. If the name of ``<target>`` started with
  ``${PROJECT_NAME}.``, said prefix is stripped before making the alias. Adding
  such a prefix is common practice to avoid name collisions. For example,
  ``Busses.Uart`` will be aliased to ``Busses::Uart`` rather than
  ``Busses::Busses.Uart``.

  Signatures::

    target_install_package(
      <target>
      [CLEANUP_INCLUDE_DIRS]
    )

  The options are:

  ``<target>``
  Target to prepare for install.

  ``[CLEANUP_INCLUDE_DIRS]``
  If present, wraps all of a target's
  :prop_tgt:`INTERFACE_INCLUDE_DIRECTORIES` with ``$<BUILD_INTERFACE:>``. This
  is intended as a migration assistant for old projects that do not properly
  wrap their include directories with build interface.

  Example usage:

  .. code-block:: cmake

  project(Busses VERSION 2.3)
  add_library(Busses.Uart uart.c uart.h)
  target_include_directories(Busses.Uart
    PUBLIC
      $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>
  )
  # Creates ALIAS target Busses::Uart, and prepares the target for install
  target_install_package(Busses.Uart)
#]=======================================================================]
function(target_install_package TARGET)
  set(optArgs CLEANUP_INCLUDE_DIRS)
  set(oneValueArgs "")
  set(multiValueArgs "")
  cmake_parse_arguments(PARSE_ARGV 1 arg "${optArgs}" "${oneValueArgs}" "${multiValueArgs}")

  # Turn an add_subdirectory friendly target in the form of "foo.sometarget" (where foo is PROJECT_NAME) into the bare name "sometarget"
  string(REGEX REPLACE "^${PROJECT_NAME}\\." "" targetWithoutNamespace "${TARGET}")
  # Add an alias that looks identical to installed targets so add_subdirectory and find_package behave identically
  add_library(${PROJECT_NAME}::${targetWithoutNamespace} ALIAS ${TARGET})

  # If the user does not want to install this package, stop here.
  if (NOT ${PROJECT_NAME}_INSTALL)
    return()
  endif()

  # Give the user a help message with fix steps if they failed to specify the include interface specification
  get_target_property(interfaceIncludes ${TARGET} INTERFACE_INCLUDE_DIRECTORIES)
  set(buildInterfaceIncludes "")
  foreach(indir IN LISTS interfaceIncludes)
    list(APPEND buildInterfaceIncludes "$<BUILD_INTERFACE:${indir}>")
    if (NOT arg_CLEANUP_INCLUDE_DIRS)
      string(REGEX MATCH "(BUILD_INTERFACE|INSTALL_INTERFACE)" hasInterfaceSpec ${indir})
      if (NOT hasInterfaceSpec)
        message(WARNING "\
 Attempting to install \"${TARGET}\" with INTERFACE include_directory \"${indir}\" \n\
 This will fail at install time. Wrap your include_directory with \n\
 $<BUILD_INTERFACE:${indir}> (or if you really meant relative to install, use\n\
 $<INSTALL_INTERFACE:${indir}> with an install-relative path)")
      endif()
    endif()
  endforeach()

  # Add $<BUILD_INTERFACE:> to all if the user opted in
  if (arg_CLEANUP_INCLUDE_DIRS)
    set_target_properties(${TARGET}
      PROPERTIES
        INTERFACE_INCLUDE_DIRECTORIES ${buildInterfaceIncludes}
    )
  endif()

  set_target_properties(${TARGET} PROPERTIES
    # If a target is named "foo.sometarget" (where foo is PROJECT_NAME), change the install name to "foo::sometarget".
    # If EXPORT_NAME was not specified, we would instead export "foo::foo.sometarget".
    EXPORT_NAME ${targetWithoutNamespace}
    # Set shared object version suffixes
    VERSION     ${PROJECT_VERSION}
    SOVERSION   ${PROJECT_VERSION_MAJOR}
  )
  install(
    TARGETS
      ${TARGET}
    EXPORT
      ${PROJECT_NAME}
    RUNTIME
      DESTINATION ${CMAKE_INSTALL_BINDIR}
      COMPONENT   ${PROJECT_NAME}_Runtime
    LIBRARY
      DESTINATION ${CMAKE_INSTALL_LIBDIR}
      COMPONENT   ${PROJECT_NAME}_Runtime
      NAMELINK_COMPONENT ${PROJECT_NAME}_Development
    ARCHIVE
      DESTINATION ${CMAKE_INSTALL_LIBDIR}
      COMPONENT   ${PROJECT_NAME}_Development
    INCLUDES
      DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
  )
endfunction()

#[=======================================================================[.rst:
.. command:: project_install_package

  Unconditionally makes the current project available to other projects in the
  same source tree via :command:`find_package` a la :command:`find_package_register`.

  Prepares the project for install and packing if :prop_cache:`${PROJECT_NAME}_INSTALL`
  is ``ON``. This should be the **last** line of your project. This creates
  relocatable find modules:

  * ``<ProjectName>Config.cmake``
  * ``<ProjectName>ConfigVersion.cmake``
  * ``<ProjectName>Targets.cmake``
  * ``<ProjectName>Targets-<Config>.cmake``

  with path ``${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR`` in both the :variable:`PROJECT_BINARY_DIR`
  upon build and the install director upon install.

  This build path should not be added to :variable:`CMAKE_MODULE_PATH`. The
  ``IMPORTED`` targets will name clash with the existing targets in the source
  tree CMakeLists.

  Include files for install are presumed to reside in either ``${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}``
  or ``${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}`` (which typically
  expands to ``${PROJECT_BINARY_DIR}/include``)

  Signatures::

    project_install_package()

  Example usage:

  .. code-block:: cmake

  project(Busses VERSION 2.3)
  add_library(Busses.Uart uart.c uart.h)
  target_include_directories(Busses.Uart
    PUBLIC
      $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>
  )
  target_install_package(Busses.Uart)
  # And we're ready for install!
  project_install_package()
#]=======================================================================]
function(project_install_package)
  # Make this project vailable via find_project if we were added via add_subdirectory
  find_package_register(${PROJECT_NAME})

  # If the user does not want to install this package, stop here.
  if (NOT ${PROJECT_NAME}_INSTALL)
    return()
  endif()

  # Install package version info
  write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    COMPATIBILITY SameMinorVersion
  )
  install(
    FILES       ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    DESTINATION ${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}
  )

  # Install target info
  install(EXPORT ${PROJECT_NAME}
    DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}
    FILE        ${PROJECT_NAME}Targets.cmake
    NAMESPACE   ${PROJECT_NAME}::
  )

  # Install full project info
  configure_package_config_file(
    ${installProjectCMakeDir}/BaseProjectConfig.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}Config.cmake
    INSTALL_DESTINATION ${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}
    NO_SET_AND_CHECK_MACRO
    NO_CHECK_REQUIRED_COMPONENTS_MACRO
  )
  install(
    FILES
      ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}Config.cmake
      ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}ConfigVersion.cmake
      ${CMAKE_CURRENT_BINARY_DIR}/${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}/${PROJECT_NAME}Targets.cmake
    DESTINATION
      ${${PROJECT_NAME}_INSTALL_CMAKE_CONFIG_MODULE_DIR}
  )

  # Install include directories
  install(
    DIRECTORY   ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    COMPONENT   ${CMAKE_PROJECT_NAME}_Development
  )
  install(
    DIRECTORY   ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_INCLUDEDIR}
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
    COMPONENT   ${CMAKE_PROJECT_NAME}_Development
  )

  # TODO: CPack
  # TODO: support injecting dependencies in BaseProjectConfig.cmake.in
endfunction()
