################################################################################
##                                                                            ##
##  This file is part of NCrystal (see https://mctools.github.io/ncrystal/)   ##
##                                                                            ##
##  Copyright 2015-2025 NCrystal developers                                   ##
##                                                                            ##
##  Licensed under the Apache License, Version 2.0 (the "License");           ##
##  you may not use this file except in compliance with the License.          ##
##  You may obtain a copy of the License at                                   ##
##                                                                            ##
##      http://www.apache.org/licenses/LICENSE-2.0                            ##
##                                                                            ##
##  Unless required by applicable law or agreed to in writing, software       ##
##  distributed under the License is distributed on an "AS IS" BASIS,         ##
##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  ##
##  See the License for the specific language governing permissions and       ##
##  limitations under the License.                                            ##
##                                                                            ##
################################################################################

cmake_minimum_required(VERSION 3.16...3.31)

#Detect skbuild mode (std/monolithic) if appropriate:

if ( DEFINED SKBUILD_PROJECT_NAME )
  set( NCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE "ON" )
  set( nccore_pymodname "_ncrystal_core" )
  if ( DEFINED NCRYSTAL_SKBUILD_MONOLITHIC )
    if ( NCRYSTAL_SKBUILD_MONOLITHIC )
      set ( NCRYSTAL_SKBUILD_MONOLITHIC "ON" )
      set( nccore_pymodname "_ncrystal_core_monolithic" )
    endif()
  else()
    set ( NCRYSTAL_SKBUILD_MONOLITHIC "OFF" )
  endif()
else()
  set ( NCRYSTAL_SKBUILD_MONOLITHIC "OFF" )
endif()

# Respect value of CMAKE_BUILD_TYPE if already defined, otherwise fall back to
# Release. In any case, expose CMAKE_BUILD_TYPE as an explicit cache variable
# (gives drop-down list in gui). This must come before the call to
# project(..). We do not do this in case the generator is multi-cfg, and we also
# provide the hidden NCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE option to not do it.
#

if( NOT NCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE )
  get_property( gen_is_multicfg GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG )
  if ( NOT gen_is_multicfg )
    if( DEFINED CMAKE_BUILD_TYPE )
      set( _def_cbt ${CMAKE_BUILD_TYPE} )
    else()
      set( _def_cbt Release )
    endif()
    set( CMAKE_BUILD_TYPE ${_def_cbt}  CACHE STRING
      "Choose the type of build, options are: Debug Release RelWithDebInfo and MinSizeRel." )
    set_property( CACHE CMAKE_BUILD_TYPE
      PROPERTY STRINGS Debug Release RelWithDebInfo MinSizeRel None )
  endif()
endif()

#Setup project:

set( _project_metadata LANGUAGES CXX C )

list( APPEND
  _project_metadata DESCRIPTION
  "Library for thermal neutron transport in crystals and other materials" )
list( APPEND
  _project_metadata HOMEPAGE_URL
  "https://github.com/mctools/ncrystal" )

project( NCrystal VERSION 4.1.4 ${_project_metadata} )

unset( _project_metadata )

if( NOT NCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE )
  if ( NOT gen_is_multicfg )
    if ( NOT CMAKE_BUILD_TYPE )
      #This can happen if parent project called the project(..) function before
      #doing the song and dance we did above.
      set(CMAKE_BUILD_TYPE Release)
    endif()
  endif()
endif()

# Set module path
set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake/modules")

#Define options:
include(ncrystal_options)

function( _ncrystal_option_force varname value )
  message( STATUS "Forcing ${varname}=\"${value}\"" )
  set( "${varname}" "${value}" PARENT_SCOPE )
endfunction()

if ( DEFINED SKBUILD_PROJECT_NAME )
  message( STATUS "scikit-build mode detected. Overriding some settings." )
  _ncrystal_option_force( CMAKE_INSTALL_LIBDIR "${nccore_pymodname}/data/lib" )
  _ncrystal_option_force( CMAKE_INSTALL_INCLUDEDIR "${nccore_pymodname}/data/include" )
  _ncrystal_option_force( CMAKE_INSTALL_DATADIR "${nccore_pymodname}/data/data" )
  _ncrystal_option_force( CMAKE_INSTALL_BINDIR "${nccore_pymodname}/data/bin" )
  set ( ncrystal_skbld_autogendir "${PROJECT_SOURCE_DIR}/skbld_autogen" )
  #Cleanup skbld_autogendir unless we are building FROM an sdist (or creating
  #one):
  if ( EXISTS "${ncrystal_skbld_autogendir}/is_sdist.txt"
      AND NOT "${SKBUILD_STATE}" STREQUAL "sdist" )
    message(STATUS "Running from sdist - will not clean autogenerated files")
  else()
    if ( IS_DIRECTORY "${ncrystal_skbld_autogendir}" )
      message( STATUS "Cleaning out ${ncrystal_skbld_autogendir}" )
      file( REMOVE_RECURSE "${ncrystal_skbld_autogendir}" )
    endif()
    file( MAKE_DIRECTORY "${ncrystal_skbld_autogendir}" )
    if ( "${SKBUILD_STATE}" STREQUAL "sdist" )
      file(TOUCH "${ncrystal_skbld_autogendir}/is_sdist.txt")
    endif()
  endif()
