# This file is part of the mlhpbf project. License: See LICENSE

# 3.19 for COMMAND_ERROR_IS_FATAL and Development.Module, used by the installed mode below
cmake_minimum_required( VERSION 3.19 )

project( mlhpbf LANGUAGES CXX )

# Find out if this is the root CMake project or not
get_directory_property( hasParent PARENT_DIRECTORY )
string( COMPARE EQUAL "${hasParent}" "" PBF_IS_ROOT )

# Options
option( PBF_TESTS "Enable verification tests." ${PBF_IS_ROOT} )
option( PBF_PYTHON "Enable python bindings." ON )

set( PBF_MLHP_SOURCE submodule CACHE STRING "Compile mlhp from the submodule or link a pip installed one." )
set_property( CACHE PBF_MLHP_SOURCE PROPERTY STRINGS submodule installed )

# Defines CMAKE_INSTALL_BINDIR used below; in installed mode mlhp does not pull it in
include( GNUInstallDirs )

# The released version lives in the git tag, not in the tree: scikit-build-core passes
# through what setuptools-scm derived, a plain build asks git directly.
if( DEFINED SKBUILD_PROJECT_VERSION )
    set( PBF_VERSION_STRING ${SKBUILD_PROJECT_VERSION} )
else( )
    execute_process( COMMAND git describe --tags --abbrev=0
                     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                     OUTPUT_VARIABLE PBF_VERSION_STRING
                     OUTPUT_STRIP_TRAILING_WHITESPACE
                     ERROR_QUIET )
endif( )

if( NOT PBF_VERSION_STRING )
    message( WARNING "Found no version tag, falling back to 0.0.0." )
    set( PBF_VERSION_STRING 0.0.0 )
endif( )

# Numeric part only, which is what the installed package version file can compare
if( PBF_VERSION_STRING MATCHES "^([0-9]+(\\.[0-9]+)*)" )
    set( PBF_VERSION ${CMAKE_MATCH_1} )
else( )
    set( PBF_VERSION 0.0.0 )
endif( )

# -------------------- External dependencies ----------------------

if( PBF_MLHP_SOURCE STREQUAL "submodule" )

    # Configure mlhp. Index sizes are part of the bound signatures, so they must match
    # the mlhp release pymlhpbf is paired with.
    set( MLHP_PYTHON ${PBF_PYTHON} CACHE BOOL "" FORCE )
    set( MLHP_DIMENSIONS 3 CACHE STRING "" )
    set( MLHP_DEBUG_CHECKS OFF CACHE BOOL "" )
    set( MLHP_INDEX_SIZE_CELLS "64" CACHE STRING "" )
    set( MLHP_INDEX_SIZE_DOFS "64" CACHE STRING "" )

    add_subdirectory( external/mlhp )

    # Subdirectory scope, so mlhp's own version variables do not reach here. Same
    # derivation, so the value matches what mlhp compiled into its config.
    if( DEFINED SKBUILD_PROJECT_VERSION )
        set( PBF_MLHP_VERSION ${SKBUILD_PROJECT_VERSION} )
    else( )
        execute_process( COMMAND git describe --tags --abbrev=0
                         WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/external/mlhp
                         OUTPUT_VARIABLE PBF_MLHP_VERSION
                         OUTPUT_STRIP_TRAILING_WHITESPACE
                         ERROR_QUIET )
    endif( )

    set( PBF_MLHP_INDEX_SIZE_CELLS ${MLHP_INDEX_SIZE_CELLS} )
    set( PBF_MLHP_INDEX_SIZE_DOFS ${MLHP_INDEX_SIZE_DOFS} )
    set( PBF_BUILD_BINARY_DIR ${MLHP_BUILD_BINARY_DIR} )

