# ~~~
# SPDX-FileCopyrightText: Michael Popoloski
# SPDX-License-Identifier: MIT
# ~~~

# Locate nanobind. Prefer a nanobind installed in the active Python environment
# (this is how scikit-build-core provides it during a wheel build); otherwise
# fall back to fetching it from GitHub.
if(NOT nanobind_FOUND)
  execute_process(
    COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir
    OUTPUT_STRIP_TRAILING_WHITESPACE
    OUTPUT_VARIABLE nanobind_cmake_dir
    RESULT_VARIABLE nanobind_result
    ERROR_QUIET)
  if(nanobind_result EQUAL 0 AND EXISTS "${nanobind_cmake_dir}")
    list(APPEND CMAKE_PREFIX_PATH "${nanobind_cmake_dir}")
    find_package(nanobind CONFIG REQUIRED)
    message(
      STATUS "Using nanobind from Python environment: ${nanobind_cmake_dir}")
  else()
    FetchContent_Declare(
      nanobind
      GIT_REPOSITORY ${SLANG_DEP_URL_PREFIX}wjakob/nanobind.git
      GIT_TAG v3.0.1
      GIT_SHALLOW ON
      FIND_PACKAGE_ARGS)
    FetchContent_MakeAvailable(nanobind)
    message(STATUS "Using remote nanobind library")
  endif()
endif()

# Generate bindings for syntax node types and SyntaxFactory
add_custom_command(
  COMMAND
    ${Python_EXECUTABLE} ${SCRIPTS_DIR}/syntax_gen.py --dir
    ${CMAKE_CURRENT_BINARY_DIR} --python-bindings --syntax
    ${SCRIPTS_DIR}/syntax.txt
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings0.cpp
         ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings1.cpp
         ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings2.cpp
         ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings3.cpp
         ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxFactory.cpp
  DEPENDS ${SCRIPTS_DIR}/syntax_gen.py ${SCRIPTS_DIR}/syntax.txt
  COMMENT "Generating syntax bindings")

# Add the pyslang module via nanobind, built in "split mode": the compiled
# nanobind runtime (libnanobind) is provided at runtime by the separate
# `nanobind-backend` PyPI package instead of being linked in. This targets the
# stable ABI (abi3), so a single wheel per platform covers all supported Python
# versions. See https://nanobind.readthedocs.io/en/latest/split_mode.html
#
# The stable ABI floor is pinned to 3.11 (rather than nanobind's 3.10 default)
# because pyslang.h's std::span buffer caster uses the Python buffer protocol
# (PyObject_GetBuffer / Py_buffer), which only became part of the limited API in
# CPython 3.11. Keep this in sync with `wheel.py-api` / `requires-python` in
# pyproject.toml.
nanobind_add_module(
  pyslang
  BACKEND_MODULE
  nanobind_backend
  STABLE_ABI_VERSION
  3.11
  python/AnalysisBindings.cpp
  python/ASTBindings.cpp
  python/CompBindings.cpp
  python/ExpressionBindings.cpp
  python/NumericBindings.cpp
  python/pyslang.cpp
  python/StatementBindings.cpp
  python/SymbolBindings.cpp
  python/SyntaxBindings.cpp
  python/TypeBindings.cpp
  python/UtilBindings.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings0.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings1.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings2.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxBindings3.cpp
  ${CMAKE_CURRENT_BINARY_DIR}/PySyntaxFactory.cpp)
set_source_files_properties(
  python/pyslang.cpp PROPERTIES COMPILE_DEFINITIONS
                                VERSION_INFO=${PROJECT_VERSION})
target_link_libraries(pyslang PUBLIC slang::slang)
target_link_libraries(pyslang PRIVATE fmt::fmt)
target_include_directories(pyslang PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/python)

install(
  TARGETS pyslang
  COMPONENT pylib
  DESTINATION .)

# Install py.typed marker file for PEP 561 compliance
install(
  FILES ${CMAKE_CURRENT_SOURCE_DIR}/../pyslang/pyslang/py.typed
        ${CMAKE_CURRENT_SOURCE_DIR}/../pyslang/pyslang/__init__.py
  COMPONENT pylib
  DESTINATION .)

# Generate a Python stub (.pyi) for the compiled module at build time using
# nanobind's stub generator, then install it alongside the module.
#
# In split mode the generator must import the extension, which in turn imports
# the compiled `nanobind_backend` module. If the build environment doesn't
# already provide it (a wheel build installs it as a build dependency), fetch a
# matching copy from PyPI into the build tree and add it to the stub import
# path.
set(pyslang_stub_python_path "$<TARGET_FILE_DIR:pyslang>")
execute_process(
  COMMAND "${Python_EXECUTABLE}" -c "import nanobind_backend"
  RESULT_VARIABLE nanobind_backend_missing
  OUTPUT_QUIET ERROR_QUIET)
if(nanobind_backend_missing)
  set(nanobind_backend_dir "${CMAKE_CURRENT_BINARY_DIR}/nanobind_backend")
  # Re-probe with the build-tree copy on the path so reconfigures don't
  # re-fetch.
  execute_process(
    COMMAND "${CMAKE_COMMAND}" -E env "PYTHONPATH=${nanobind_backend_dir}"
            "${Python_EXECUTABLE}" -c "import nanobind_backend"
    RESULT_VARIABLE nanobind_backend_absent
    OUTPUT_QUIET ERROR_QUIET)
  if(nanobind_backend_absent)
    message(
      STATUS
        "pyslang: nanobind_backend not found; fetching it from PyPI for stub generation"
    )
    execute_process(
      COMMAND "${Python_EXECUTABLE}" -m pip install --only-binary :all: --target
              "${nanobind_backend_dir}" "nanobind-backend>=1.0"
      RESULT_VARIABLE nanobind_backend_fetch_result)
    if(NOT nanobind_backend_fetch_result EQUAL 0)
      message(
        FATAL_ERROR
          "pyslang: failed to fetch nanobind-backend from PyPI. Install it into the "
          "build environment (pip install nanobind-backend) and re-run CMake.")
    endif()
  endif()
  list(APPEND pyslang_stub_python_path "${nanobind_backend_dir}")
endif()

nanobind_add_stub(
  pyslang_stub
  MODULE
  pyslang
  PYTHON_PATH
  ${pyslang_stub_python_path}
  DEPENDS
  pyslang
  RECURSIVE
  # With RECURSIVE, nanobind's stubgen places the top-level stub into
  # pyslang/__init__.pyi (not pyslang.pyi) and nests every submodule under
  # pyslang/, because the compiled module has planted submodules. The OUTPUT
  # list must name the files that are actually produced so ninja tracks the step
  # correctly.
  OUTPUT_PATH
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/
  OUTPUT
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/__init__.pyi
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/ast.pyi
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/syntax.pyi
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/parsing.pyi
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/analysis.pyi
  ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/driver.pyi)

# Install the generated stub tree alongside the module. The submodule stubs
# (ast.pyi, syntax.pyi, ...) map onto the pyslang.<sub> modules registered by
# the package __init__.py. This is placed into the pyslang/ directory since the
# modules are actually imported via pyslang.pyslang.[module]
install(
  DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/
  COMPONENT pylib
  DESTINATION ./pyslang/
  FILES_MATCHING
  PATTERN "*.pyi")

# The stubs also need to be installed in the root directory to account for the
# modules being re-exported to sys.modules from the __init__.py
install(
  DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/pyslang-stubs/pyslang/
  COMPONENT pylib
  DESTINATION .
  FILES_MATCHING
  PATTERN "*.pyi")