endif()

if ( NCRYSTAL_ENABLE_TESTING AND NCRYSTAL_ENABLE_CORE_TESTING )
  message( FATAL_ERROR
    "Enabling both NCRYSTAL_ENABLE_TESTING"
    " and NCRYSTAL_ENABLE_CORE_TESTING is not supported" )
endif()

if ( NCRYSTAL_ENABLE_CORE_TESTING )
  enable_testing()
  include(ncrystal_coretests)
endif()

if ( NCRYSTAL_ENABLE_TESTING )
  #When testing we require a namespace of "test" which is intended to ensure
  #that we never clash with a system-installed NCrystal version (for instance we
  #had problems when a ctest executable picked up libNCrystal.so from the conda
  #installed NCrystal, resulting in hard to debug errors).
  #
  #Additionally on windows we need to use NCRYSTAL_WINEXPORTALL=ON so the ctest
  #binaries can access internal symbols in the NCrystal library.
  #
  #For both of these reasons we will for safety forbid actual installations with
  #these settings, to avoid potential weird installations.
  if( "${CMAKE_VERSION}" VERSION_LESS "3.28" )
    #NB: Keep this minimum version value synchronised with the one on the top of
    #tests/CMakeLists.txt
    message( FATAL_ERROR "NCRYSTAL_ENABLE_TESTING requires cmake >= 3.28" )
  endif()
  if ( NOT NCRYSTAL_SKIP_INSTALL )
    message(
      WARNING
      "NCRYSTAL_ENABLE_TESTING implies NCRYSTAL_SKIP_INSTALL=ON for safety"
    )
    _ncrystal_option_force( NCRYSTAL_SKIP_INSTALL "ON" )
  endif()
  if ( WIN32 AND NOT NCRYSTAL_WINEXPORTALL )
    message(
      WARNING
      "NCRYSTAL_ENABLE_TESTING implies NCRYSTAL_WINEXPORTALL=ON in Windows"
    )
    _ncrystal_option_force( NCRYSTAL_WINEXPORTALL "ON" )
  endif()
  if ( NCRYSTAL_NAMESPACE AND NOT "x${NCRYSTAL_NAMESPACE}" STREQUAL "xtest" )
    message(
      FATAL_ERROR
      "Do not set NCRYSTAL_NAMESPACE with NCRYSTAL_ENABLE_TESTING."
    )
  endif()
    _ncrystal_option_force( NCRYSTAL_NAMESPACE "test" )
else()
  #Make sure the namespace "test" is only ever used by the
  #NCRYSTAL_ENABLE_TESTING option (so no installation out there can have that
  #namespace).
  if ( "x${NCRYSTAL_NAMESPACE}" STREQUAL "xtest" )
    message(
      FATAL_ERROR
      "The NCRYSTAL_NAMESPACE value \"test\" is reserved for internal usage."
    )
  endif()
endif()

function( ncinstall )
  if ( NOT NCRYSTAL_SKIP_INSTALL )
    install( ${ARGN} )
  endif()
endfunction()

if ( NCRYSTAL_ENABLE_CPACK )
  set(CPACK_PACKAGE_CONTACT "ncrystal-developers@cern.ch")
  set(CPACK_NSIS_PACKAGE_NAME "${PROJECT_NAME} ${PROJECT_VERSION}")
  set(CPACK_NSIS_DISPLAY_NAME "${PROJECT_NAME} ${PROJECT_VERSION}")
  include(CPack)
endif()

if ( NCRYSTAL_NAMESPACE )
  string( MAKE_C_IDENTIFIER "${NCRYSTAL_NAMESPACE}" tmp)
  string( TOLOWER "${tmp}" tmp)
  if ( NOT "x${NCRYSTAL_NAMESPACE}" STREQUAL "x${tmp}" )
    message(
      FATAL_ERROR
      "The NCRYSTAL_NAMESPACE must be a lowercased valid C identifier"
      "(meaning: alphanumeric, not leading with digit, lowercased)."
      )
  endif()
else()
  if ( NOT "x${NCRYSTAL_NAMESPACE}" STREQUAL "x" )
    message(
      FATAL_ERROR
      "Do not set NCRYSTAL_NAMESPACE to a value which CMake interprets "
      "as \"false\". To disable, instead set it to an empty string."
      )
  endif()
endif()
if ( NCRYSTAL_SKBUILD_MONOLITHIC )
  #Add a namespace for safety
  set( NCRYSTAL_NAMESPACE "mono${NCRYSTAL_NAMESPACE}" )
endif()

#Installation directories (try to follow standard conventions):
include(GNUInstallDirs)
set(NCrystal_BINDIR "${CMAKE_INSTALL_BINDIR}")#e.g. <prefix>/bin>
set(NCrystal_LIBDIR "${CMAKE_INSTALL_LIBDIR}")#e.g. <prefix>/lib>

