# This software was produced by NIST, an agency of the U.S. government,
# and by statute is not subject to copyright in the United States.
# Recipients of this software assume all responsibilities associated
# with its operation, modification and maintenance. However, to
# facilitate maintenance we ask that before distributing modifed
# versions of this software, you first contact the authors at
# oof_manager@nist.gov.

cmake_minimum_required(VERSION 3.18)
project(oof2 VERSION 2.3.1)

set(OOF2_SWIG_VERSION 4.1 CACHE STRING "Use this version of swig")
set_property(CACHE OOF2_SWIG_VERSION PROPERTY STRINGS 4.0 4.1)

# The initial value of OOF2_PYTHON3_VERSION is "Latest" so that the
# initial configuration pass doesn't raise an error if the requested
# version isn't found.
## TODO: Can we list only the available versions of swig and python?
set(OOF2_PYTHON3_VERSION "Latest" CACHE STRING "Use this version of Python")
set_property(CACHE OOF2_PYTHON3_VERSION PROPERTY STRINGS
  3.8 3.9 3.10 3.11 Latest)

# If OOF2 is being installed by a regular user, the python files
# should be installed into lib/pythonX.Y/site-packages/oofcanvas,
# which is probably in /usr/local or the user's home directory.  But
# if it's being installed by MacPorts or another package manager they
# should be installed in
# ${Python3_LIBRARY_DIRS}/pythonX.Y/site-packages.  The package
# manager should set OOF2_SYSTEM_INSTALL to ON, which will cause
# the files to be installed under Python3_LIBRARY_DIRS.
option(OOF2_SYSTEM_INSTALL OFF "Install in system directory?")
mark_as_advanced(OOF2_SYSTEM_INSTALL)

include(GNUInstallDirs)

## TODO PYTHON3: Get openMP working. See the openmp-branch branch.

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# OOF2/SRC subdirectories containing python files to install.  The
# entire hierarchies under these will be installed in
# .../site-packages/oof2/ooflib.

set(oofdirs
  common
  engine
  image
  orientationmap
  tutorials
  EXTENSIONS
  )

# OOF2 libraries to build

set(ooflibs
  oof2common
  oof2commonGUI
  oof2engine
  oof2image
  oof2orientationmap
  )

# executable python scripts to install

set(oofscripts
  oof2
  oof2-test
  oof2-guitest
  )

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Required version numbers for dependencies

set(OOFCANVAS_MIN_VERSION 1.1.1)
set(MAGICK_MIN_VERSION 6.0.0)
set(MAGICK_MAX_VERSION 7.0.0)
set(GTK3_MIN_VERSION 3.22)

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Call swig_sources in the CMakeLists.txt files in all subdirectories
# that contain swig files.
#  swig_sources(
#        SWIGFILES  a b      # .swg suffix is assumed
#     optional arguments
#        LIBRARIES  <names of libraries to link to, must be cmake targets>
#        CFLAGS <additional compiler options>)
#        INCLUDE_DIRECTORIES <additional directories to search for C++ headers>
#        TARGET_SFX <suffix to use for the cmake target.  See below>
#  )

