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

cmake_minimum_required( VERSION 3.15 )

project( mlhp LANGUAGES CXX )

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

# ------------------- set project variables -----------------------

option( BUILD_SHARED_LIBS "Build shared library" ON )
option( MLHP_DEBUG_CHECKS "Enable debug checks with (hopefully) minor performance penalty." ON )
option( MLHP_ALL_OPTIMIZATIONS "Compile with all optimizations, including platform specific ones" ON )

option( MLHP_PYTHON "Enable pybind11 based python interface" ${MLHP_IS_ROOT} )
option( MLHP_TEST_PYTHON "Enable C++ unit and system tests." ${MLHP_IS_ROOT} )
option( MLHP_TEST_CPP "Enable C++ unit and system tests." ${MLHP_IS_ROOT} )
set( MLHP_EXAMPLES Off CACHE STRING "Example drivers: Off, On (pick individual C++ examples), or All." )
set_property( CACHE MLHP_EXAMPLES PROPERTY STRINGS Off On All )

set( MLHP_MULTITHREADING OMP CACHE STRING "Select multi-threading implementation." )
set_property( CACHE MLHP_MULTITHREADING PROPERTY STRINGS OMP Off )

option( MLHP_VENDOR_OPENMP "Install the OpenMP runtime beside the core library." OFF )

set( MLHP_DIMENSIONS 3 CACHE STRING "Highest dimension to instantiate." )

set( MLHP_BUILD_ARCHIVE_DIR ${CMAKE_BINARY_DIR}/lib CACHE PATH "Build directory for static libs." )
set( MLHP_BUILD_LIBRARY_DIR ${CMAKE_BINARY_DIR}/lib CACHE PATH "Build directory for dynamic libs." )
set( MLHP_BUILD_BINARY_DIR ${CMAKE_BINARY_DIR}/bin CACHE PATH "Build directory for binaries." )

set( MLHP_SIMD_ALIGNMENT 32 CACHE STRING "Simd over-alignment 32 or 64, depending on cache line size of the CPU." )
set( MLHP_INDEX_SIZE_CELLS 32 CACHE STRING "Unsigned integer size used to index mesh cells." )
set( MLHP_INDEX_SIZE_DOFS 64 CACHE STRING "Unsigned integer size used to index degrees of freedom and sparse indices." )

mark_as_advanced( MLHP_BUILD_ARCHIVE_DIR MLHP_BUILD_LIBRARY_DIR MLHP_BUILD_BINARY_DIR
                  MLHP_INDEX_SIZE_CELLS MLHP_INDEX_SIZE_DOFS )

set( MLHP_OUTPUT_DIRS ARCHIVE_OUTPUT_DIRECTORY ${MLHP_BUILD_ARCHIVE_DIR}
                      LIBRARY_OUTPUT_DIRECTORY ${MLHP_BUILD_LIBRARY_DIR}
                      RUNTIME_OUTPUT_DIRECTORY ${MLHP_BUILD_BINARY_DIR} )
       