function( ncfind_relinstpath resultvar srcdir tgtdir )
  if ( NOT srcdir OR NOT IS_ABSOLUTE "${srcdir}" )
    set( srcdir "${CMAKE_INSTALL_PREFIX}/${srcdir}" )
  endif()
  if ( NOT tgtdir OR NOT IS_ABSOLUTE "${tgtdir}" )
    set( tgtdir "${CMAKE_INSTALL_PREFIX}/${tgtdir}" )
  endif()
  file(RELATIVE_PATH res "${srcdir}" "${tgtdir}")
  set( "${resultvar}" "${res}" PARENT_SCOPE )
endfunction()

set(ncrystal_skbuild_shlib_in_wheel_scripts "OFF")
if ( WIN32 )
  #dll's must be in %PATH% so we put them in bin (same as seems to be the way
  #most packages do it on conda-forge).
  set(NCrystal_SHLIBDIR "${NCrystal_BINDIR}")
  if ( DEFINED SKBUILD_PROJECT_NAME )
    #For wheels we will place the NCrystal.dll into the scripts folder, since
    #that seems to be the only way to get the dll onto the PATH.
    set(NCrystal_SHLIBDIR "${SKBUILD_SCRIPTS_DIR}")
    set(ncrystal_skbuild_shlib_in_wheel_scripts "ON")
  endif()
else()
  #sane standard world
  set(NCrystal_SHLIBDIR "${NCrystal_LIBDIR}")
endif()

set(NCrystal_INCDIR "${CMAKE_INSTALL_INCLUDEDIR}")#e.g. <prefix>/include>
set(NCrystal_DATAROOT "${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}")#e.g. <prefix>/share/NCrystal>
if( NOT NCrystal_DATAFILESDIR )
  set( NCrystal_DATAFILESDIR "${NCrystal_DATAROOT}/data")#e.g. <prefix>/share/NCrystal/data>
endif()
if ( NOT NCrystal_CMAKEDIR )
  set(NCrystal_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}")#e.g. <prefix>/lib/cmake/NCrystal>
endif()

#Get a few relative paths, mostly for expansion in various installed files (we
#use PROJECT_BINARY_DIR as prefix here, but it should not matter which as long
#as it is an absolute path):

ncfind_relinstpath( NCrystal_relpath_BINDIR2LIBDIR "${NCrystal_BINDIR}" "${NCrystal_LIBDIR}" )
ncfind_relinstpath( NCrystal_relpath_BINDIR2SHLIBDIR "${NCrystal_BINDIR}"  "${NCrystal_SHLIBDIR}" )
#file(RELATIVE_PATH NCrystal_relpath_BINDIR2LIBDIR   "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/${NCrystal_LIBDIR}")
#file(RELATIVE_PATH NCrystal_relpath_BINDIR2SHLIBDIR   "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/${NCrystal_SHLIBDIR}")
if ( NCRYSTAL_ENABLE_DATA STREQUAL "ON" )
  ncfind_relinstpath( NCrystal_relpath_BINDIR2DATADIR "${NCrystal_BINDIR}" "${NCrystal_DATAROOT}/data" )
  #file(RELATIVE_PATH NCrystal_relpath_BINDIR2DATADIR "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/${NCrystal_DATAROOT}/data")
else()
  set(NCrystal_relpath_BINDIR2DATADIR "")
endif()
ncfind_relinstpath( NCrystal_relpath_BINDIR2CMAKEDIR "${NCrystal_BINDIR}"  "${NCrystal_CMAKEDIR}" )
ncfind_relinstpath( NCrystal_relpath_BINDIR2INCDIR "${NCrystal_BINDIR}"  "${NCrystal_INCDIR}" )
#file(RELATIVE_PATH NCrystal_relpath_BINDIR2CMAKEDIR "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}")
#file(RELATIVE_PATH NCrystal_relpath_BINDIR2INCDIR   "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/${NCrystal_INCDIR}")
#REMOVED file(RELATIVE_PATH NCrystal_relpath_BINDIR2ROOT     "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}"   "${PROJECT_BINARY_DIR}/")
ncfind_relinstpath( NCrystal_relpath_BINDIR2CMAKEDIR "${NCrystal_BINDIR}"  "${NCrystal_CMAKEDIR}" )
ncfind_relinstpath( NCrystal_relpath_BINDIR2INCDIR "${NCrystal_BINDIR}"  "${NCrystal_INCDIR}" )
ncfind_relinstpath( NCrystal_relpath_CMAKEDIR2ROOT "${NCrystal_CMAKEDIR}" "" )
ncfind_relinstpath( NCrystal_relpath_CMAKEDIR2BINDIR "${NCrystal_BINDIR}" "" )
ncfind_relinstpath( NCrystal_relpath_CMAKEDIR2LIBDIR "${NCrystal_LIBDIR}" "" )
ncfind_relinstpath( NCrystal_relpath_CMAKEDIR2INCDIR "${NCrystal_INCDIR}" "" )
ncfind_relinstpath( NCrystal_relpath_CMAKEDIR2DATAFILESDIR "${NCrystal_DATAFILESDIR}" "" )
#file(RELATIVE_PATH NCrystal_relpath_CMAKEDIR2ROOT   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}" "${PROJECT_BINARY_DIR}/")
#file(RELATIVE_PATH NCrystal_relpath_CMAKEDIR2BINDIR   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}" "${PROJECT_BINARY_DIR}/${NCrystal_BINDIR}")
#file(RELATIVE_PATH NCrystal_relpath_CMAKEDIR2LIBDIR   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}" "${PROJECT_BINARY_DIR}/${NCrystal_LIBDIR}")
#file(RELATIVE_PATH NCrystal_relpath_CMAKEDIR2INCDIR   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}" "${PROJECT_BINARY_DIR}/${NCrystal_INCDIR}")
#file(RELATIVE_PATH NCrystal_relpath_CMAKEDIR2DATAFILESDIR   "${PROJECT_BINARY_DIR}/${NCrystal_CMAKEDIR}" "${PROJECT_BINARY_DIR}/${NCrystal_DATAFILESDIR}")