function(swig_sources)
  set(multiValueArgs SWIGFILES LIBRARIES CFLAGS INCLUDE_DIRECTORIES TARGET_SFX)
  cmake_parse_arguments(PARSE_ARGV 0 SS "" "" "${multiValueArgs}")
  #                                  ^ "SS" for Swig_Sources

  foreach(swigfile ${SS_SWIGFILES})
    set_property(
      SOURCE ${swigfile}.swg
      PROPERTY CPLUSPLUS ON)
    # Set include directories for the swig command.
    set_property(
      SOURCE ${swigfile}.swg
      PROPERTY INCLUDE_DIRECTORIES
      ${CMAKE_CURRENT_SOURCE_DIR}
      ${PROJECT_SOURCE_DIR}/SRC
      ${SS_INCLUDE_DIRECTORIES})
    # Set include directories for the compiler
    set_property(
      SOURCE ${swigfile}.swg
      PROPERTY GENERATED_INCLUDE_DIRECTORIES
      ${SS_INCLUDE_DIRECTORIES}
      ${CMAKE_CURRENT_SOURCE_DIR}
      ${PROJECT_SOURCE_DIR}/SRC
      ${PROJECT_BINARY_DIR}	# for headers generated by configure_file
      ${PYINCL}
      ${Python3_INCLUDE_DIRS})
    set_property(
      SOURCE ${swigfile}.swg
      PROPERTY GENERATED_COMPILE_OPTIONS
      "${SS_CFLAGS}"
      "${OOFCANVAS_CFLAGS}"
    )

    if(SS_TARGET_SFX)
      # If the TARGET_SFX argument is supplied, append it to
      # ${swigfile} to create the target name, although the installed
      # name will still be ${swigfile}.  This allows us to build two
      # modules with the same names but different locations, despite
      # the fact that cmake requires all target names to be unique.
      set(TARGET ${swigfile}${SS_TARGET_SFX}) # create a unique target name
      # set_property(			      # but don't use it for the files
      # 	SOURCE ${swigfile}.swg
      # 	PROPERTY OUTPUT_NAME
      # 	${swigfile})	 # *not* ${TARGET}, which has the suffix added
    else()
      set(TARGET ${swigfile})
    endif()

    swig_add_library(
      ${TARGET}
      TYPE MODULE
      LANGUAGE PYTHON
      SOURCES ${swigfile}.swg)

    target_link_libraries(
      ${TARGET}
      PRIVATE
      ${Python3_LIBRARIES}
      ${SS_LIBRARIES}
      ${OOFCANVAS_LINK_LIBRARIES}
      )

    # Get the path from the top of the source directory hierarchy to
    # the current directory.  This is the path from the top of the
    # installation directory hierarchy to the installation directory
    # for the compiled swig output and python file.

    # file(RELATIVE_PATH ...) has been superseded by cmake_path(...)
    # in cmake 3.20, but 3.20 isn't available on Ubuntu 20.04.
    if(${CMAKE_VERSION} VERSION_LESS "3.20")
      file(
	RELATIVE_PATH relpath	     # this is the path ...
	${PROJECT_SOURCE_DIR}/SRC    # ... from here ...
	${CMAKE_CURRENT_SOURCE_DIR}) # ... to here
    else()
      set(relpath ${CMAKE_CURRENT_SOURCE_DIR})
      cmake_path(
	RELATIVE_PATH
	relpath			# Change this path ...
	BASE_DIRECTORY ${PROJECT_SOURCE_DIR}/SRC) # ... to be relative to this
    endif()
    # Install the swig-generated and compiled library
    install(
      TARGETS ${TARGET}
      DESTINATION ${PYDEST}/ooflib/SWIG/${relpath})
    # Install the swig-generated python file
    install(
      FILES ${CMAKE_CURRENT_BINARY_DIR}/${swigfile}.py
      DESTINATION ${PYDEST}/ooflib/SWIG/${relpath})
  endforeach()

endfunction()

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Set the build type.

# These are the build types that we support:
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release)

# Set the default build type.  See
# https://www.kitware.com/cmake-and-the-default-build-type/
set(default_build_type "Release")
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE
    STRING "Debug or Release" FORCE)
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release")
endif()

# Apparently using -DDEBUG for debugging is not the modern
# convention. CMake instead assumes that we're using -DNDEBUG when not
# debugging, so add -DDEBUG here.
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Set C++ version
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS False)	# use -std=c++11 instead of -std=gnu++11
set(CMAKE_CXX_STANDARD_REQUIRED True) # don't fall back to an earlier standard

set(BUILD_SHARED_LIBS ON)

# On macOS 12, we need to set RPATH or libraries installed outside the
# default locations won't be found.
set(CMAKE_INSTALL_RPATH ${CMAKE_INSTALL_PREFIX}/lib)

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Find dependencies

