cmake_minimum_required(VERSION 3.10.2)

# Workaround for OS X
if(APPLE)
  set(PYTHON_CONFIG_NAME python3-config)
  find_program(PYTHON_CONFIG_EXECUTABLE
               NAMES ${PYTHON_CONFIG_NAME} DOC "python-config executable")
  if(PYTHON_CONFIG_EXECUTABLE)
    execute_process(COMMAND ${PYTHON_CONFIG_EXECUTABLE} --prefix
                    OUTPUT_VARIABLE PYTHON_PREFIX_STRING
                    RESULT_VARIABLE PYTHON_PREFIX_FAILED
                    ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
    if(NOT PYTHON_PREFIX_FAILED)
      file(GLOB LOCAL_PYTHON_NAME RELATIVE ${PYTHON_PREFIX_STRING}/lib "${PYTHON_PREFIX_STRING}/lib/python*.*")
      find_library(PYTHON_LIBRARY NAMES ${LOCAL_PYTHON_NAME}
        PATHS ${PYTHON_PREFIX_STRING}/lib NO_DEFAULT_PATH)
      file(GLOB LOCAL_PYTHON_NAME "${PYTHON_PREFIX_STRING}/include/python*")
      find_path(PYTHON_INCLUDE_DIR
        NAMES Python.h
        PATHS ${LOCAL_PYTHON_NAME} NO_DEFAULT_PATH)
    endif()
    message(STATUS ${PYTHON_INCLUDE_DIR})
  endif()
endif()

function(CasadiGetPythonVersionMajor VERSION_MAJOR_RESULT INCLUDE_PATH)
  set(MATCH_STRING "#define[ \t]+PY_MAJOR_VERSION[ \t]+([0-9]+)")
  file(STRINGS "${INCLUDE_PATH}/patchlevel.h" PY_MAJOR_VERSION REGEX ${MATCH_STRING})
  string(REGEX REPLACE "${MATCH_STRING}" "\\1" PYTHON_VERSION_MAJOR "${PY_MAJOR_VERSION}")
  set(${VERSION_MAJOR_RESULT} ${PYTHON_VERSION_MAJOR} PARENT_SCOPE)
endfunction()

if(SWIG_EXPORT)
  set(PYTHON_VERSION_MAJOR 3)
else()
  if(PYTHON_INCLUDE_DIR)
    set(PYTHON_INCLUDE_PATH ${PYTHON_INCLUDE_DIR})
  else()
    # We don't really need the libs (except on Windows), but the headers.
    include(CasadiFindPython)
    casadi_find_python(DEVELOPMENT)
    if(NOT PYTHON_INCLUDE_PATH)
      message(SEND_ERROR "Python include headers not found, but required." )
    endif()
  endif()
  
  CasadiGetPythonVersionMajor(PYTHON_VERSION_MAJOR "${PYTHON_INCLUDE_PATH}")
  include_directories(${PYTHON_INCLUDE_PATH})
  
  
endif()

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

# a python library is built in the build directory inside swig/python
make_directory(${PROJECT_BINARY_DIR}/python/casadi)

if(WITH_PYTHON_INTERRUPTS)
  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DWITH_PYTHON_INTERRUPTS")
endif()
if(WITH_COPYSIGN_UNDEF)
  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DCASADI_WITH_COPYSIGN_UNDEF")
endif()

set(PYTHONFLAG "")
set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DPy_USING_UNICODE")
set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-noproxydel")
set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-fastunpack")
set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-modernargs")

# -stubs emits PEP-484 .pyi files alongside the .py shadow.  Only the
# patched casadi fork of SWIG supports this; vanilla SWIG rejects the
# flag.  Feature-detect so the build still works with stock SWIG
# (without pyright-friendly type hints).  PEP-484 stub syntax is
# inherently Python 3 — never emit .pyi on a Python 2 build.  Under
# SWIG_IMPORT (sdist consumer) SWIG may be absent entirely; skip the
# feature-detect and let the sdist carry the pre-generated .pyi.
set(_emit_stubs OFF)
if(SWIG_IMPORT)
  message(STATUS "SWIG_IMPORT: consuming pre-generated SWIG output; no local stub generation")
elseif(NOT "${PYTHON_VERSION_MAJOR}" STREQUAL "3")
  message(STATUS "Python 2 target; skipping .pyi stub generation (PEP-484 syntax is Python 3 only)")
else()
  execute_process(
    COMMAND ${SWIG_EXECUTABLE} -python -help
    OUTPUT_VARIABLE _swig_python_help
    ERROR_VARIABLE  _swig_python_help_err
    RESULT_VARIABLE _swig_help_rc
  )
  string(FIND "${_swig_python_help}${_swig_python_help_err}" "-stubs" _swig_has_stubs)
  if(NOT _swig_has_stubs EQUAL -1)
    set(_emit_stubs ON)
    set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-stubs")
    message(STATUS "SWIG supports -stubs and Python 3 target; generating PEP-484 type stubs (.pyi)")
  else()
    message(STATUS "SWIG lacks -stubs support; skipping .pyi stub generation. "
                   "For typed Python bindings, build with the casadi-patched SWIG "
                   "fork (see ~/programs/swig or equivalent).")
  endif()
endif()

# Emit the tiny stub-forwarder artifacts in the build dir at configure
# time.  They're one-line files (forwarder + py.typed marker) and
# embedding them here avoids needing to track generated-looking files
# in source control.
set(_stubs_gen_dir ${CMAKE_CURRENT_BINARY_DIR}/stubs)
# issue #2959: `casadi.array` is a qualified-only alias of ArrayInterface
# (defined in __init__.py, kept out of `from casadi import *`); surface it to
# type checkers so `ca.array(...)` resolves.  NB use a plain assignment, not
# `from .casadi import ArrayInterface as array`: in a .pyi the latter is a
# non-re-exported (private) import that pyright hides from attribute access.
file(WRITE ${_stubs_gen_dir}/__init__.pyi "from .casadi import *\narray = ArrayInterface\n")
file(WRITE ${_stubs_gen_dir}/py.typed     "partial\n")
if(WITH_PYTHON_GIL_RELEASE)
  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-threads")
  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DCASADI_WITH_PYTHON_GIL_RELEASE")
endif()
if("${PYTHON_VERSION_MAJOR}" STREQUAL "3")
set(PYTHONFLAG "3")
set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-py3")
endif()

# Limited API support for stable ABI
# Minimum target is 3.8 (adds PyImport_GetModule). Targets < 3.11 use buffer protocol
# functions not officially in Limited API until 3.11, but de facto stable since Python 3.0.
function(python_version_to_abi_hex out_var version_str)
  string(REPLACE "." ";" _ver "${version_str}")
  list(GET _ver 0 _major)
  list(GET _ver 1 _minor)
  # Convert to hex using math(EXPR) - compatible with CMake 3.13+
  # (string FORMAT requires CMake 3.18+)
  math(EXPR _major_hex "${_major}" OUTPUT_FORMAT HEXADECIMAL)
  math(EXPR _minor_hex "${_minor}" OUTPUT_FORMAT HEXADECIMAL)
  # Remove 0x prefix and pad to 2 digits
  string(REGEX REPLACE "^0x" "" _major_hex "${_major_hex}")
  string(REGEX REPLACE "^0x" "" _minor_hex "${_minor_hex}")
  string(LENGTH "${_major_hex}" _major_len)
  string(LENGTH "${_minor_hex}" _minor_len)
  if(_major_len LESS 2)
    set(_major_hex "0${_major_hex}")
  endif()
  if(_minor_len LESS 2)
    set(_minor_hex "0${_minor_hex}")
  endif()
  string(TOUPPER "${_major_hex}${_minor_hex}" _hex_upper)
  set(${out_var} "0x${_hex_upper}0000" PARENT_SCOPE)
endfunction()

if(WITH_PYTHON_LIMITED_API)
  if(NOT "${PYTHON_VERSION_MAJOR}" STREQUAL "3")
    message(FATAL_ERROR "Python Limited API is only supported with Python 3")
  endif()

  set(PYTHON_LIMITED_API_TARGET "3.11" CACHE STRING "Python Limited API target version (>= 3.11)")
  if(PYTHON_LIMITED_API_TARGET VERSION_LESS "3.11")
    message(FATAL_ERROR "PYTHON_LIMITED_API_TARGET must be >= 3.11 (got: ${PYTHON_LIMITED_API_TARGET})")
  endif()

  python_version_to_abi_hex(PYTHON_LIMITED_API_VERSION "${PYTHON_LIMITED_API_TARGET}")

  message(STATUS "Building abi3 wheel for Python ${PYTHON_LIMITED_API_TARGET}+")

  # Check Python version (skip for SWIG export/import)
  if(NOT SWIG_EXPORT AND NOT SWIG_IMPORT)
    if(PYTHON_VERSION_STRING VERSION_LESS "${PYTHON_LIMITED_API_TARGET}")
      message(FATAL_ERROR "Limited API target ${PYTHON_LIMITED_API_TARGET} requires Python >= ${PYTHON_LIMITED_API_TARGET} (found ${PYTHON_VERSION_STRING})")
    endif()
  endif()

  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DPy_LIMITED_API=${PYTHON_LIMITED_API_VERSION}")
  set(CMAKE_SWIG_FLAGS ${CMAKE_SWIG_FLAGS} "-DPY_VERSION_HEX=${PYTHON_LIMITED_API_VERSION}")
endif()

if(WITH_PYTHON_GIL_RELEASE)
  set(CMAKE_SWIG_NAME_SUFFIX "_gil_release")
else()
  set(CMAKE_SWIG_NAME_SUFFIX "")
endif()

# Main header files
if(NOT SKIP_CONFIG_H_GENERATION)
  configure_file(../swig_config.h.cmake "${CMAKE_CURRENT_BINARY_DIR}/swig_config.h" ESCAPE_QUOTES)
endif()

if(SWIG_EXPORT)
  set(CMAKE_SWIG_OUTDIR ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra)
  set(CMAKE_SWIG_WRAP_OUTDIR ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/source/dummy)
  
  configure_file(setup.py.in "${PROJECT_SOURCE_DIR}/setup.py" @ONLY)
  file(COPY requirements.txt DESTINATION ${PROJECT_SOURCE_DIR})
  file(COPY MANIFEST.in DESTINATION ${PROJECT_SOURCE_DIR})

  # target*/extra/ is the self-contained Python package surface that a
  # downstream SWIG_IMPORT build consumes.  SWIG writes casadi.py (and
  # casadi.pyi under -stubs) here; we need to also drop in __init__.py
  # and, for Python 3, the stub forwarder + py.typed marker so that
  # the SWIG_IMPORT install step has everything it needs without
  # having to reach back into the source tree.
  file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
       DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra)
  if(_emit_stubs)
    file(COPY ${_stubs_gen_dir}/__init__.pyi
              ${_stubs_gen_dir}/py.typed
         DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra)
  endif()