#Dummy interface target for common properties. Note that the interface is always
#built with C++11 compatible compiler (which CMake will choose from a wide
#variety depending on platform and other targets,
#i.e. C++11/C++14/C++17/gnu++11/...). The NCRYSTAL_BUILD_STRICT option only affects
#private non-transitive properties.
add_library( ncrystal_common INTERFACE )
target_compile_features( ncrystal_common INTERFACE cxx_std_11 )

#Properties for executables (can't transfer all properties via INTERFACE
#targets, so we need this variable-based workaround):
set( _nc_executable_rpath "")

if ( NCRYSTAL_MODIFY_RPATH )
  #Set RPATH properties. For some annoying reason, this is not possible to do
  #via interface targets, so we have to use a variable-based workaround:

  if ( NOT DEFINED CMAKE_INSTALL_RPATH_USE_LINK_PATH )
    #TODO: Figure out if we really need this (perhaps it was only needed for
    #geant4 targets?)
    set( CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE )
  endif()
  if( NOT APPLE )
    #Relocatable RPATHS: $ORIGIN in RPATH (including the $-char!!)  means the
    #location of the binary requiring the dependency:
    list( APPEND _nc_executable_rpath "$ORIGIN/${NCrystal_relpath_BINDIR2LIBDIR}" )
  else()
    #On OSX, rpaths are absolute paths:
    get_filename_component( tmp "${CMAKE_INSTALL_PREFIX}/${NCrystal_LIBDIR}" ABSOLUTE)
    list( APPEND _nc_executable_rpath  "${tmp}" )
  endif()

  if ( DEFINED ENV{CONDA_PREFIX} AND EXISTS "$ENV{CONDA_PREFIX}/lib" )
    #In a conda environment we add $CONDA_PREFIX/lib to the rpath:
    list( APPEND _nc_executable_rpath "$ENV{CONDA_PREFIX}/lib" )
  endif()

  #Test if compiler supports -Wl,--disable-new-dtags. If it does, apply it
  #(otherwise RPATH sections in binaries become RUNPATH instead, which can be
  #overridden by users LD_LIBRARY_PATH (CMake>=3.14 is needed for LINK_OPTIONS on
  #try_compile and for the target_link_options function):
  #
  #NB: CMake 3.18 introduces CheckLinkerFlag module which we can eventually use
  #    instead of try_compile!!
  if( NOT MSVC )
    set(TMP_TESTDIR ${PROJECT_BINARY_DIR}/test_dtagflags)
    file(WRITE ${TMP_TESTDIR}/test.c "int main() { return 0; }\n")
    try_compile(LINKER_HAS_DTAGS "${TMP_TESTDIR}" "${TMP_TESTDIR}/test.c" LINK_OPTIONS -Wl,--disable-new-dtags)
    if (LINKER_HAS_DTAGS)
      #target_link_options(NCrystal PUBLIC "-Wl,--disable-new-dtags")
      target_link_options( ncrystal_common INTERFACE -Wl,--disable-new-dtags )
    endif()
  endif()

endif()