# Find Python3.

# Without this line, cmake finds the system python on Mac even if
# MacPorts is installed.  With this line, it finds MacPorts' python,
# whether the argument is FIRST or LAST. This confuses me.  If
# Python_FIND_FRAMEWORK is used instead, it seems to always find the
# system python.
set(CMAKE_FIND_FRAMEWORK LAST)

include(FindPython)
if(${OOF2_PYTHON3_VERSION} STREQUAL "Latest")
  find_package(Python3 COMPONENTS Interpreter Development)
else()
  find_package(
    Python3 ${OOF2_PYTHON3_VERSION} EXACT
    COMPONENTS Interpreter Development)
endif()
set(PYVERSION ${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR})

# message("Python3 is ${Python3_EXECUTABLE}")
# message("Python3_INCLUDE_DIRS is ${Python3_INCLUDE_DIRS}")
# message("Python3_LIBRARIES is ${Python3_LIBRARIES}")
# message("Python3_LIBRARY_DIRS is ${Python3_LIBRARY_DIRS}")
# message("Python3_RUNTIME_LIBRARY_DIRS is ${Python3_RUNTIME_LIBRARY_DIRS}")
# message("Python3_SITELIB is ${Python3_SITELIB}")

# SITE_PACKAGES is used to set the path in oof2.in, oof2-test.in and
# oof2-guitest.in so that users can run the top level scripts without
# setting PYTHONPATH.
set(PYLIBPATH python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages)
if(OOF2_SYSTEM_INSTALL)
  set(SITE_PACKAGES ${Python3_LIBRARY_DIRS}/${PYLIBPATH})
else()
  set(SITE_PACKAGES lib/${PYLIBPATH})
endif()
# PYDEST is the destination for installing OOF2 python files.
set(PYDEST ${SITE_PACKAGES}/${CMAKE_PROJECT_NAME})

# message("PYDEST is ${PYDEST}")
# message("SITE_PACKAGES is ${SITE_PACKAGES}")

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

include(FindBLAS)

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

include(FindSWIG)
include(UseSWIG)
find_package(SWIG ${OOF2_SWIG_VERSION} COMPONENTS python)
## UseSWIG can generate dependencies only for cmake >= 3.20.  Setting
## the flag for it is harmless even if it doesn't always work.
set(SWIG_USE_SWIG_DEPENDENCIES True)
set(UseSWIG_TARGET_NAME_PREFERENCE STANDARD)
set(SWIG_SOURCE_FILE_EXTENSIONS ".swg" ".i")
if(${SWIG_VERSION} VERSION_LESS "4.1")
  set(CMAKE_SWIG_FLAGS -py3)
endif()
# add_compile_definitions(SWIG_TYPE_TABLE=oof2)
if(${SWIG_USE_BUILTIN})		# wishful thinking
  list(APPEND CMAKE_SWIG_FLAGS -builtin)
endif()
# UseSWIG doesn't seem to use -DNDEBUG or -DDEBUG when calling
# swig.  This has to be set with a generator expression because
# CMAKE_BUILD_TYPE can't be evaluated reliably at this time.
list(APPEND CMAKE_SWIG_FLAGS $<$<CONFIG:Debug>:-DDEBUG>)

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Use pkg-config to get info about dependencies
include(FindPkgConfig)
# Update the pkgconfig search path
# MacPorts puts some pkg-config files in the python library
if(APPLE)
  set(ENV{PKG_CONFIG_PATH}
    "$ENV{PKG_CONFIG_PATH}:${Python3_LIBRARY_DIRS}/pkgconfig")
endif()
# Look in the anaconda directory if necessary
if($ENV{CONDA_PREFIX})
  set(ENV{PKG_CONFIG_PATH}
    "$ENV{PKG_CONFIG_PATH}:$ENV{CONDA_PREFIX}/lib/pkgconfig")
