cmake_minimum_required(VERSION 3.18)
project(affine_mpc_py LANGUAGES CXX)

option(AFFINE_MPC_PY_ADD_TESTS "Register Python binding tests" OFF)

# Shared warning settings. OPTIONAL keeps standalone builds working when only
# this directory is available.
include(${CMAKE_CURRENT_LIST_DIR}/../cmake/compiler_warnings.cmake OPTIONAL)

# -----------------------------------------
# Dependencies
# -----------------------------------------
# Use the vcpkg toolchain file if defined (for the Github Windows workflow)
if(DEFINED ENV{VCPKG_TOOLCHAIN_FILE})
  set(CMAKE_TOOLCHAIN_FILE $ENV{VCPKG_TOOLCHAIN_FILE} CACHE STRING "")
endif()

set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 3 CONFIG REQUIRED)

if(NOT TARGET affine_mpc::affine_mpc)
  find_package(affine_mpc CONFIG REQUIRED)
endif()

# -----------------------------------------
# Python bindings
# -----------------------------------------
pybind11_add_module(_bindings MODULE
  bindings/affine_mpc_py_module.cpp
  bindings/py_options.cpp
  bindings/py_parameterization.cpp
  bindings/py_discretizer.cpp
  bindings/py_osqp_settings.cpp
  bindings/py_solve_status.cpp
  bindings/py_quadratic_program.cpp
  bindings/py_mpc.cpp
  bindings/py_mpc_logger.cpp
)

if(COMMAND affine_mpc_enable_warnings)
  affine_mpc_enable_warnings(_bindings)
endif()

set_target_properties(_bindings PROPERTIES
  LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/affine_mpc"
  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/affine_mpc"
)

target_link_libraries(_bindings PRIVATE affine_mpc::affine_mpc)

# Copy the Python package into the build tree so the built extension is
# importable for local testing and stub generation.
file(GLOB affine_mpc_py_files
  CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/affine_mpc/*.py"
)
add_custom_command(TARGET _bindings POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
    ${affine_mpc_py_files}
    ${CMAKE_CURRENT_BINARY_DIR}/affine_mpc
)

add_custom_target(python_stubgen
  COMMAND ${CMAKE_COMMAND} -E env
    "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}"
    -- "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/custom_stubgen.py"
  DEPENDS _bindings
  WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
  COMMENT "Generating Python stubs from built bindings"
)

if(AFFINE_MPC_PY_ADD_TESTS)
  include(CTest)
  enable_testing()

  execute_process(
    COMMAND "${Python_EXECUTABLE}" -m pytest --version
    RESULT_VARIABLE pytest_found
    OUTPUT_QUIET
    ERROR_QUIET
  )
  if(NOT pytest_found EQUAL 0)
    message(FATAL_ERROR "pytest is required to register Python binding tests")
  endif()

  add_test(
    NAME python_bindings_tests
    COMMAND "${Python_EXECUTABLE}" -m pytest
      "${CMAKE_CURRENT_SOURCE_DIR}/../test/bindings"
  )

  set(python_bindings_test_env
    "PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR}"
    "AFFINE_MPC_HAS_ZLIB=$<IF:$<BOOL:${ZLIB_FOUND}>,1,0>"
  )
  set_tests_properties(python_bindings_tests PROPERTIES
    ENVIRONMENT "${python_bindings_test_env}"
  )
endif()

# -----------------------------------------
# Installation
# -----------------------------------------

# Install the compiled bindings module
install(TARGETS _bindings
  DESTINATION affine_mpc
  COMPONENT python_bindings
)

# When using shared libs, python must be able to find the .so files:
#   - if using system libs then they should be in standard findable paths.
#   - else the .so files must be copied into the python environment and the
#     runtime path (RPATH) of _bindings must be set to look in that same
#     location. Windows does not handle RPATH the same way, so using system libs
#     may not work on Windows builds.
if(BUILD_SHARED_LIBS)
  # These libs are aliased when built by this project rather than imported from
  # a system package with find_package. If built by this project they must be
  # installed to the python environment.
  get_target_property(affine_mpc_is_aliased
    affine_mpc::affine_mpc ALIASED_TARGET
  )
  get_target_property(osqp_is_aliased osqp::osqp ALIASED_TARGET)

  if(affine_mpc_is_aliased OR osqp_is_aliased)
    # This means a .so file will be copied into the python environment
    # and the RPATH must know to look for it there.
    if(APPLE)
      set_target_properties(_bindings PROPERTIES INSTALL_RPATH "@loader_path")
    elseif(UNIX)
      set_target_properties(_bindings PROPERTIES INSTALL_RPATH "$ORIGIN")
    endif()
  endif()

  if(affine_mpc_is_aliased)
    install(TARGETS affine_mpc
      RUNTIME DESTINATION affine_mpc
      LIBRARY DESTINATION affine_mpc
      COMPONENT python_bindings
    )
  else()
    message(STATUS "Using affine_mpc shared lib found from system.")
  endif()

  if(osqp_is_aliased)
    install(TARGETS osqp
      RUNTIME DESTINATION affine_mpc
      LIBRARY DESTINATION affine_mpc
      COMPONENT python_bindings
    )
  else()
    message(STATUS "Using OSQP shared lib found from system.")
  endif()
endif()

# Install package Python and stub files
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/affine_mpc
  DESTINATION .
  COMPONENT python_bindings
  FILES_MATCHING
    PATTERN "*.py"
    PATTERN "*.pyi"
    PATTERN "py.typed"
)

# Install example files
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../examples/
  DESTINATION affine_mpc/_examples
  COMPONENT python_bindings
  FILES_MATCHING
    PATTERN "*.py"
)