function(file_globsrc output_var pattern)
  file(GLOB tmp LIST_DIRECTORIES false CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/${pattern}" )
  set(${output_var} "${tmp}" PARENT_SCOPE)
endfunction()

function(file_glob output_var pattern)
  file(GLOB tmp LIST_DIRECTORIES false CONFIGURE_DEPENDS "${pattern}" )
  set(${output_var} "${tmp}" PARENT_SCOPE)
endfunction()

function( get_datafile_list output_var )
  set ( ddir "${PROJECT_SOURCE_DIR}/../data" )
  if ( NOT EXISTS "${ddir}/Al_sg225.ncmat" )
    message( FATAL_ERROR "Could not find data directory: ${ddir}" )
  endif()
  file(GLOB tmp LIST_DIRECTORIES false CONFIGURE_DEPENDS "${ddir}/*.ncmat" )
  set( ${output_var} "${tmp}" PARENT_SCOPE )
endfunction()
file_globsrc( SRCS_NC "src/*.cc")

include( "ncrystal_ncapi" )
nccfgapp_create_ncapi_h( ncapi_include_path )

# Detect available NCrystal components and how they depend on each other,
# placing results in "ncpkg_*" variables. It also installs core header files and
# sets up properties on the source files:
include( "ncrystal_coresrc" )
ncrystal_detectsrc()
ncrystal_process_core_srcfiles()

#We must additionally install a few special top-level include files:

ncinstall(
  FILES
  "${ncapi_include_path}/NCrystal/ncapi.h"
  "${PROJECT_SOURCE_DIR}/include/NCrystal/ncrystal.h" #redirects to cinterface/ncrystal.h
  "${PROJECT_SOURCE_DIR}/include/NCrystal/NCrystal.hh" #includes all public non-C headers
  "${PROJECT_SOURCE_DIR}/include/NCrystal/NCRNG.hh" #deprecated (kept for openmc compilation for now)
  "${PROJECT_SOURCE_DIR}/include/NCrystal/NCPluginBoilerplate.hh"
  DESTINATION
  "${NCrystal_INCDIR}/NCrystal"
)

include(CheckCXXCompilerFlag)

if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
  set( NC_STRICT_COMP_FLAGS "/WX" "/W4" "/WD4324" )
else()
  set( NC_STRICT_COMP_FLAGS -Wall -Wextra -pedantic -Werror )
endif()

if ( NCRYSTAL_BUILD_STRICT )
  set( "CMAKE_COMPILE_WARNING_AS_ERROR" "ON" )
  # turn list into space separted string for check_cxx_compiler_flag:
  string( REPLACE ";" " " tmp "${NC_STRICT_COMP_FLAGS}" )
  check_cxx_compiler_flag( "${tmp}" ncrystal_compiler_supports_strict_comp_flags )
endif()

set( STRICT_CPPSTD OFF )
set( STRICT_CSTD OFF )
if ( NCRYSTAL_BUILD_STRICT )
  #We also want to test the C-example with strict C standard (90, 99, 11). For
  #simplicity we simply pick a C standard value based on the provided c++
  #standard, in a way which allows us to cover the various C standards.
  if ( NCRYSTAL_BUILD_STRICT STREQUAL "11"
      OR NCRYSTAL_BUILD_STRICT STREQUAL "ON" )
    set( STRICT_CSTD 90 )
    set( STRICT_CPPSTD 11 )
  elseif ( NCRYSTAL_BUILD_STRICT STREQUAL "14" )
    set( STRICT_CSTD 99 )
    set( STRICT_CPPSTD 14 )
  elseif ( NCRYSTAL_BUILD_STRICT STREQUAL "17" )
    set( STRICT_CSTD 11 )
    set( STRICT_CPPSTD 17 )
  elseif ( NCRYSTAL_BUILD_STRICT STREQUAL "20" )
    if( "${CMAKE_VERSION}" VERSION_LESS "3.21" )
      message(FATAL_ERROR "NCRYSTAL_BUILD_STRICT=20 requires cmake 3.21")
    endif()
    set( STRICT_CSTD 17 )#needs cmake 3.21
    set( STRICT_CPPSTD 20 )
  elseif ( NCRYSTAL_BUILD_STRICT STREQUAL "23" )
    if( "${CMAKE_VERSION}" VERSION_LESS "3.21" )
      message(FATAL_ERROR "NCRYSTAL_BUILD_STRICT=23 requires cmake 3.21")
    endif()
    set( STRICT_CSTD 17 )#needs cmake 3.21
    set( STRICT_CPPSTD 23 )#needs cmake 3.20
  else()
    #should have been caught earlier:
    message( FATAL_ERROR "Unexpected value of NCRYSTAL_BUILD_STRICT" )
  endif()
endif()

set( ncrystal_extra_private_compile_options "" )
if ( "x${CMAKE_CXX_COMPILER_ID}" STREQUAL "xIntelLLVM" )
  #Intel defaults to the equivalent of -ffast-math, revert back to proper math:
  set( ncrystal_extra_private_compile_options -fp-model=precise )
elseif( "${CMAKE_VERSION}" VERSION_LESS "3.20" AND "x${CMAKE_CXX_COMPILER_ID}" STREQUAL "xClang" )
  #Older cmake had the llvm-based intel compiler classified as "Clang". In
  #this case, we check whether or not the -fp-model=precise flag is supported
  #or not:
  check_cxx_compiler_flag( -fp-model=precise ncrystal_compiler_supports_fpmodelprecise )
  if ( ncrystal_compiler_supports_fpmodelprecise )
    set( ncrystal_extra_private_compile_options -fp-model=precise )
  endif()
endif()

function(set_target_common_props targetname)
  #Set private non-transitive properties. If strict builds are enabled, this can
  #enforce no warnings and compilation with a specific standards.
  if ( NCRYSTAL_BUILD_STRICT AND ncrystal_compiler_supports_strict_comp_flags )
    target_compile_options( ${targetname} PRIVATE ${NC_STRICT_COMP_FLAGS} )
  endif()
  if ( STRICT_CPPSTD )
    set_target_properties( ${targetname} PROPERTIES CXX_STANDARD ${STRICT_CPPSTD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)
  endif()
  if ( STRICT_CSTD )
    set_target_properties( ${targetname} PROPERTIES C_STANDARD ${STRICT_CSTD} C_STANDARD_REQUIRED ON C_EXTENSIONS OFF)
  endif()
  #Always disallow M_PI and friends in our own code (they are not portable), but
  #do not add to ncapi.h to avoid introducing issues in user code:
  target_compile_definitions( ${targetname} PRIVATE NCRYSTAL_NO_CMATH_CONSTANTS )
  if ( ncrystal_extra_private_compile_options )
    target_compile_options( ${targetname} PRIVATE ${ncrystal_extra_private_compile_options} )
  endif()
endfunction()

#Can we use -fno-math-errno when building the NCrystal library?
check_cxx_compiler_flag( -fno-math-errno ncrystal_compiler_supports_nomatherrno_flag )

set( ncrystal_threads_lib OFF )
if ( NOT NCRYSTAL_ENABLE_THREADS STREQUAL "OFF" )
  #set(THREADS_PREFER_PTHREAD_FLAG ON)
  find_package(Threads)
  if( Threads_FOUND )
    set( ncrystal_threads_lib "Threads::Threads" )
  else()
    if ( NOT NCRYSTAL_ENABLE_THREADS STREQUAL "IFAVAILABLE" )
      message( FATAL_ERROR "NCRYSTAL_ENABLE_THREADS set to ON but failed to enable thread support (set to IFAVAILABLE or OFF to proceed without it)." )
    endif()
  endif()
endif()

#NCrystal library and header files, including optional built-in modules if enabled:
add_library( NCrystal SHARED ${ncpkg_all_ncrystal_lib_srcfiles} )

#Make sure client code will use at least c++11:
target_compile_features( NCrystal INTERFACE cxx_std_11 )

if ( ncrystal_compiler_supports_nomatherrno_flag )
  target_compile_options( NCrystal PRIVATE -fno-math-errno )
endif()

if ( ncrystal_threads_lib )
  message( STATUS "Thread support will be enabled.")
  set( _ncrystal_actual_enable_threads ON )
  target_link_libraries( NCrystal PRIVATE ${ncrystal_threads_lib} )
else()
  message( STATUS "Thread support will not be enabled.")
  set( _ncrystal_actual_enable_threads OFF )
  target_compile_definitions( NCrystal PRIVATE NCRYSTAL_DISABLE_THREADS )
endif()

#Libname:
set( _tmp_basiclibname "NCrystal" )
if ( NCRYSTAL_NAMESPACE )
  set( _tmp_basiclibname "${_tmp_basiclibname}-${NCRYSTAL_NAMESPACE}" )
endif()
set_target_properties( NCrystal PROPERTIES OUTPUT_NAME "${_tmp_basiclibname}" )

#Export all symbols on windows if requested:
if ( WIN32 AND NCRYSTAL_WINEXPORTALL )
  #We must export all symbols in libNCrystal.so, even those from internal
  #headers - since we also use internal files in our tests and plugins. We use
  #NCRYSTAL_PREVENT_WINDLLEXPORT (in ncrystal_ncapi.cmake) to avoid explicitly
  #exporting any symbols in this case, since that triggers spurious warnings
  #from VisualStudio.
  set_target_properties( NCrystal PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS TRUE )
endif()

# Dynamic library loading:
if ( NCRYSTAL_ENABLE_DYNLOAD )
  if ( UNIX AND CMAKE_DL_LIBS )
    #Cf. https://gitlab.kitware.com/cmake/cmake/-/merge_requests/1642 for why we
    #only do this on UNIX.
    target_link_libraries( NCrystal PRIVATE ${CMAKE_DL_LIBS} )
  endif()
else()
  target_compile_definitions( NCrystal PRIVATE NCRYSTAL_DISABLE_DYNLOAD )
endif()

set_target_common_props( NCrystal )
target_link_libraries( NCrystal PRIVATE ncrystal_common )
target_include_directories(
  NCrystal
  PRIVATE "${PROJECT_SOURCE_DIR}/src"
  PUBLIC
  $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
  $<BUILD_INTERFACE:${ncapi_include_path}>
  $<INSTALL_INTERFACE:${NCrystal_INCDIR}>
)

#Make sure we link in math functions correctly (typically the linker needs libm on unix, but nothing on Windows).
set(TMP_TESTLIBMSRC "#include <math.h>\nint main(int argc,char** argv) { (void)argv;double a=(exp)(argc+1.0); return (int)(a*0.1); }\n")
set(TMP_TESTDIR ${PROJECT_BINARY_DIR}/test_libm)
file(WRITE ${TMP_TESTDIR}/test.c "${TMP_TESTLIBMSRC}")
try_compile(ALWAYS_HAS_MATH "${TMP_TESTDIR}" "${TMP_TESTDIR}/test.c")
if (NOT ALWAYS_HAS_MATH)
  set(TMP_TESTDIR ${PROJECT_BINARY_DIR}/test_libm2)
  file(WRITE ${TMP_TESTDIR}/test.c "${TMP_TESTLIBMSRC}")
  try_compile(MATH_NEEDS_LIBM "${TMP_TESTDIR}" "${TMP_TESTDIR}/test.c" LINK_LIBRARIES m)
  if (MATH_NEEDS_LIBM)
    target_link_libraries(NCrystal PRIVATE m)
  else()
    message( FATAL_ERROR "Could not figure out link flags needed to enable math functions" )
  endif()
endif()

if ( NCRYSTAL_ENABLE_DATA STREQUAL "EMBED" )
  #Embed data (needs to invoke python process to generate C++ code from .ncmat files)

  target_compile_definitions(NCrystal PRIVATE NCRYSTAL_STDCMAKECFG_EMBED_DATA_ON)

  set( ncmatcc_bn "autogen_ncmat_data.cc" )
  set( ncmatcc_file "${PROJECT_BINARY_DIR}/${ncmatcc_bn}" )
  set( ncmatcc_file_needs_update ON )
  if ( DEFINED SKBUILD_PROJECT_NAME )
    #Ensure the file gets packaged up in the sdist (that way we do not need data
    #files and _ncmat2cpp_impl.py to be in subdirs of ncrystal_core).
    set( ncmatcc_file "${ncrystal_skbld_autogendir}/${ncmatcc_bn}" )
    if ( EXISTS ${ncmatcc_file} AND NOT "${SKBUILD_STATE}" STREQUAL "sdist" )
      message( STATUS "Using pregenerated ${ncmatcc_bn} with embedded NCMAT data" )
      set( ncmatcc_file_needs_update OFF )
      #Note: This can in principle fail if the generated .cc file is older than
      #the source files. However, checking for this is a bit complicated since
      #the data files might be absent. In any case, we only ever set
      #ncmatcc_file_needs_update to off in skbuild mode, so this is unlikely to
      #come up.
    else()
      message(STATUS "created ${ncrystal_skbld_autogendir}" )
    endif()
  endif()
  if ( ncmatcc_file_needs_update )
    #We must find python3 interpreter.
    message( STATUS "Looking for python interpreter (needed when NCRYSTAL_ENABLE_DATA=EMBED).")
    find_package(Python3 3.8 REQUIRED COMPONENTS Interpreter)
    #Generate C++ code from the .ncmat files:
    get_datafile_list( data_file_list )
    set( ncmat2cpppy "${PROJECT_SOURCE_DIR}/../ncrystal_python/src/NCrystal/_ncmat2cpp_impl.py" )
    if ( NOT EXISTS "${ncmat2cpppy}" )
      message( FATAL_ERROR "Missing file: ${ncmat2cpppy}" )
    endif()
    execute_process(
      COMMAND "${Python3_EXECUTABLE}" "-BI" "${ncmat2cpppy}"
      "-n" "NCrystal::AutoGenNCMAT::registerStdNCMAT" "--regfctname"
      "NCrystal::internal::registerEmbeddedNCMAT(const char*,const char*)"
      "-o" "${ncmatcc_file}" ${data_file_list} RESULT_VARIABLE status
    )
    if(status AND NOT status EQUAL 0)
      message(FATAL_ERROR "Failure while trying to invoke"
        " ncmat2cpp (needed since NCRYSTAL_ENABLE_DATA=EMBED).")
    endif()
    message(STATUS "Generated ${ncmatcc_bn} with embedded NCMAT data (will be compiled into the NCrystal library)." )
  endif()
  target_sources(NCrystal PRIVATE "${ncmatcc_file}")
elseif( NCRYSTAL_ENABLE_DATA STREQUAL "ON" )
  #Hardwiring NCRYSTAL_DATADIR in the binary, although handled with
  #NCRYSTAL_DATADIR env var in ncrystal_setup.sh. The environment variable makes
  #the installation relocatable (at least for users sourcing the installed
  #ncrystal_setup.sh):
  target_compile_definitions(NCrystal PRIVATE "NCRYSTAL_DATADIR=${CMAKE_INSTALL_PREFIX}/${NCrystal_DATAFILESDIR}")
  get_datafile_list( DATAFILES )
  ncinstall(FILES ${DATAFILES} DESTINATION ${NCrystal_DATAFILESDIR})
endif()

ncinstall(
  TARGETS NCrystal
  EXPORT NCrystalTargets
  RUNTIME DESTINATION ${NCrystal_SHLIBDIR}
  ARCHIVE DESTINATION ${NCrystal_LIBDIR}
  LIBRARY DESTINATION ${NCrystal_LIBDIR}
)

#Examples:
if ( NCRYSTAL_ENABLE_EXAMPLES )
  file_globsrc( EXAMPLES_NC "../examples/ncrystal_example_c*.c*")
  foreach(ex ${EXAMPLES_NC})
    get_filename_component(exbn "${ex}" NAME_WE)
    add_executable(${exbn} "${ex}")
    set_target_common_props( ${exbn} )
    target_link_libraries(${exbn} NCrystal ncrystal_common )
    if ( NOT "x${_nc_executable_rpath}" STREQUAL "x" )
      set_target_properties(${exbn} PROPERTIES INSTALL_RPATH "${_nc_executable_rpath}" )
    endif()
    ncinstall(TARGETS ${exbn} DESTINATION ${NCrystal_BINDIR} )
  endforeach()
endif()

#Package configuration files for downstream cmake projects:
ncinstall( EXPORT NCrystalTargets FILE "NCrystalTargets.cmake" NAMESPACE NCrystal:: DESTINATION ${NCrystal_CMAKEDIR} )
add_library(NCrystal::NCrystal ALIAS NCrystal)#always alias namespaces locally

if ( ncrystal_skbuild_shlib_in_wheel_scripts )
  #In python wheels on windows we put NCrystal.dll in the scripts directory,
  #which moves around upon installation. To make the imported NCrystal::NCrystal
  #target work, we need to fix the library location at the end of the generated
  #NCrystalTargets-*.cmake. We put those fixups in a function in
  #NCrystalConfig.cmake, and simply add a function call here:
  install(CODE "
file(
  APPEND
  \"${CMAKE_INSTALL_PREFIX}/${NCrystal_CMAKEDIR}/NCrystalTargets-$<LOWER_CASE:$<CONFIG>>.cmake\"
  \"\n_ncrystal_fixup_ncrystaltargets()\n\"
)
")
endif()

#NB: Important we do any configure_file AFTER all variables used have been set!
include(CMakePackageConfigHelpers)
#NB: We choose COMPATIBILITY AnyNewerVersion, since we mostly aim for API (but
#not ABI) compatiblity even when bumping major versions. Otherwise client code
#would break whenever we update major versions:
write_basic_package_version_file(
  "${PROJECT_BINARY_DIR}/NCrystalConfigVersion.cmake"
  VERSION ${NCrystal_VERSION} COMPATIBILITY AnyNewerVersion
)

#First generated a configured version of the template:
configure_file(
  "${PROJECT_SOURCE_DIR}/cmake/NCrystalConfig.cmake.in"
  "${PROJECT_BINARY_DIR}/NCrystalConfig.cmake.pregen.in" @ONLY
)

#At build-time, generate the actual files needed for inclusion, expanding any
#generator-expressions:
file(
  GENERATE
  OUTPUT "${PROJECT_BINARY_DIR}/NCrystalConfig.cmake"
  INPUT "${PROJECT_BINARY_DIR}/NCrystalConfig.cmake.pregen.in"
)

ncinstall(
  FILES "${PROJECT_BINARY_DIR}/NCrystalConfigVersion.cmake"
  "${PROJECT_BINARY_DIR}/NCrystalConfig.cmake"
  DESTINATION ${NCrystal_CMAKEDIR}
)

set( nccfg_expects_shlibdir_override OFF )
if ( DEFINED SKBUILD_PROJECT_NAME )
  #Only if installed via scikit-build will we generate an appropriate
  #_ncrystal_core Python module for installation. We will also copy over some
  #metadata files which resides outside <reporoot>/ncrystal_core for inclusion
  #in the sdist. We always regenerate this, even when running from sdist.
  set( pymoddir "${ncrystal_skbld_autogendir}/${nccore_pymodname}" )
  if ( IS_DIRECTORY ${pymoddir} )
    file ( REMOVE_RECURSE ${pymoddir} )
  endif()
  file( MAKE_DIRECTORY "${pymoddir}" )
  file( WRITE "${pymoddir}/__init__.py" "__version__='${NCrystal_VERSION}'\n" )
  #First generated a configured version of the template:
  if ( ncrystal_skbuild_shlib_in_wheel_scripts )
    #dll is over in the wheel scripts dir, at an unknown relative location.
    set( ncrystal_pymod_shlib_in_scripts "1" )#non-empty string
    set( ncrystal_pymod_shlib_dirname "" )
    set( nccfg_expects_shlibdir_override ON )
  else()
    set( ncrystal_pymod_shlib_in_scripts "" )#empty string
    get_filename_component(
      "ncrystal_pymod_shlib_dirname" "${NCrystal_SHLIBDIR}" NAME
    )
  endif()

  configure_file(
    "${PROJECT_SOURCE_DIR}/cmake/template_pymod.py.in"
    "${PROJECT_BINARY_DIR}/template_pymod.py.pregen.in"
    @ONLY
  )
  #At build-time, generate the actual files needed for inclusion, expanding any
  #generator-expressions:
  file( GENERATE
    OUTPUT "${pymoddir}/info.py"
    INPUT "${PROJECT_BINARY_DIR}/template_pymod.py.pregen.in"
  )
endif()

if ( NCRYSTAL_ENABLE_CFGAPP )
  include( "ncrystal_nccfgapp" )
  create_ncrystal_config_app( ${nccfg_expects_shlibdir_override} )
endif()

if ( NOT NCRYSTAL_QUIET )
  foreach( optname ${_NCrystal_all_opts} )
    message( STATUS "NCrystal-cfg: ${optname}=${${optname}}" )
  endforeach()
endif()