# Changes the default install path to build/install, assuming build is the project directory
if ( CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT )
    set( CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/install" CACHE PATH "default install path" FORCE )
endif( )

# Set dimension to 3 if lower
if( ${MLHP_DIMENSIONS} LESS 3 )
    message(WARNING "MLHP_DIMENSION must be at least 3.")

    set( MLHP_DIMENSIONS 3 CACHE STRING "Highest dimension to instantiate." FORCE ) 
endif( ${MLHP_DIMENSIONS} LESS 3 )

# Set up build types with Release as default
include(tools/cmake/ConfigureBuildType.cmake)

# ---------------------------- Core -------------------------------

# Provides interface target vtu11::vtu11
add_subdirectory(external/vtu11)

# Create mlhp_public_compile_flags and mlhp_private_compile_flags interface targets
include(tools/cmake/ConfigureCompiler.cmake)

# To automatically configure a library export header into build tree
include(GenerateExportHeader)

# Sets MLHP_CORE_SOURCES and MLHP_CORE_INCLUDES
include( src/core/files.cmake )

list( TRANSFORM MLHP_CORE_SOURCES PREPEND src/core/ )
list( TRANSFORM MLHP_CORE_INCLUDES PREPEND include/mlhp/core/ )

add_library( mlhpcore ${MLHP_CORE_SOURCES} ${MLHP_CORE_INCLUDES} include/mlhp/core.hpp )
add_library( mlhp::core ALIAS mlhpcore )

# Wrapped in BUILD_INTERFACE: a static library records its private dependencies as
# $<LINK_ONLY:...> usage requirements, and install( EXPORT ) below then refuses to
# export mlhpcore because vtu11 and the flag target are not in the export set.
target_link_libraries( mlhpcore PRIVATE $<BUILD_INTERFACE:mlhp_private_compile_flags>
                                        $<BUILD_INTERFACE:vtu11::vtu11>
                                PUBLIC mlhp_public_compile_flags )

# Absolute paths hold only while building; the installed interface gets its include
# directory from the INCLUDES DESTINATION of install( TARGETS ) below.
target_include_directories( mlhpcore PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include> )

generate_export_header( mlhpcore EXPORT_MACRO_NAME MLHP_EXPORT EXPORT_FILE_NAME 
                        ${CMAKE_CURRENT_BINARY_DIR}/include/mlhp/core/coreexport.hpp )

# Set output directories
set_target_properties( mlhpcore PROPERTIES ${MLHP_OUTPUT_DIRS} )

# 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( MLHP_VERSION_STRING ${SKBUILD_PROJECT_VERSION} )
else( )
    execute_process( COMMAND git describe --tags --abbrev=0
                     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                     OUTPUT_VARIABLE MLHP_VERSION_STRING
                     OUTPUT_STRIP_TRAILING_WHITESPACE
                     ERROR_QUIET )
endif( )

if( MLHP_VERSION_STRING MATCHES "^([0-9]+(\\.[0-9]+)*)" )
    set( MLHP_VERSION ${CMAKE_MATCH_1} )
else( )
    message( WARNING "Found no version tag, falling back to 0.0.0. Consumers of the "
                     "installed cmake package cannot request a version." )
    set( MLHP_VERSION 0.0.0 )
    set( MLHP_VERSION_STRING 0.0.0 )
endif( )

# Pybind11 version a downstream extension module must match to share a type registry with
# mlhp's bindings. From the header, since pybind11's own cmake is only added further below.
set( MLHP_PYBIND11_HEADER external/pybind11/include/pybind11/detail/common.h )

if( EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${MLHP_PYBIND11_HEADER} )
    file( STRINGS ${MLHP_PYBIND11_HEADER} MLHP_PYBIND11_VERSION
          REGEX "^#define PYBIND11_VERSION_(MAJOR|MINOR|MICRO) +[0-9]+$" )

    list( TRANSFORM MLHP_PYBIND11_VERSION REPLACE "^.* ([0-9]+)$" "\\1" )
    list( JOIN MLHP_PYBIND11_VERSION "." MLHP_PYBIND11_VERSION )
endif( )

# Installation layout: a wheel keeps everything inside the python package, a plain
# install follows GNUInstallDirs.
include( GNUInstallDirs )

if( SKBUILD )
    set( MLHP_INSTALL_PACKAGEDIR mlhp )
    set( MLHP_INSTALL_INCLUDEDIR mlhp/include )
    set( MLHP_INSTALL_CMAKEDIR   mlhp/cmake )
    set( MLHP_INSTALL_RUNTIMEDIR mlhp )
    set( MLHP_INSTALL_LIBRARYDIR mlhp )
    set( MLHP_INSTALL_ARCHIVEDIR mlhp/lib )
else( SKBUILD )
    set( MLHP_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_BINDIR}/mlhp )
    set( MLHP_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} )
    set( MLHP_INSTALL_CMAKEDIR   ${CMAKE_INSTALL_LIBDIR}/cmake/mlhp )
    set( MLHP_INSTALL_RUNTIMEDIR ${CMAKE_INSTALL_BINDIR} )
    set( MLHP_INSTALL_LIBRARYDIR ${CMAKE_INSTALL_LIBDIR} )
    set( MLHP_INSTALL_ARCHIVEDIR ${CMAKE_INSTALL_LIBDIR} )