elseif( PBF_MLHP_SOURCE STREQUAL "installed" )

    if( PBF_TESTS )
        message( FATAL_ERROR "PBF_TESTS needs PBF_MLHP_SOURCE=submodule: the test runner takes "
                             "catch2 from the submodule, and an installed mlhp is compiled "
                             "without debug checks, which is a different ABI." )
    endif( )

    # Both components: pybind11 finds Python again below and a cached interpreter-only
    # result leaves python_add_library undefined.
    find_package( Python COMPONENTS Interpreter Development.Module REQUIRED )

    # Without this pybind11 uses its classic finder, which picks whichever interpreter comes
    # first on PATH rather than the one given in Python_EXECUTABLE. Building against another
    # python than mlhp was built for fails only later, when both modules are imported.
    if( NOT DEFINED PYBIND11_FINDPYTHON )
        set( PYBIND11_FINDPYTHON NEW )
    endif( )

    # Both ship their cmake package inside the python distribution
    foreach( package mlhp pybind11 )
        if( NOT DEFINED ${package}_DIR )
            execute_process( COMMAND ${Python_EXECUTABLE} -c
                             "import ${package}; print(${package}.get_cmake_dir())"
                             OUTPUT_VARIABLE ${package}_DIR
                             OUTPUT_STRIP_TRAILING_WHITESPACE
                             COMMAND_ERROR_IS_FATAL ANY )
        endif( )
    endforeach( )

    find_package( mlhp CONFIG REQUIRED )
    find_package( pybind11 CONFIG REQUIRED )

    # Stricter than the internals version that governs compatibility, so that a mismatch
    # surfaces here rather than as an unresolved type when both modules are imported.
    if( mlhp_PYBIND11_VERSION AND NOT pybind11_VERSION VERSION_EQUAL mlhp_PYBIND11_VERSION )
        message( FATAL_ERROR "Found pybind11 ${pybind11_VERSION}, but this mlhp was built "
                             "against ${mlhp_PYBIND11_VERSION}. Install the matching one with "
                             "\"pip install pybind11==${mlhp_PYBIND11_VERSION}\"." )
    endif( )

    # Index sizes come from the installed config.hpp, so they are read back rather than set.
    # What is recorded is the mlhp this build compiled against, which the python package
    # compares against the mlhp actually imported.
    execute_process( COMMAND ${Python_EXECUTABLE} -c
                     "import mlhp; c = mlhp.config; print(c.version, c.cellIndexSize, c.dofIndexSize)"
                     OUTPUT_VARIABLE PBF_MLHP_CONFIG
                     OUTPUT_STRIP_TRAILING_WHITESPACE
                     COMMAND_ERROR_IS_FATAL ANY )

    separate_arguments( PBF_MLHP_CONFIG )
    list( GET PBF_MLHP_CONFIG 0 PBF_MLHP_VERSION )
    list( GET PBF_MLHP_CONFIG 1 PBF_MLHP_INDEX_SIZE_CELLS )
    list( GET PBF_MLHP_CONFIG 2 PBF_MLHP_INDEX_SIZE_DOFS )

    set( PBF_BUILD_BINARY_DIR ${CMAKE_BINARY_DIR}/bin )

else( )
    message( FATAL_ERROR "PBF_MLHP_SOURCE is \"${PBF_MLHP_SOURCE}\", expected submodule or installed." )
endif( )

if( NOT PBF_MLHP_VERSION )
    set( PBF_MLHP_VERSION 0.0.0 )
endif( )

# ------------------------- PBF library ---------------------------

set( MLHPBF_HEADERS laser.hpp materials.hpp meltstate.hpp thermal.hpp mechanical.hpp )
list( TRANSFORM MLHPBF_HEADERS PREPEND ${CMAKE_CURRENT_SOURCE_DIR}/include/mlhp/pbf/ )
list( APPEND MLHPBF_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/include/mlhp/pbf.hpp )

add_library( mlhpbf INTERFACE )

# Absolute paths hold only while building; the installed interface gets its include
# directory from the INCLUDES DESTINATION of install( TARGETS ) below.
target_sources( mlhpbf INTERFACE "$<BUILD_INTERFACE:${MLHPBF_HEADERS}>" )
target_include_directories( mlhpbf INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> )
target_link_libraries( mlhpbf INTERFACE mlhp::core )

set_target_properties( mlhpbf PROPERTIES EXPORT_NAME pbf )

add_library( mlhp::pbf ALIAS mlhpbf )

file( COPY materials DESTINATION ${CMAKE_BINARY_DIR} )

if ( CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT )
    set( CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install" CACHE PATH "default install path" FORCE )
endif( )

# A wheel keeps everything inside the python package, a cmake install follows GNUInstallDirs
if( SKBUILD )
    set( PBF_INSTALL_PACKAGEDIR pbf )
    set( PBF_INSTALL_INCLUDEDIR pbf/include )
    set( PBF_INSTALL_CMAKEDIR   pbf/cmake )
else( SKBUILD )
    set( PBF_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_BINDIR}/pbf )
    set( PBF_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} )
    set( PBF_INSTALL_CMAKEDIR   ${CMAKE_INSTALL_LIBDIR}/cmake/pbf )
endif( SKBUILD )

install( TARGETS mlhpbf EXPORT pbf-targets INCLUDES DESTINATION ${PBF_INSTALL_INCLUDEDIR} )