endif()

if(SWIG_IMPORT)
  set(PYTHON_FILE ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/source/casadiPYTHON_wrap${CMAKE_SWIG_NAME_SUFFIX}.cxx)
  set_source_files_properties(${PYTHON_FILE}  PROPERTIES  CPLUSPLUS ON)
else()
  # Generate SWIG wrapper
  set_source_files_properties(../casadi.i  PROPERTIES  CPLUSPLUS ON)
  swig_module_initialize(casadi python)
  swig_add_source_to_module(casadi FALSE PYTHON_FILE ../casadi.i)
  get_directory_property(swig_extra_clean_files ADDITIONAL_MAKE_CLEAN_FILES)
  set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES "${PYTHON_FILE}")
endif()

if(SWIG_EXPORT)
  add_custom_target(python_source DEPENDS ${PYTHON_FILE})
endif()

# Make target
add_custom_target(python DEPENDS _casadi)

add_library(_casadi MODULE ${PYTHON_FILE})

set_target_properties(_casadi PROPERTIES PREFIX "")

# Add compile definitions for Limited API
if(WITH_PYTHON_LIMITED_API)
  target_compile_definitions(_casadi PRIVATE Py_LIMITED_API=${PYTHON_LIMITED_API_VERSION})
endif()
if(WIN32 AND NOT CYGWIN)
  set_target_properties(_casadi PROPERTIES SUFFIX ".pyd")
  set(CASADI_PYTHON_LIBRARY_SUFFIX ".pyd")
