#
# Copyright (c) 2015-2023 CNRS INRIA
# Copyright (c) 2015 Wandercraft, 86 rue de Paris 91400 Orsay, France.
#

include(${JRL_CMAKE_MODULES}/python-helpers.cmake)
include(${JRL_CMAKE_MODULES}/stubs.cmake)
set(CMAKE_MODULE_PATH "${JRL_CMAKE_MODULES}/find-external/GMP"
                      "${JRL_CMAKE_MODULES}/find-external/MPFR" ${CMAKE_MODULE_PATH})

# --- PYTHON TARGET --- #
set(PYWRAP ${PROJECT_NAME}_pywrap)
set(PYWRAP
    ${PYWRAP}
    PARENT_SCOPE)

# --- COMPILE WRAPPER
make_directory("${${PROJECT_NAME}_BINARY_DIR}/bindings/python/${PROJECT_NAME}")
set(${PYWRAP}_SOURCES ${${PROJECT_NAME}_BINDINGS_PYTHON_SOURCES})
set(${PYWRAP}_HEADERS ${${PROJECT_NAME}_BINDINGS_PYTHON_PUBLIC_HEADERS})

function(PINOCCHIO_PYTHON_BINDINGS_SPECIFIC_TYPE scalar_name)
  set(scalar_name "${scalar_name}")
  set(PYTHON_LIB_NAME "${PYWRAP}_${scalar_name}")

  string(TOUPPER ${scalar_name} upper_scalar_name)
  set(EXTRA_SOURCES ${${PROJECT_NAME}_BINDINGS_PYTHON_EXTRA_${upper_scalar_name}_SOURCES})
  set(EXTRA_HEADERS ${${PROJECT_NAME}_BINDINGS_PYTHON_EXTRA_${upper_scalar_name}_PUBLIC_HEADERS})

  set(${PYTHON_LIB_NAME}_SOURCES ${${PYWRAP}_SOURCES} ${EXTRA_SOURCES})
  set(${PYTHON_LIB_NAME}_HEADERS ${${PYWRAP}_HEADERS} ${EXTRA_HEADERS})

  add_library(${PYTHON_LIB_NAME} SHARED ${${PYTHON_LIB_NAME}_SOURCES} ${${PYTHON_LIB_NAME}_HEADERS})

  # Links against accelerate
  if(BUILD_WITH_ACCELERATE_SUPPORT)
    # modernize_target_link_libraries(${PYTHON_LIB_NAME} SCOPE PUBLIC TARGETS Accelerate)
    target_link_libraries(${PYTHON_LIB_NAME} PRIVATE "-framework accelerate")
  endif(BUILD_WITH_ACCELERATE_SUPPORT)

  if(BUILD_WITH_OPENMP_SUPPORT)
    # OpenMP is linked with pinocchio_parallel target
    target_compile_definitions(${PYTHON_LIB_NAME} PRIVATE -DPINOCCHIO_PYTHON_INTERFACE_WITH_OPENMP)
  endif()
  add_dependencies(${PROJECT_NAME}-python ${PYTHON_LIB_NAME})

  # Remove lib prefix for the target and use the right define for DLLAPI definition.
  #
  # Set inlined function visibility to hidden by default. This avoid bug on OSX where two symbol
  # from two different binding (`pinocchio::python::exposeSpecificTypeFeatures`) can be wrongly
  # loaded (see https://github.com/stack-of-tasks/pinocchio/issues/2462).
  set_target_properties(
    ${PYTHON_LIB_NAME}
    PROPERTIES PREFIX ""
               DEFINE_SYMBOL "${PYWRAP}_EXPORTS"
               CXX_VISIBILITY_PRESET hidden
               VISIBILITY_INLINES_HIDDEN ON)

  # Do not report:
  #
  # * -Wconversion as the BOOST_PYTHON_FUNCTION_OVERLOADS implicitly converts.
  # * -Wcomment as latex equations have multi-line comments.
  # * -Wself-assign-overloaded as bp::self operations trigger this (Clang only).
  # * -Xclang=-fno-pch-timestamp to allow ccache to use pch
  #   (https://ccache.dev/manual/latest.html#_precompiled_headers).
  cxx_flags_by_compiler_frontend(
    GNU -Wno-conversion -Wno-comment -Wno-self-assign-overloaded -Xclang=-fno-pch-timestamp
    MSVC -Xclang=-fno-pch-timestamp
    OUTPUT PRIVATE_OPTIONS
    FILTER)
  target_compile_options(${PYTHON_LIB_NAME} PRIVATE ${PRIVATE_OPTIONS})

  set(PINOCCHIO_PYTHON_CONTEXT_FILE_VALUE "pinocchio/bindings/python/context/${scalar_name}.hpp")
  target_compile_definitions(
    ${PYTHON_LIB_NAME}
    PRIVATE PINOCCHIO_PYTHON_CONTEXT_FILE="${PINOCCHIO_PYTHON_CONTEXT_FILE_VALUE}"
            PINOCCHIO_PYTHON_MODULE_NAME=${PYTHON_LIB_NAME})

  set_target_properties(${PYTHON_LIB_NAME} PROPERTIES VERSION ${PROJECT_VERSION})
  if(BUILD_WITH_COMMIT_VERSION)
    tag_library_version(${PYTHON_LIB_NAME})
  endif(BUILD_WITH_COMMIT_VERSION)
  add_header_group(${PYTHON_LIB_NAME}_HEADERS)
  add_source_group(${PYTHON_LIB_NAME}_SOURCES)

  modernize_target_link_libraries(
    ${PYTHON_LIB_NAME}
    SCOPE PUBLIC
    TARGETS eigenpy::eigenpy)
  target_link_libraries(${PYTHON_LIB_NAME} PUBLIC ${PROJECT_NAME}::${PROJECT_NAME})

  if(BUILD_WITH_URDF_SUPPORT)
    # Link directly against console_bridge since we bind some enums and call
    # console_bridge::setLogLevel function.
    modernize_target_link_libraries(
      ${PYTHON_LIB_NAME}
      SCOPE PUBLIC
      TARGETS console_bridge::console_bridge
      LIBRARIES ${console_bridge_LIBRARIES}
      INCLUDE_DIRS ${console_bridge_INCLUDE_DIRS})
  endif()
  if(BUILD_WITH_HPP_FCL_SUPPORT)
    target_compile_definitions(
      ${PYTHON_LIB_NAME} PRIVATE PINOCCHIO_PYTHON_INTERFACE_WITH_HPP_FCL_PYTHON_BINDINGS
                                 COAL_DISABLE_HPP_FCL_WARNINGS)
  endif()
  if(WIN32)
    target_link_libraries(${PYTHON_LIB_NAME} PUBLIC ${PYTHON_LIBRARY})
  endif()

  set(${PYWRAP}_INSTALL_DIR ${ABSOLUTE_PYTHON_SITELIB}/${PROJECT_NAME})

  set_target_properties(
    ${PYTHON_LIB_NAME}
    PROPERTIES PREFIX ""
               SUFFIX ${PYTHON_EXT_SUFFIX}
               OUTPUT_NAME "${PYTHON_LIB_NAME}"
               LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bindings/python/${PROJECT_NAME}"
               # On Windows, shared library are treat as binary
               RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/bindings/python/${PROJECT_NAME}")

  if(UNIX)
    get_relative_rpath(${${PYWRAP}_INSTALL_DIR} ${PYWRAP}_INSTALL_RPATH)
    set_target_properties(${PYTHON_LIB_NAME} PROPERTIES INSTALL_RPATH "${${PYWRAP}_INSTALL_RPATH}")
  endif()

  install(
    TARGETS ${PYTHON_LIB_NAME}
    EXPORT ${TARGETS_EXPORT_NAME}
    DESTINATION ${PINOCCHIO_PYTHON_INSTALL_DIR})
endfunction()

function(INSTALL_PYTHON_FILES)
  set(options)
  set(oneValueArgs MODULE)
  set(multiValueArgs FILES)
  cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

  set(SOURCE_PATH ${PROJECT_NAME})
  set(INSTALL_PATH ${PINOCCHIO_PYTHON_INSTALL_DIR})
  if(ARGS_MODULE)
    set(SOURCE_PATH ${SOURCE_PATH}/${ARGS_MODULE})
    set(INSTALL_PATH ${INSTALL_PATH}/${ARGS_MODULE})
  endif()

  foreach(f ${ARGS_FILES})
    python_build(${SOURCE_PATH} ${f})
    install(FILES ${SOURCE_PATH}/${f} DESTINATION ${INSTALL_PATH})
  endforeach()
endfunction()

if(BUILD_PYTHON_INTERFACE)
  add_custom_target(${PROJECT_NAME}-python)
  set_target_properties(${PROJECT_NAME}-python PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD True)

  python_build_get_target(python_build_target)
  add_dependencies(${PROJECT_NAME}-python ${python_build_target})

  set(PKG_CONFIG_PYWRAP_REQUIRES "eigenpy >= 2.6.5")
  if(IS_ABSOLUTE ${PYTHON_SITELIB})
    set(ABSOLUTE_PYTHON_SITELIB ${PYTHON_SITELIB})
  else()
    set(ABSOLUTE_PYTHON_SITELIB ${CMAKE_INSTALL_PREFIX}/${PYTHON_SITELIB})
  endif()
  set(PINOCCHIO_PYTHON_INSTALL_DIR ${ABSOLUTE_PYTHON_SITELIB}/${PROJECT_NAME})

  pinocchio_python_bindings_specific_type(default)
  # Precompiled headers are used to speed up compilation of the Python bindings. We have also tested
  # precompiling headers for the main library's `pinocchio_default` target, but the time gain there
  # was negligible compared to the time gained in the Python bindings. We also precompile headers
  # for other scalar types as well.
  target_precompile_headers(${PYWRAP}_default PRIVATE
                            ${PROJECT_SOURCE_DIR}/include/pinocchio/bindings/python/pch.hpp)
  set(PYTHON_LIB_NAME "${PYWRAP}_default")
  set(STUBGEN_DEPENDENCIES "${PYWRAP}_default")

  if(BUILD_WITH_AUTODIFF_SUPPORT)
    pinocchio_python_bindings_specific_type(cppad cppad)
    # CPPAD_DEBUG_AND_RELEASE allow to mix debug and release versions of CppAD in the same program.
    # This can happen when Pinocchio is build in Debug mode and pycppad is build in Release mode.
    # See
    # https://coin-or.github.io/CppAD/doc/preprocessor.htm#Documented%20Here.CPPAD_DEBUG_AND_RELEASE.
    target_compile_definitions(
      ${PYWRAP}_cppad PRIVATE PYCPPAD_EXCLUDE_EIGEN_NUMTRAITS_SPECIALIZATION
                              CPPAD_DEBUG_AND_RELEASE)
    target_include_directories(${PYWRAP}_cppad SYSTEM
                               PUBLIC $<BUILD_INTERFACE:${cppad_INCLUDE_DIR}>)
    target_link_libraries(${PYWRAP}_cppad PUBLIC ${cppad_LIBRARY})
    list(APPEND STUBGEN_DEPENDENCIES "${PYWRAP}_cppad")

    target_precompile_headers(
      ${PYWRAP}_cppad PRIVATE ${PROJECT_SOURCE_DIR}/include/pinocchio/bindings/python/pch.hpp
      <cppad/cppad.hpp> <boost/mpl/int.hpp>)

    # --- INSTALL SCRIPTS
    install_python_files(MODULE cppad FILES __init__.py)
  endif(BUILD_WITH_AUTODIFF_SUPPORT)

  if(BUILD_WITH_CODEGEN_SUPPORT)
    pinocchio_python_bindings_specific_type(cppadcg cppadcg)

    # On osx, use default visiblitiy because of an issue with thread_local storage defined in
    # cppad/cg/cg.hpp header (see
    # https://github.com/stack-of-tasks/pinocchio/pull/2469#issuecomment-2461845127)
    if(APPLE)
      set_target_properties(${PYWRAP}_cppadcg PROPERTIES CXX_VISIBILITY_PRESET default)
    endif()

    # CPPAD_DEBUG_AND_RELEASE allow to mix debug and release versions of CppAD in the same program.
    target_compile_definitions(
      ${PYWRAP}_cppadcg PRIVATE PYCPPAD_EXCLUDE_EIGEN_NUMTRAITS_SPECIALIZATION
                                CPPAD_DEBUG_AND_RELEASE)
    target_include_directories(${PYWRAP}_cppadcg SYSTEM
                               PUBLIC $<BUILD_INTERFACE:${cppadcg_INCLUDE_DIR}>)
    target_link_libraries(${PYWRAP}_cppadcg PUBLIC ${cppadcg_LIBRARY} ${cppad_LIBRARY})
    list(APPEND STUBGEN_DEPENDENCIES "${PYWRAP}_cppadcg")

    target_precompile_headers(
      ${PYWRAP}_cppadcg PRIVATE ${PROJECT_SOURCE_DIR}/include/pinocchio/bindings/python/pch.hpp
      <cmath> <boost/mpl/int.hpp> <cppad/cg/support/cppadcg_eigen.hpp>)

    # --- INSTALL SCRIPTS
    install_python_files(MODULE cppadcg FILES __init__.py)
  endif(BUILD_WITH_CODEGEN_SUPPORT)

  if(BUILD_WITH_CASADI_SUPPORT)
    pinocchio_python_bindings_specific_type(casadi)
    target_link_libraries(${PYWRAP}_casadi PUBLIC casadi::casadi)
    list(APPEND STUBGEN_DEPENDENCIES "${PYWRAP}_casadi")

    target_precompile_headers(
      ${PYWRAP}_casadi PRIVATE ${PROJECT_SOURCE_DIR}/include/pinocchio/bindings/python/pch.hpp
      <casadi/casadi.hpp> <casadi/core/casadi_types.hpp> <casadi/core/code_generator.hpp>)

    # --- INSTALL SCRIPTS
    install_python_files(MODULE casadi FILES __init__.py)
  endif(BUILD_WITH_CASADI_SUPPORT)

  if(BUILD_PYTHON_BINDINGS_WITH_BOOST_MPFR_SUPPORT)
    find_package(GMP REQUIRED 6.0.0)
    find_package(MPFR REQUIRED 4.0.0)
    pinocchio_python_bindings_specific_type(mpfr)

    target_link_libraries(${PYWRAP}_mpfr PRIVATE gmp mpfr)
    list(APPEND STUBGEN_DEPENDENCIES "${PYWRAP}_mpfr")

    target_precompile_headers(
      ${PYWRAP}_mpfr PRIVATE ${PROJECT_SOURCE_DIR}/include/pinocchio/bindings/python/pch.hpp
      <boost/multiprecision/mpfr.hpp>)

    # --- INSTALL SCRIPTS
    install_python_files(MODULE mpfr FILES __init__.py)
  endif(BUILD_PYTHON_BINDINGS_WITH_BOOST_MPFR_SUPPORT)

  # --- INSTALL SCRIPTS
  install_python_files(
    FILES __init__.py
          deprecated.py
          deprecation.py
          utils.py
          robot_wrapper.py
          romeo_wrapper.py
          explog.py
          shortcuts.py
          windows_dll_manager.py)

  # --- INSTALL DERIVATIVE SCRIPTS
  install_python_files(MODULE derivative FILES xm.py dcrba.py lambdas.py)

  # --- INSTALL VISUALIZATION SCRIPTS
  install_python_files(
    MODULE visualize
    FILES __init__.py
          base_visualizer.py
          gepetto_visualizer.py
          meshcat_visualizer.py
          panda3d_visualizer.py
          rviz_visualizer.py
          viser_visualizer.py
          visualizers.py)

  # --- STUBS --- #
  if(GENERATE_PYTHON_STUBS)
    python_build_get_target(python_build_target_name)
    load_stubgen()
    # Set PYWRAP and PROJECT_NAME as stubs dependencies.
    #
    # PROJECT_NAME is mandatory (even if it's a PYWRAP dependency) to find PROJECT_NAME name DLL on
    # windows.
    generate_stubs(
      ${CMAKE_CURRENT_BINARY_DIR}
      ${PROJECT_NAME}
      ${ABSOLUTE_PYTHON_SITELIB}
      ${PYWRAP}_default
      ${PROJECT_NAME}::${PROJECT_NAME}_default
      ${STUBGEN_DEPENDENCIES}
      ${python_build_target_name})
  endif(GENERATE_PYTHON_STUBS)

  # --- PACKAGING --- #

  # Format string
  set(_PKG_CONFIG_PYWRAP_LIBDIR ${PINOCCHIO_PYTHON_INSTALL_DIR})
  set(_PKG_CONFIG_PYWRAP_BINDIR ${PINOCCHIO_PYTHON_INSTALL_DIR})
  set(_PKG_CONFIG_PYWRAP_CONFLICTS)
  set(_PKG_CONFIG_PYWRAP_REQUIRES "${PROJECT_NAME}")
  foreach(dep ${PKG_CONFIG_PYWRAP_REQUIRES})
    set(_PKG_CONFIG_PYWRAP_REQUIRES "${_PKG_CONFIG_PYWRAP_REQUIRES}, ${dep}")
  endforeach(dep ${PKG_CONFIG_PYWRAP_REQUIRES})

  set(_PKG_CONFIG_PYWRAP_LIBS "-L\${libdir} -l${PYWRAP}")
  if(APPLE)
    set(_PKG_CONFIG_PYWRAP_LIBS
        "${_PKG_CONFIG_PYWRAP_LIBS} -Wl,-undefined,dynamic_lookup,${Boost_${UPPERCOMPONENT}_LIBRARY}"
    )
  else(APPLE)
    set(_PKG_CONFIG_PYWRAP_LIBS "${_PKG_CONFIG_PYWRAP_LIBS} ${LIBINCL_KW}boost_python")
  endif(APPLE)

  set(_PKG_CONFIG_PYWRAP_CFLAGS "-I\${includedir}")
  set(_PKG_CONFIG_PYWRAP_CFLAGS "${_PKG_CONFIG_PYWRAP_CFLAGS} -I${PYTHON_INCLUDE_DIRS}")
  foreach(cflags ${CFLAGS_DEPENDENCIES})
    set(_PKG_CONFIG_PYWRAP_CFLAGS "${_PKG_CONFIG_PYWRAP_CFLAGS} ${cflags}")
  endforeach(cflags ${CFLAGS_DEPENDENCIES})
  foreach(cflags ${CFLAGS_OPTIONS})
    set(_PKG_CONFIG_PYWRAP_CFLAGS "${_PKG_CONFIG_PYWRAP_CFLAGS} ${cflags}")
  endforeach()

  configure_file("${CMAKE_CURRENT_SOURCE_DIR}/pinocchiopy.pc.cmake"
                 "${CMAKE_CURRENT_BINARY_DIR}/pinocchiopy.pc")

  if(NOT BUILD_STANDALONE_PYTHON_INTERFACE)
    install(
      FILES "${CMAKE_CURRENT_BINARY_DIR}/pinocchiopy.pc"
      DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
      PERMISSIONS OWNER_READ GROUP_READ WORLD_READ OWNER_WRITE)
  endif()

  if(DOXYGEN_FOUND AND DOXYGEN_VERSION VERSION_GREATER 1.8.17)
    set(DOXYGEN_GENERATE_HTML YES)
    set(DOXYGEN_GENERATE_LATEX NO)
    set(DOXYGEN_PROJECT_NAME "Pinocchio PyBind11 helpers.")
    set(_source_headers_root "../../include/${PROJECT_NAME}/bindings/python")
    doxygen_add_docs(
      doc_pybind11 ${_source_headers_root}/pybind11.hpp ${_source_headers_root}/pybind11-all.hpp
      USE_STAMP_FILE COMMENT "Generating documentation of the PyBind11 helpers.")
  endif()
endif(BUILD_PYTHON_INTERFACE)