endif()

# TODO: OOFCanvas was probably installed with the same prefix, so it
# would be nice to add CMAKE_INSTALL_PREFIX/lib/pkgconfig to
# PKG_CONFIG_PATH here, and not require users to set PKG_CONFIG_PATH
# themselves.  But when ccmake opens this file for the first time,
# CMAKE_INSTALL_PREFIX is not set, and so oofcanvas is not found.  I
# don't know how to delay the pkg_check_modules call until after
# CMAKE_INSTALL_PREFIX is set.
#   if(EXISTS ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig/oofcanvas.pc})
#     set(ENV{PKG_CONFIG_PATH}
#       "$ENV{PKG_CONFIG_PATH}:${CMAKE_INSTALL_PREFIX}/lib/pkgconfig")
#   endif()

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

pkg_check_modules(
  OOFCANVAS REQUIRED
  oofcanvas>=${OOFCANVAS_MIN_VERSION})
list(APPEND CMAKE_INSTALL_RPATH ${OOFCANVAS_LIBDIR})

## Dump all variables
# get_cmake_property(_variableNames VARIABLES)
# list (SORT _variableNames)
# foreach (_variableName ${_variableNames})
#     message(STATUS "${_variableName}=${${_variableName}}")
# endforeach()

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

pkg_check_modules(
  MAGICK REQUIRED
  Magick\+\+>=${MAGICK_MIN_VERSION}
  Magick\+\+<${MAGICK_MAX_VERSION})

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

pkg_check_modules(
  GTK3 REQUIRED
  gtk+-3.0>=${GTK3_MIN_VERSION})

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

include(TestForSSTREAM)		# sets CMAKE_NO_ANSI_STRING_STREAM
## TODO: Is there a non-stupid way of setting HAVE_SSTREAM?
if(NOT ${CMAKE_NO_ANSI_STRING_STREAM})
  set(HAVE_SSTREAM True)
endif()

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Create the shared library targets, which are listed in the ooflibs
# macro defined above.

foreach(olib ${ooflibs})
  add_library(${olib} SHARED)
  target_include_directories(${olib}
    PRIVATE
    ${PROJECT_BINARY_DIR}
    ${CMAKE_CURRENT_BINARY_DIR}
    ${PROJECT_SOURCE_DIR}/SRC
    ## TODO: Don't add all include dirs here. Use only as required by
    ## setting property INCLUDE_DIRECTORIES on source files
    ${Python3_INCLUDE_DIRS} 
    ${OOFCANVAS_INCLUDE_DIRS}
    ${MAGICK_INCLUDE_DIRS}
    ${GTK3_INCLUDE_DIRS}
    )
  # It's necessary to include OOFCANVAS_CFLAGS for all files that
  # include oofcanvas.h, because oofcanvas.h indirectly includes
  # ImageMagick headers, and ImageMagick complains bitterly if
  # MAGICKCORE_QUANTUM_DEPTH isn't defined.  OOFCANVAS_CFLAGS includes
  # the ImageMagick preprocessor definitions that it used.  This
  # results in the ImageMagick definitions being used in far more
  # places than actually necessary, which is messy but probably
  # harmless.
  target_compile_options(${olib}
    PRIVATE
    -Wno-deprecated-register
    ${OOFCANVAS_CFLAGS})
endforeach()

configure_file(
  ${PROJECT_SOURCE_DIR}/oofconfig.h.in
  ${PROJECT_BINARY_DIR}/oofconfig.h
  @ONLY
  )

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Create swigruntime.h by running "swig -external-runtime". The
# dependence of oofswigruntime.h on swigruntime.h is set explicitly in
# SRC/common/CMakeLists.txt.

add_custom_command(
  OUTPUT swigruntime.h
  COMMAND ${SWIG_EXECUTABLE} -python -external-runtime swigruntime.h
)

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