install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/mlhp DESTINATION ${PBF_INSTALL_INCLUDEDIR} )

install( EXPORT pbf-targets
         FILE pbf-targets.cmake
         NAMESPACE mlhp::
         DESTINATION ${PBF_INSTALL_CMAKEDIR} )

include( CMakePackageConfigHelpers )

configure_package_config_file( tools/cmake/pbfConfig.cmake.in
                               ${CMAKE_CURRENT_BINARY_DIR}/pbfConfig.cmake
                               INSTALL_DESTINATION ${PBF_INSTALL_CMAKEDIR} )

write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/pbfConfigVersion.cmake
                                  VERSION ${PBF_VERSION}
                                  COMPATIBILITY SameMinorVersion )

install( FILES ${CMAKE_CURRENT_BINARY_DIR}/pbfConfig.cmake
               ${CMAKE_CURRENT_BINARY_DIR}/pbfConfigVersion.cmake
         DESTINATION ${PBF_INSTALL_CMAKEDIR} )

# ----------------------- Python bindings -------------------------

if( ${PBF_PYTHON} )

    include( src/files.cmake )
 
    list( TRANSFORM PBF_PYTHON_BINDING_SOURCES PREPEND src/ )
    list( TRANSFORM PBF_PYTHON_PACKAGE_SOURCES PREPEND src/pbf/ )

    configure_file( src/config.hpp.in include/mlhp/pbf/config.hpp )

    pybind11_add_module( pymlhpbf ${PBF_PYTHON_BINDING_SOURCES} )

    target_link_libraries( pymlhpbf PRIVATE mlhp::pbf )

    target_include_directories( pymlhpbf PRIVATE . ${CMAKE_CURRENT_BINARY_DIR}/include )

    target_include_directories( pymlhpbf SYSTEM PRIVATE external )

    # Imported as pbf._core, so the module file is named _core. Its output directory is
    # left at the default: mlhp's module is also named _core and would collide in the
    # shared binary directory. The package below is assembled by a post-build copy.
    set_target_properties( pymlhpbf PROPERTIES OUTPUT_NAME _core )

    if( PBF_MLHP_SOURCE STREQUAL "submodule" )

        target_link_libraries( pymlhpbf PRIVATE mlhp_private_compile_flags )

        # Whenever we build pymlhpbf we want to build pymlhpcore too
        add_dependencies( pymlhpbf pymlhpcore )

    else( )

        # The mlhp core library sits in the mlhp package, which installs beside pbf's.
        # Windows needs no rpath: importing mlhp loads the library before _core is loaded.
        # On macOS the same entry resolves the OpenMP runtime, which the mlhp package
        # installs next to the core library with the install name @rpath/libomp.dylib.
        if( APPLE )
            set_target_properties( pymlhpbf PROPERTIES INSTALL_RPATH "@loader_path/../mlhp" )
        elseif( UNIX )
            set_target_properties( pymlhpbf PROPERTIES INSTALL_RPATH "$ORIGIN/../mlhp" )
        endif( )

    endif( )

    set( PBF_PACKAGE_DIR ${PBF_BUILD_BINARY_DIR}/pbf )

    # Copy python sources
    file( COPY ${PBF_PYTHON_PACKAGE_SOURCES} DESTINATION ${PBF_PACKAGE_DIR} )

    # The binding source ships with the package to be read, not compiled: it is what maps a
    # python name to the C++ behind it, which no other shipped file states. Reached from
    # python through get_source_dir(), so it goes inside the package in every install mode.
    file( COPY ${PBF_PYTHON_BINDING_SOURCES} DESTINATION ${PBF_PACKAGE_DIR}/src )

    # Carries the mlhp version and index sizes of this build, so it is configured
    configure_file( src/pbf/_buildinfo.py.in ${PBF_PACKAGE_DIR}/_buildinfo.py )

    # _core has to sit inside the package, while multi-config generators build it into a
    # per-config subdirectory, so the package holds whatever was compiled last.
    add_custom_command( TARGET pymlhpbf POST_BUILD COMMAND ${CMAKE_COMMAND} -E
        copy_if_different $<TARGET_FILE:pymlhpbf> ${PBF_PACKAGE_DIR} )

    install( FILES ${PBF_PYTHON_PACKAGE_SOURCES} ${PBF_PACKAGE_DIR}/_buildinfo.py
             DESTINATION ${PBF_INSTALL_PACKAGEDIR} )
    install( FILES ${PBF_PYTHON_BINDING_SOURCES} DESTINATION ${PBF_INSTALL_PACKAGEDIR}/src )
    install( TARGETS pymlhpbf LIBRARY DESTINATION ${PBF_INSTALL_PACKAGEDIR} )