else()
  set(CASADI_PYTHON_LIBRARY_SUFFIX ${CMAKE_SHARED_MODULE_SUFFIX})
endif()

if(APPLE AND ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang" OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU"))
  set_target_properties(_casadi PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif()

if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
  set(COMPILE_FLAGS "-Wno-dynamic-class-memaccess -Wno-self-assign -Wno-register ${MAYBE_WERROR}")
elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
  set(COMPILE_FLAGS "-Wno-dynamic-class-memaccess -Wno-self-assign ${MAYBE_WERROR} -Wno-maybe-uninitialized")
  if(WIN32)
    # Fix for error: '_hypot' has not been declared in 'std'
    # cfr https://github.com/tudat-team/tudatpy/commit/ea9ef90a462825a65741f89ac1776413868f52cb
    set(COMPILE_FLAGS "${COMPILE_FLAGS} -D_hypot=hypot")
    # Fix for error: casadiPYTHON_wrap.cxx:(.text+0xd25cc): undefined reference to `__imp_Py_InitModule4'
    # cfr https://github.com/tpaviot/pythonocc-core/commit/9aef6c8e86c5b77c33742acf36e45db0b2f9ca23#diff-1e7de1ae2d059d21e1dd75d5812d5a34b0222cef273b7c3a2af62eb747f9d20a
    if(${CMAKE_SIZEOF_VOID_P} MATCHES "8")
      set(COMPILE_FLAGS "${COMPILE_FLAGS} -DMS_WIN64")
    endif()
  endif()
else()
  set(COMPILE_FLAGS)
endif()

if(MSVC)
  set(COMPILE_FLAGS "${COMPILE_FLAGS} /bigobj")
endif()

message("COMPILE_FLAGS: ${COMPILE_FLAGS}")

set_target_properties(_casadi PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS}")

if(WIN32) # dlls don't allow missing symbols
  if(NOT PYTHON_LIBRARY)
    message(SEND_ERROR "Python libraries not found, but required." )
  endif()
  # For an abi3 build, link against python3.lib (the version-agnostic
  # stable-ABI marshal lib) instead of the version-specific pythonXY.lib.
  # Otherwise the resulting _casadi.pyd has a hard dependency on
  # python311.dll and only loads under Python 3.11 — defeating the
  # whole point of abi3. python3.lib ships in the standard CPython
  # Windows distribution alongside python<X><Y>.lib in the same libs/ dir.
  if(WITH_PYTHON_LIMITED_API)
    get_filename_component(_python_libdir "${PYTHON_LIBRARY}" DIRECTORY)
    find_library(PYTHON3_STABLE_LIB
                 NAMES python3
                 PATHS "${_python_libdir}"
                 NO_DEFAULT_PATH)
    if(PYTHON3_STABLE_LIB)
      message(STATUS "abi3 build: linking against ${PYTHON3_STABLE_LIB} (was ${PYTHON_LIBRARY})")
      target_link_libraries(_casadi ${PYTHON3_STABLE_LIB})
    else()
      message(WARNING
        "WITH_PYTHON_LIMITED_API is ON but python3.lib was not found in "
        "${_python_libdir}; falling back to ${PYTHON_LIBRARY}. The resulting "
        ".pyd will only load under the build-time Python minor version.")
      target_link_libraries(_casadi ${PYTHON_LIBRARY})
    endif()
  else()
    target_link_libraries(_casadi ${PYTHON_LIBRARY})
  endif()
endif()
target_link_libraries(_casadi casadi)

# Custom installation command for Python
add_custom_target(install_python
COMMAND ${CMAKE_COMMAND}
  -D COMPONENT=install_python
  -D CMAKE_INSTALL_PREFIX="${PYTHON_PREFIX}"
  -P cmake_install.cmake
)
add_dependencies(install_python _casadi)

# Install C++ wrapper library
install(TARGETS _casadi
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
)

if (SWIG_IMPORT)
# Install Python proxy classes + the generated PEP-484 stub (alongside .py).
install(FILES
  ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra/casadi.py
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
)
install(FILES
  ${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra/casadi.pyi
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
  OPTIONAL
)
else()
install(FILES
  ${PROJECT_BINARY_DIR}/swig/python/casadi.py
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
)
install(FILES
  ${PROJECT_BINARY_DIR}/swig/python/casadi.pyi
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
  OPTIONAL
)
endif()

# Ship __init__.pyi forwarder + py.typed marker whenever the main
# stub (casadi.pyi) will be installed.  Two cases:
#   - SWIG_IMPORT (pip install from sdist): the sdist producer already
#     made the call — if target*/extra/casadi.pyi exists there, we ship
#     the trio.  The local SWIG may be absent or vanilla; don't gate
#     on `_emit_stubs`.
#   - Normal / SWIG_EXPORT build: use `_emit_stubs`, which tracks
#     whether we just generated casadi.pyi ourselves.
# Python 2 never gets stubs either way (PEP-484 is Py3 syntax).
set(_ship_stub_trio OFF)
if("${PYTHON_VERSION_MAJOR}" STREQUAL "3")
  if(SWIG_IMPORT)
    if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/target${PYTHONFLAG}/extra/casadi.pyi")
      set(_ship_stub_trio ON)
    endif()
  elseif(_emit_stubs)
    set(_ship_stub_trio ON)
  endif()
endif()
if(_ship_stub_trio)
  install(FILES
    ${_stubs_gen_dir}/__init__.pyi
    ${_stubs_gen_dir}/py.typed
    DESTINATION "${PYTHON_PREFIX}/casadi"
    COMPONENT install_python
  )
endif()

# Install Python tools
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tools
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
  USE_SOURCE_PERMISSIONS
  PATTERN .pyc EXCLUDE
  PATTERN .svn EXCLUDE
)

# Install Python package initialization
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/__init__.py
  DESTINATION "${PYTHON_PREFIX}/casadi"
  COMPONENT install_python
)

