# Check Cython version, need to be >=0.25.2
# Unset these cached variables to avoid surprises when the python/cython
# in the current environment are different from the cached!
unset(PYTHON_EXECUTABLE CACHE)
unset(CYTHON_EXECUTABLE CACHE)
unset(PYTHON_INCLUDE_DIR CACHE)
unset(PYTHON_MAJOR_VERSION CACHE)
unset(PYTHON_LIBRARY CACHE)

# Allow override from command line
if(NOT DEFINED GTSAM_USE_CUSTOM_PYTHON_LIBRARY)
  if(GTSAM_PYTHON_VERSION STREQUAL "Default")
    find_package(PythonInterp REQUIRED)
    find_package(PythonLibs REQUIRED)
  else()
    find_package(PythonInterp ${GTSAM_PYTHON_VERSION} EXACT REQUIRED)
    find_package(PythonLibs ${GTSAM_PYTHON_VERSION} EXACT REQUIRED)
  endif()
endif()
find_package(Cython 0.25.2 REQUIRED)

execute_process(COMMAND "${PYTHON_EXECUTABLE}" "-c"
    "from __future__ import print_function;import sys;print(sys.version[0], end='')"
    OUTPUT_VARIABLE PYTHON_MAJOR_VERSION
)

# User-friendly Cython wrapping and installing function.
# Builds a Cython module from the provided interface_header.
# For example, for the interface header gtsam.h,
# this will build the wrap module 'gtsam'.
#
# Arguments:
#
# interface_header:  The relative path to the wrapper interface definition file.
# extra_imports: extra header to import in the Cython pxd file.
#                For example, to use Cython gtsam.pxd in your own module,
#        use "from gtsam cimport *"
# install_path: destination to install the library
# libs: libraries to link with
# dependencies: Dependencies which need to be built before the wrapper
function(wrap_and_install_library_cython interface_header extra_imports install_path libs dependencies)
  # Paths for generated files
  get_filename_component(module_name "${interface_header}" NAME_WE)
  set(generated_files_path "${install_path}")
  wrap_library_cython("${interface_header}" "${generated_files_path}" "${extra_imports}" "${libs}" "${dependencies}")
endfunction()

function(set_up_required_cython_packages)
  # Set up building of cython module
  include_directories(${PYTHON_INCLUDE_DIRS})
  find_package(NumPy REQUIRED)
  include_directories(${NUMPY_INCLUDE_DIRS})
endfunction()


# Convert pyx to cpp by executing cython
# This is the first step to compile cython from the command line
# as described at: http://cython.readthedocs.io/en/latest/src/reference/compilation.html
#
# Arguments:
#    - target:  The specified target for this step
#    - pyx_file:   The input pyx_file in full *absolute* path
#    - generated_cpp:   The output cpp file in full absolute path
#    - include_dirs:   Directories to include when executing cython
function(pyx_to_cpp target pyx_file generated_cpp include_dirs)
  foreach(dir ${include_dirs})
    set(includes_for_cython ${includes_for_cython}  -I ${dir})
  endforeach()

  add_custom_command(
    OUTPUT ${generated_cpp}
    COMMAND
    ${CYTHON_EXECUTABLE} -X boundscheck=False -v --fast-fail --cplus -${PYTHON_MAJOR_VERSION} ${includes_for_cython} ${pyx_file} -o ${generated_cpp}
    VERBATIM)
  add_custom_target(${target} ALL DEPENDS ${generated_cpp})
endfunction()

# Build the cpp file generated by converting pyx using cython
# This is the second step to compile cython from the command line
# as described at: http://cython.readthedocs.io/en/latest/src/reference/compilation.html
#
# Arguments:
#    - target:  The specified target for this step
#    - cpp_file:   The input cpp_file in full *absolute* path
#    - output_lib_we:   The output lib filename only (without extension)
#    - output_dir:   The output directory
function(build_cythonized_cpp target cpp_file output_lib_we output_dir)
  add_library(${target} MODULE ${cpp_file})
  
  if(WIN32)
    # Use .pyd extension instead of .dll on Windows
    set_target_properties(${target} PROPERTIES SUFFIX ".pyd")

    # Add full path to the Python library
    target_link_libraries(${target} ${PYTHON_LIBRARIES})
  endif()
  
  if(APPLE)
    set(link_flags "-undefined dynamic_lookup")
  endif()
  set_target_properties(${target}
      PROPERTIES COMPILE_FLAGS "-w"
      LINK_FLAGS "${link_flags}"
      OUTPUT_NAME ${output_lib_we}
      PREFIX ""
      ${CMAKE_BUILD_TYPE_UPPER}_POSTFIX ""
      LIBRARY_OUTPUT_DIRECTORY ${output_dir})