endif( SKBUILD )

# Own directory so that the interface include directory above does not expose omp.h
# and shadow the compiler's for everyone else
set( MLHP_INSTALL_OMPDIR ${MLHP_INSTALL_INCLUDEDIR}/openmp )

# Consumers link mlhp::core, so the exported target carries that name
set_target_properties( mlhpcore PROPERTIES EXPORT_NAME core )

if( APPLE )
    set_target_properties( mlhpcore PROPERTIES INSTALL_NAME_DIR "@rpath" )
endif( )

# Reads MLHP_VENDOR_OPENMP, the OpenMP find result, mlhpcore and the MLHP_INSTALL_* layout.
# Sets no variable for later use, but retargets OpenMP::OpenMP_CXX and mlhpcore's install rpath.
# Adds install rules for the OpenMP runtime and the omp.h to compile it against. macOS only.
include( tools/cmake/VendorOpenMP.cmake )

# In a wheel the library goes into the package directory beside the extension module,
# where it is found through the module's own directory on Windows and $ORIGIN elsewhere.
install( TARGETS mlhpcore mlhp_public_compile_flags
         EXPORT mlhpcore-targets
         ARCHIVE DESTINATION ${MLHP_INSTALL_ARCHIVEDIR}
         LIBRARY DESTINATION ${MLHP_INSTALL_LIBRARYDIR}
         RUNTIME DESTINATION ${MLHP_INSTALL_RUNTIMEDIR}
         INCLUDES DESTINATION ${MLHP_INSTALL_INCLUDEDIR} )

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

# config.hpp and coreexport.hpp are configured into the build tree
install( DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/mlhp
         DESTINATION ${MLHP_INSTALL_INCLUDEDIR} )

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

include( CMakePackageConfigHelpers )

configure_package_config_file( tools/cmake/mlhpConfig.cmake.in
                               ${CMAKE_CURRENT_BINARY_DIR}/mlhpConfig.cmake
                               INSTALL_DESTINATION ${MLHP_INSTALL_CMAKEDIR}
                               PATH_VARS MLHP_INSTALL_LIBRARYDIR MLHP_INSTALL_OMPDIR )

write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/mlhpConfigVersion.cmake
                                  VERSION ${MLHP_VERSION}
                                  COMPATIBILITY SameMinorVersion )

install( FILES ${CMAKE_CURRENT_BINARY_DIR}/mlhpConfig.cmake
               ${CMAKE_CURRENT_BINARY_DIR}/mlhpConfigVersion.cmake
         DESTINATION ${MLHP_INSTALL_CMAKEDIR} )

# ----------------------- Core testrunner -------------------------