# Install pip metadata files to ensure that casadi installed via CMake is listed by pip list
# See https://packaging.python.org/specifications/recording-installed-packages/
# and https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata
option(CASADI_PYTHON_PIP_METADATA_INSTALL "Use CMake to install Python pip metadata. Set to off if some other tool already installs it." OFF)
mark_as_advanced(CASADI_PYTHON_PIP_METADATA_INSTALL)
set(CASADI_PYTHON_PIP_METADATA_INSTALLER "cmake" CACHE STRING "Specify the string to identify the pip Installer. Default: cmake, change this if you are using another tool.")
mark_as_advanced(CASADI_PYTHON_PIP_METADATA_INSTALLER)
if (CASADI_PYTHON_PIP_METADATA_INSTALL)
  file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/METADATA "")
  file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/METADATA "Metadata-Version: 2.1\n")
  file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/METADATA "Name: casadi\n")
  file(APPEND ${CMAKE_CURRENT_BINARY_DIR}/METADATA "Version: ${CASADI_VERSION}\n")
  file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/INSTALLER "${CASADI_PYTHON_PIP_METADATA_INSTALLER}\n")
  install(
    FILES "${CMAKE_CURRENT_BINARY_DIR}/METADATA" "${CMAKE_CURRENT_BINARY_DIR}/INSTALLER"
    DESTINATION ${PYTHON_PREFIX}/casadi-${CASADI_VERSION}.dist-info
    COMPONENT install_python)