endif( ${PBF_PYTHON} )

# ---------------------- Verification tests -----------------------

if( ${PBF_TESTS} )
	include( tests/files.cmake )
	list( TRANSFORM PBF_TEST_SOURCES PREPEND tests/ )

	add_executable( mlhpbf_tests ${PBF_TEST_SOURCES} )
	target_link_libraries( mlhpbf_tests PRIVATE mlhp::pbf )
	set_target_properties( mlhpbf_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin )
	target_include_directories( mlhpbf_tests PRIVATE mlhp/external/catch2 )

	target_include_directories( mlhpbf_tests SYSTEM PRIVATE external )


    install( TARGETS mlhpbf_tests )

	if( ${PBF_PYTHON} )

		list( TRANSFORM PBF_PYTHON_TESTS PREPEND tests/ )
				
		file( COPY ${PBF_PYTHON_TESTS} DESTINATION ${PBF_BUILD_BINARY_DIR}/pbftests )
		file( COPY tests/run_pbftests.py DESTINATION ${PBF_BUILD_BINARY_DIR} )
				
		install( FILES ${PBF_PYTHON_TESTS} DESTINATION ${CMAKE_INSTALL_BINDIR}/pbftests )
		install( FILES tests/run_pbftests.py DESTINATION ${CMAKE_INSTALL_BINDIR} )

	endif( ${PBF_PYTHON} )
endif( ${PBF_TESTS} )

# ----------------------- Example drivers -------------------------

include( tools/cmake/CreateExampleArchive.cmake )

function( createPythonExample name description )

    # Collected for the example archive below, which pbf.examples() reads. The description
    # it prints is the module docstring of the script, not the argument here.
    set_property( GLOBAL APPEND PROPERTY PBF_PYTHON_EXAMPLES ${name}.py )

    if( ${PBF_PYTHON} )
        configure_file( examples/${name}.py ${PBF_BUILD_BINARY_DIR}/${name}.py COPYONLY )
    endif( ${PBF_PYTHON} )

endfunction( )

createPythonExample( AMB2018-01 "Benchmark problem by NIST." )
createPythonExample( hatched_square "Hatched square with thermomechanics." )
createPythonExample( hatched_square_multilayer "Multilayer hatched square with thermomechanics." )
createPythonExample( hollow_sphere "Ressidual heat buildup when printing a hollow sphere." )
createPythonExample( multilayer_thermal "Multi-layer thermal simulation with powder bed." )
createPythonExample( multilayer_thermomech "Multi-layer thermomechanical simulation with powder bed." )
createPythonExample( powder_height "Assess difference between powder height and layer thickness." )
createPythonExample( singletrack_thermal "Single track thermal computation." )
createPythonExample( singletrack_thermomech "Single track thermomechanical computation." )
createPythonExample( stldomain "Single track thermal computation with stl domain initialization." )
createPythonExample( steadystate_thermal "Steady state thermal computation with surface heat source." )
createPythonExample( thermal_metrics "Hatched square with custom postprocessing." )

configure_file( examples/stldomain.stl ${CMAKE_BINARY_DIR}/stldomain.stl COPYONLY )

if( ${PBF_PYTHON} )

    # Zip the examples registered above into an archive inside the package, read by
    # pbf.examples()/pbf.example(). The stl geometry is deliberately left out, so it
    # never ships in a wheel; stldomain.py says where to obtain it.
    get_property( PBF_PYTHON_EXAMPLES GLOBAL PROPERTY PBF_PYTHON_EXAMPLES )

    set( PBF_EXAMPLE_ARCHIVE ${PBF_PACKAGE_DIR}/pbf_examples.zip )

    CreateExampleArchive( ${PBF_EXAMPLE_ARCHIVE} ${CMAKE_CURRENT_SOURCE_DIR}/examples
                          ${PBF_PYTHON_EXAMPLES} )

    # The C++ extension is multi-file and goes into its own archive
    set( PBF_CPP_EXAMPLE_ARCHIVE ${PBF_PACKAGE_DIR}/pbf_cpp_extension.zip )

    CreateExampleArchive( ${PBF_CPP_EXAMPLE_ARCHIVE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/extension
                          CMakeLists.txt extension.cpp extension_test.py )

    install( FILES ${PBF_EXAMPLE_ARCHIVE} ${PBF_CPP_EXAMPLE_ARCHIVE}
             DESTINATION ${PBF_INSTALL_PACKAGEDIR} )

endif( ${PBF_PYTHON} )