if( ${MLHP_TEST_CPP} )

    # Creates MLHP_CORE_TEST_SOURCES variable
    include( tests/core/files.cmake )
    include( tests/system/files.cmake )

    # Core unit tests
    list( TRANSFORM MLHP_CORE_TEST_SOURCES PREPEND tests/core/ )
    list( TRANSFORM MLHP_SYSTEM_TEST_SOURCES PREPEND tests/system/ )

    # Catch builds unique identifiers from __COUNTER__, which clang 20 and newer report as
    # a C2y extension. Falling back to __LINE__ only forbids two Catch macros on one line.
    set( MLHP_CATCH_DEFINITIONS CATCH_CONFIG_NO_COUNTER )

    add_executable( mlhpcore_testrunner ${MLHP_CORE_TEST_SOURCES} )

    target_link_libraries( mlhpcore_testrunner PRIVATE mlhp::core mlhp_private_compile_flags )
    target_compile_definitions( mlhpcore_testrunner PRIVATE ${MLHP_CATCH_DEFINITIONS} )
    target_include_directories( mlhpcore_testrunner PRIVATE . )
    set_target_properties( mlhpcore_testrunner PROPERTIES ${MLHP_OUTPUT_DIRS} )

    file( GLOB MLHP_CORE_TESTFILES tests/core/testfiles/* )
    file( COPY ${MLHP_CORE_TESTFILES} DESTINATION testfiles/core )

    install( DIRECTORY tests/core/testfiles/ DESTINATION testfiles/core )

    # System tests
    add_executable( system_testrunner ${MLHP_SYSTEM_TEST_SOURCES} )
                    
    target_link_libraries( system_testrunner PRIVATE mlhp::core mlhp_private_compile_flags )
    target_compile_definitions( system_testrunner PRIVATE ${MLHP_CATCH_DEFINITIONS} )
    target_include_directories( system_testrunner PRIVATE . )
    set_target_properties( system_testrunner PROPERTIES ${MLHP_OUTPUT_DIRS} )
  
    install( TARGETS mlhpcore_testrunner system_testrunner )

endif( ${MLHP_TEST_CPP} )

# --------------------------- Examples ----------------------------

include( tools/cmake/CreateExampleDriver.cmake )
include( tools/cmake/CreateExampleArchive.cmake )

CreateExampleCppDriver( fichera_corner "Poisson equation fichera corner benchmark." )
CreateExampleCppDriver( j2_pressurized_sphere_fcm "Immersed pressurized sphere with J2 plasticity." )
CreateExampleCppDriver( linear_elastic_fcm_stl "FCM example with stl geometry." )
CreateExampleCppDriver( navier_stokes_vms "2D lid-driven cavity with Navier-Stokes VMS stabilization." )
CreateExampleCppDriver( travelling_heat_source "Linear heat equation with moving point-like source." )
CreateExampleCppDriver( waveequation_matrix_free "Immersed scalar wave equation." )
CreateExampleCppDriver( wing_elastic_fcm "FCM example with linear elasticity." )

CreateExamplePythonDriver( elastic_fcm_csg "Compute with a csg geometry." )
CreateExamplePythonDriver( elastic_fcm_stl "Compute with an stl geometry." )
CreateExamplePythonDriver( elastic_gmsh "Compute with unstructured grid from gmsh file." )
CreateExamplePythonDriver( elastic_gyroid "Gyroid TPMS implicit geometry." )
CreateExamplePythonDriver( elastic_mapped_thin_solid "Curved thin-walled geometry from a mapped grid." )
CreateExamplePythonDriver( elastic_wave_IMEX_SEM "Immersed spectral elements with implicit Newmark on cut elements." )
CreateExamplePythonDriver( hyperelastic_gyroid "Large deformation Gyroid TPMS implicit geometry." )
CreateExamplePythonDriver( interactive_eigenmodes "Interactive eigenvalue analysis using a line segment boundary." INTERACTIVE )
CreateExamplePythonDriver( j2_plasticity_compiled "Immersed J2 plasticity using a cffi-compiled material." )
CreateExamplePythonDriver( planestress_fcm_lshaped_adaptive "Immersed l-shaped domain with adaptive refinement." )
CreateExamplePythonDriver( planestress_fcm_plate_with_hole "Plate with a hole using FCM." )
CreateExamplePythonDriver( poisson_compiled "Define integrand with numba or cffi." )
CreateExamplePythonDriver( scalar_wave_SEM "Spectral elements for scalar wave equation." )
CreateExamplePythonDriver( thermoelastic_spinodoid_fcm "One-way thermomechanical spinodoid with FCM and Nitsche." )

# Copy example input geometry and the example runner next to the example drivers
# in bin so argument-less runs find them. Example builds only; the geometry is
# deliberately not added to the python example archive, so it never ships in wheels.
string( TOUPPER "${MLHP_EXAMPLES}" examplesMode )
if( ${MLHP_EXAMPLES} OR examplesMode STREQUAL "ALL" )
    configure_file( examples/elastic_gmsh.msh ${MLHP_BUILD_BINARY_DIR}/elastic_gmsh.msh COPYONLY )
    configure_file( tests/core/testfiles/csg_binary.stl ${MLHP_BUILD_BINARY_DIR}/csg_binary.stl COPYONLY )

    file( COPY tools/scripts/run_examples.py DESTINATION ${MLHP_BUILD_BINARY_DIR} )

    # Write the example list consumed by run_examples.py from the registrations
    # above (the JSON objects carry no semicolons, so the list separator is safe).
    get_property( MLHP_RUN_EXAMPLES_ENTRIES GLOBAL PROPERTY MLHP_RUN_EXAMPLES )
    string( REPLACE ";" ",\n  " MLHP_RUN_EXAMPLES_ENTRIES "${MLHP_RUN_EXAMPLES_ENTRIES}" )
    file( WRITE ${MLHP_BUILD_BINARY_DIR}/run_examples.json "[\n  ${MLHP_RUN_EXAMPLES_ENTRIES}\n]\n" )
endif( )

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

if( ${MLHP_PYTHON} )

    # Use CMake's FindPython (NEW) instead of pybind11's classic finder
    if( NOT DEFINED PYBIND11_FINDPYTHON OR PYBIND11_FINDPYTHON STREQUAL "COMPAT" )
        set( PYBIND11_FINDPYTHON NEW )
    endif( )

    add_subdirectory( external/pybind11 )

    # Creates MLHP_PYTHON_BINDING_SOURCES and MLHP_PYTHON_INCLUDES variables
    include( src/python/files.cmake )

    list( TRANSFORM MLHP_PYTHON_BINDING_SOURCES PREPEND src/python/ )
    list( TRANSFORM MLHP_PYTHON_INCLUDES PREPEND include/mlhp/python/ )
    list( TRANSFORM MLHP_PYTHON_PACKAGE_SOURCES PREPEND src/python/mlhp/ )

    pybind11_add_module( pymlhpcore ${MLHP_PYTHON_BINDING_SOURCES} ${MLHP_PYTHON_INCLUDES} )

    target_link_libraries( pymlhpcore PRIVATE mlhpcore mlhp_private_compile_flags )
    
    target_include_directories( pymlhpcore PRIVATE . )

    # Imported as mlhp._core, so the module file is named _core
    set_target_properties( pymlhpcore PROPERTIES OUTPUT_NAME _core
                           LIBRARY_OUTPUT_DIRECTORY ${MLHP_BUILD_BINARY_DIR} )

    # The core library is installed next to the module. Windows searches the module's own
    # directory for it, everywhere else that takes an rpath.
    if( APPLE )
        set_target_properties( pymlhpcore PROPERTIES INSTALL_RPATH "@loader_path" )
    elseif( UNIX )
        set_target_properties( pymlhpcore PROPERTIES INSTALL_RPATH "$ORIGIN" )
    endif( )

    set( MLHP_PACKAGE_DIR ${MLHP_BUILD_BINARY_DIR}/mlhp )

    # Copy python sources
    file( COPY ${MLHP_PYTHON_PACKAGE_SOURCES} DESTINATION ${MLHP_PACKAGE_DIR} )

    # Assemble an importable package: the module and the core library must sit inside
    # mlhp/ to be found without path manipulation. Multi-config generators build into a
    # per-config subdirectory, so the package holds whatever was compiled last.
    add_custom_command( TARGET pymlhpcore POST_BUILD COMMAND ${CMAKE_COMMAND} -E
        copy_if_different $<TARGET_FILE:pymlhpcore> ${MLHP_PACKAGE_DIR} )

    if( BUILD_SHARED_LIBS )
        add_custom_command( TARGET pymlhpcore POST_BUILD COMMAND ${CMAKE_COMMAND} -E
            copy_if_different $<TARGET_FILE:mlhpcore> ${MLHP_PACKAGE_DIR} )
    endif( BUILD_SHARED_LIBS )

    # Zip the python examples registered via CreateExamplePythonDriver into
    # an archive inside the package, accessed by mlhp.examples()/mlhp.example()
    get_property( MLHP_PYTHON_EXAMPLES GLOBAL PROPERTY MLHP_PYTHON_EXAMPLES )

    set( MLHP_EXAMPLE_ARCHIVE ${MLHP_PACKAGE_DIR}/mlhp_examples.zip )

    CreateExampleArchive( ${MLHP_EXAMPLE_ARCHIVE} ${CMAKE_CURRENT_SOURCE_DIR}/examples
                          ${MLHP_PYTHON_EXAMPLES} )

    # Bundle C++ extension files into archive that is kept separate from Python examples
    set( MLHP_CPP_EXAMPLE_ARCHIVE ${MLHP_PACKAGE_DIR}/mlhp_cpp_extension.zip )

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

    install( FILES ${MLHP_PYTHON_PACKAGE_SOURCES} ${MLHP_EXAMPLE_ARCHIVE}
             ${MLHP_CPP_EXAMPLE_ARCHIVE} DESTINATION ${MLHP_INSTALL_PACKAGEDIR} )

    install( TARGETS pymlhpcore LIBRARY DESTINATION ${MLHP_INSTALL_PACKAGEDIR} )

    # The extension module resolves the core library through its own directory. In a wheel
    # that is where the library already went; a plain install puts it where C++ consumers
    # expect it instead and needs this second copy.
    if( BUILD_SHARED_LIBS AND NOT SKBUILD )
        install( FILES $<TARGET_FILE:mlhpcore> DESTINATION ${MLHP_INSTALL_PACKAGEDIR} )
    endif( )

    if( ${MLHP_TEST_PYTHON} )

        # Creates MLHP_PYTHON_TESTS variable
        include( tests/system/files.cmake )

        list( TRANSFORM MLHP_PYTHON_TESTS PREPEND tests/system/ )
            
        file( COPY ${MLHP_PYTHON_TESTS} DESTINATION ${MLHP_BUILD_BINARY_DIR}/systemtests )
        file( COPY tests/system/run_systemtests.py DESTINATION ${MLHP_BUILD_BINARY_DIR} )

	    install( FILES ${MLHP_PYTHON_TESTS} DESTINATION ${CMAKE_INSTALL_BINDIR}/systemtests )
	    install( FILES tests/system/run_systemtests.py DESTINATION ${CMAKE_INSTALL_BINDIR} )

        # Separate from the systemtests/ directory discovered above, so the system test
        # suite never depends on the optional mkl module.
        file( COPY tests/mkl/run_mkltests.py DESTINATION ${MLHP_BUILD_BINARY_DIR} )

	    install( FILES tests/mkl/run_mkltests.py DESTINATION ${CMAKE_INSTALL_BINDIR} )

    endif( ${MLHP_TEST_PYTHON} )
endif( ${MLHP_PYTHON} )

# Configure x macro strings
set(MLHP_DIMENSIONS_XMACRO_LIST)
set(MLHP_POSTPROCESSING_DIMENSIONS_XMACRO_LIST)

foreach(DIM RANGE 1 ${MLHP_DIMENSIONS})
    set(MLHP_DIMENSIONS_XMACRO_LIST "${MLHP_DIMENSIONS_XMACRO_LIST} MLHP_INSTANTIATE_DIM(${DIM})")
endforeach(DIM)

foreach(DIM RANGE 1 3)
    set(MLHP_POSTPROCESSING_DIMENSIONS_XMACRO_LIST "${MLHP_POSTPROCESSING_DIMENSIONS_XMACRO_LIST} MLHP_INSTANTIATE_DIM(${DIM})")
endforeach(DIM)

# Configure configuration header to build tree
configure_file( src/core/config.hpp.in include/mlhp/core/config.hpp )