endif()

# Example of how to extend CasADi with additional features
if (WITH_EXTENDING_CASADI)
  set_source_files_properties(../extending_casadi/extending_casadi.i  PROPERTIES  CPLUSPLUS ON)
  swig_add_module(extending_casadi python ../extending_casadi/extending_casadi.i)
  swig_link_libraries(extending_casadi extending_casadi)
  if("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang")
    set_target_properties(_extending_casadi PROPERTIES COMPILE_FLAGS "-Wno-dynamic-class-memaccess -Wno-self-assign ${MAYBE_WERROR} -fno-builtin-strdup -fno-builtin-strndup")
  elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
    set_target_properties(_extending_casadi PROPERTIES COMPILE_FLAGS "-Wno-dynamic-class-memaccess -Wno-self-assign -Wno-maybe-uninitialized ${MAYBE_WERROR} -fno-builtin-strdup -fno-builtin-strndup")
  endif()
  set(CASADI_PYTHON_LIBRARY ${SWIG_MODULE_extending_casadi_REAL_NAME}${CASADI_PYTHON_LIBRARY_SUFFIX})

  add_custom_target(extending_casadi_python DEPENDS _extending_casadi extending_casadi)

  install(TARGETS _extending_casadi
    DESTINATION "${PYTHON_PREFIX}/extending_casadi"
    COMPONENT install_python
  )

  install(FILES ${PROJECT_BINARY_DIR}/swig/python/extending_casadi.py
    DESTINATION "${PYTHON_PREFIX}/extending_casadi"
    COMPONENT install_python
  )

  install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/../extending_casadi/__init__.py
    DESTINATION "${PYTHON_PREFIX}/extending_casadi"
    COMPONENT install_python
  )

endif()