foreach(script ${oofscripts})
  configure_file(
  ${PROJECT_SOURCE_DIR}/${script}.in
  ${PROJECT_BINARY_DIR}/${script}
  @ONLY
  )
endforeach()

target_link_libraries(oof2common
  PRIVATE
  ${Python3_LIBRARIES}
  ${OOFCANVAS_LINK_LIBRARIES}
  ${BLAS_LIBRARIES}
  )

target_link_libraries(oof2commonGUI
  PRIVATE
  ${Python3_LIBRARIES}
  ${OOFCANVAS_LINK_LIBRARIES}
  oof2common
  )

target_link_libraries(oof2engine
  PRIVATE
  ${Python3_LIBRARIES}
  ${OOFCANVAS_LINK_LIBRARIES}
  ${BLAS_LIBRARIES}
  oof2common
  )

target_link_libraries(oof2image
  PRIVATE
  ${Python3_LIBRARIES}
  ${OOFCANVAS_LINK_LIBRARIES}
  oof2common
  ${MAGICK_LINK_LIBRARIES}
  )

target_link_libraries(oof2orientationmap
  PRIVATE
  ${Python3_LIBRARIES}
  ${OOFCANVAS_LINK_LIBRARIES}
  oof2common
  oof2engine
  oof2image
  ${MAGICK_LINK_LIBRARIES}
  )
  
add_subdirectory(SRC)

# Install compiled libraries

install(
  TARGETS
  oof2common
  oof2commonGUI
  oof2engine
  oof2image
  oof2orientationmap
  DESTINATION ${CMAKE_INSTALL_LIBDIR})

# Install python files from SRC subdirectories, excluding SWIG output

foreach(pydir ${oofdirs})
  install(DIRECTORY SRC/${pydir}
    DESTINATION ${PYDEST}/ooflib/
    FILES_MATCHING
    PATTERN "*.py"
    PATTERN EXTRA EXCLUDE
    )
endforeach()

# Create and install the top level __init__.py files. 

file(TOUCH ${PROJECT_BINARY_DIR}/__init__.py)

install(
  FILES
  ${PROJECT_BINARY_DIR}/__init__.py
  DESTINATION ${PYDEST}
  )

install(
  FILES
  ${PROJECT_BINARY_DIR}/__init__.py
  DESTINATION ${PYDEST}/ooflib
  )
  
# Install the start up and test wrapper scripts

install(
  PROGRAMS
  ${CMAKE_BINARY_DIR}/oof2
  ${CMAKE_BINARY_DIR}/oof2-test
  ${CMAKE_BINARY_DIR}/oof2-guitest
  DESTINATION ${CMAKE_INSTALL_BINDIR})

# Install the TEST and GUITEST directories and their data

install(
  DIRECTORY TEST
  DESTINATION ${PYDEST}
  ## TODO: This will possibly install too much.  Use patterns or exclusions.
  # FILES_MATCHING 
  # PATTERN "*.py"
  # PATTERN "*.dat"
  )

# Install examples into <prefix>/share/oof2/examples
install(
  DIRECTORY examples
  DESTINATION ${CMAKE_INSTALL_DATADIR}/oof2
)

# Packaging

# Don't use cmake's packaging tools because I couldn't get them to
# work and we have a perfectly good make_dist script from the old
# days.  But in order to ensure that the version number used by the
# script is the same as the version number defined here, the script is
# created from make_dist.in.  Check that the file exists because
# make_dist.in isn't itself included in the distribution.

if(EXISTS "${PROJECT_SOURCE_DIR}/make_dist.in")
  configure_file(
    ${PROJECT_SOURCE_DIR}/make_dist.in
    ${PROJECT_BINARY_DIR}/make_dist
    @ONLY
  )
endif()

#=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=##=--=#

# Add an "uninstall" target.  Copied from
# https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#can-i-do-make-uninstall-with-cmake

if(NOT TARGET uninstall)
  configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
    IMMEDIATE @ONLY)

  add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
endif()