endfunction()

# Cythonize a pyx from the command line as described at
# http://cython.readthedocs.io/en/latest/src/reference/compilation.html
# Arguments:
#    - target:        The specified target
#    - pyx_file:      The input pyx_file in full *absolute* path
#    - output_lib_we: The output lib filename only (without extension)
#    - output_dir:    The output directory
#    - include_dirs:  Directories to include when executing cython
#    - libs:          Libraries to link with
#    - interface_header: For dependency. Any update in interface header will re-trigger cythonize
function(cythonize target pyx_file output_lib_we output_dir include_dirs libs interface_header dependencies)
  get_filename_component(pyx_path "${pyx_file}" DIRECTORY)
  get_filename_component(pyx_name "${pyx_file}" NAME_WE)
  set(generated_cpp "${output_dir}/${pyx_name}.cpp")

  set_up_required_cython_packages()
  pyx_to_cpp(${target}_pyx2cpp ${pyx_file} ${generated_cpp} "${include_dirs}")

  # Late dependency injection, to make sure this gets called whenever the interface header is updated
  # See: https://stackoverflow.com/questions/40032593/cmake-does-not-rebuild-dependent-after-prerequisite-changes
  add_custom_command(OUTPUT ${generated_cpp} DEPENDS ${interface_header} ${pyx_file} APPEND)
  if (NOT "${dependencies}" STREQUAL "")
    add_dependencies(${target}_pyx2cpp "${dependencies}")
  endif()

  build_cythonized_cpp(${target} ${generated_cpp} ${output_lib_we} ${output_dir})
  if (NOT "${libs}" STREQUAL "")
    target_link_libraries(${target} "${libs}")
  endif()
  add_dependencies(${target} ${target}_pyx2cpp)

  if(TARGET ${python_install_target})
    add_dependencies(${python_install_target} ${target})
  endif()
endfunction()

# Internal function that wraps a library and compiles the wrapper
function(wrap_library_cython interface_header generated_files_path extra_imports libs dependencies)
  # Wrap codegen interface
  # Extract module path and name from interface header file name
  # wrap requires interfacePath to be *absolute*
  get_filename_component(interface_header "${interface_header}" ABSOLUTE)
  get_filename_component(module_path "${interface_header}" PATH)
  get_filename_component(module_name "${interface_header}" NAME_WE)

  # Wrap module to Cython pyx
  message(STATUS "Cython wrapper generating ${generated_files_path}/${module_name}.pyx")
  set(generated_pyx "${generated_files_path}/${module_name}.pyx")
  if(NOT EXISTS ${generated_files_path})
    file(MAKE_DIRECTORY "${generated_files_path}")
  endif()

  add_custom_command(
    OUTPUT ${generated_pyx}
    DEPENDS ${interface_header} wrap
    COMMAND
        wrap --cython ${module_path} ${module_name} ${generated_files_path} "${extra_imports}"
    VERBATIM
    WORKING_DIRECTORY ${generated_files_path}/../)
  add_custom_target(cython_wrap_${module_name}_pyx ALL DEPENDS ${generated_pyx})
  if(NOT "${dependencies}" STREQUAL "")
    add_dependencies(cython_wrap_${module_name}_pyx ${dependencies})
  endif()

  message(STATUS "Cythonize and build ${module_name}.pyx")
  get_property(include_dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
  cythonize(cythonize_${module_name} ${generated_pyx} ${module_name}
    ${generated_files_path} "${include_dirs}" "${libs}" ${interface_header} cython_wrap_${module_name}_pyx)

  # distclean
  add_custom_target(wrap_${module_name}_cython_distclean
      COMMAND cmake -E remove_directory ${generated_files_path})
endfunction()

# Helper function to install Cython scripts and handle multiple build types where the scripts
# should be installed to all build type toolboxes
#
# Arguments:
#  source_directory: The source directory to be installed. "The last component of each directory
#                    name is appended to the destination directory but a trailing slash may be
#                    used to avoid this because it leaves the last component empty."
#                    (https://cmake.org/cmake/help/v3.3/command/install.html?highlight=install#installing-directories)
#  dest_directory: The destination directory to install to.
#  patterns: list of file patterns to install
function(install_cython_scripts source_directory dest_directory patterns)
  set(patterns_args "")
  set(exclude_patterns "")

  foreach(pattern ${patterns})
    list(APPEND patterns_args PATTERN "${pattern}")
  endforeach()

  file(COPY "${source_directory}" DESTINATION "${dest_directory}"
            FILES_MATCHING ${patterns_args} PATTERN "${exclude_patterns}" EXCLUDE)
endfunction()
