# Refactor the cmake/setup.py build to use scikit-build
# https://github.com/wjakob/nanobind_example
# Rather than creating multiple modules and dealing with multiple __init__.py
# files, we can create a single module and use __init__.py to import the
# submodules, create one extension module with submodules
# https://stackoverflow.com/a/77020918/148668

cmake_minimum_required(VERSION 3.15...3.27)
project(pyigl)

# For std::filesystem::path (generic_string)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
# ≥17 for return value optimization 
# <20 for embree
set(CMAKE_CXX_STANDARD 17)

find_package(Python 3.8
  REQUIRED COMPONENTS Interpreter Development.Module
  OPTIONAL_COMPONENTS Development.SABIModule)

if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")

# Enable FetchContent to download dependencies at configure time
include(FetchContent)

if(EMSCRIPTEN)
  # Embree supports Wasm SIMD, but its default Emscripten flags enable pthreads
  # and JS exceptions, which are incompatible with the Pyodide runtime.
  FetchContent_Declare(
    embree
    GIT_REPOSITORY https://github.com/embree/embree.git
    GIT_TAG v4.4.0
    GIT_SHALLOW TRUE
    PATCH_COMMAND "${CMAKE_COMMAND}" -DSOURCE_DIR=<SOURCE_DIR>
      "-DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/pyodide/embree.patch"
      -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pyodide/apply_patch.cmake"
  )
  set(EMBREE_MAX_ISA "SSE2" CACHE STRING "Embree ISA for Wasm SIMD")
  set(FLAGS_SSE2 "-msimd128 -msse -msse2" CACHE STRING "Wasm SSE2 emulation")
  set(EMBREE_IGNORE_CMAKE_CXX_FLAGS OFF CACHE BOOL "Preserve Pyodide compiler flags")
  set(EMBREE_CONFIG "threads=1" CACHE STRING "Single-threaded Pyodide runtime")
endif()

# Download and set up nanobind
FetchContent_Declare(
  nanobind
  GIT_REPOSITORY https://github.com/wjakob/nanobind.git
  GIT_TAG        v2.7.0
)
FetchContent_MakeAvailable(nanobind)

# Pin Eigen to 3.4.0. Recent libigl bumped its Eigen dependency to 5.0.1, whose
# stricter ScalarBinaryOpTraits rejects the int64-vs-int comparisons in some
# libigl headers (e.g. unique_edge_map), which the 64-bit-index Python bindings
# instantiate. Declaring Eigen first makes FetchContent use this version instead
# of the one requested by libigl's own recipe.
FetchContent_Declare(
  eigen
  GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
  GIT_TAG        tags/3.4.0
  GIT_SHALLOW    TRUE
)

# Download and set up libigl
option(LIBIGL_COPYLEFT_CORE        "Build target igl_copyleft::core"       ON)
option(LIBIGL_COPYLEFT_CGAL        "Build target igl_copyleft::cgal"       ON)
option(LIBIGL_EMBREE               "Build target igl::embree"              ON)
option(LIBIGL_COPYLEFT_TETGEN      "Build target igl_copyleft::tetgen"     ON)
option(LIBIGL_RESTRICTED_TRIANGLE  "Build target igl_restricted::triangle" ON)
option(LIBIGL_SPECTRA              "Build igl::spectra bindings"           ON)
option(LIBIGL_PREDICATES           "Build igl::predicates bindings"        ON)
option(LIBIGL_CYCODEBASE           "Build igl::cycodebase bindings"        ON)
if(EMSCRIPTEN)
  set(libigl_patch PATCH_COMMAND "${CMAKE_COMMAND}" -DSOURCE_DIR=<SOURCE_DIR>
    "-DPATCH_FILE=${CMAKE_CURRENT_SOURCE_DIR}/cmake/pyodide/libigl.patch"
    -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pyodide/apply_patch.cmake")
endif()
FetchContent_Declare(
  libigl
  GIT_REPOSITORY https://github.com/libigl/libigl.git
  # libigl main, including igl::lazy_cage and core igl::progressive_hulls (moved
  # to core in libigl PR #2558).
  GIT_TAG c07f8586733378c163aae57f08c9da2ce5352fb9
  ${libigl_patch}
)
FetchContent_MakeAvailable(libigl)

if(EMSCRIPTEN)
  # Pyodide's runtime does not provide pthread workers. Avoid libigl's
  # hardware-concurrency fallback attempting to launch std::threads.
  target_compile_definitions(igl_core INTERFACE IGL_PARALLEL_FOR_FORCE_SERIAL)
  # WebAssembly has no floating-point exception flags. libigl's polar_dec
  # also checks the eigenvalue ratio, so an empty underflow mask preserves
  # that numerical fallback without requiring an unsupported fenv flag.
  target_compile_definitions(igl_core INTERFACE FE_UNDERFLOW=0)
  # Wasm cannot change the rounding mode. CGAL's supported alternative widens
  # interval bounds with nextafter while keeping round-to-nearest arithmetic.
  if(TARGET igl_copyleft_cgal)
    target_compile_definitions(igl_copyleft_cgal INTERFACE
      CGAL_ALWAYS_ROUND_TO_NEAREST CGAL_HAS_NO_THREADS)
  endif()
endif()

# set PYIGL_OUTPUT_DIRECTORY to [this dir]/igl
set(PYIGL_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/igl")

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
# Color output
include(UseColors)

# Extra warnings
include(Warnings)

# Use C++11/14
include(CXXFeatures)

# Generate position independent code by default
set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE INTERNAL "")



# don't need to worry about nested modules (opengl/** are the only ones and
# those probably aren't ever getting python bindings)
#
# prefix is either "", "copyleft", or "restricted"
function(pyigl_include prefix name)
  string(TOUPPER "${prefix}" prefix_uc)
  string(TOUPPER "${name}" name_uc)
  if(prefix_uc)
      string(PREPEND prefix_uc _)
  endif()
  string(TOLOWER "${prefix_uc}" prefix_lc)
  # if(LIBIGL${prefix_uc}_${name_uc}) or name == "core"
  if(LIBIGL${prefix_uc}_${name_uc} OR name STREQUAL "core")
    if("${prefix}" STREQUAL "copyleft")
      if("${name}" STREQUAL "core")
        set(subpath "copyleft")
      else()
        set(subpath "copyleft/${name}")
      endif()
    elseif("${name}" STREQUAL "core")
      set(subpath "")
    else() # "" or "restricted"
      set(subpath "${name}")
    endif()
    file(GLOB sources "${CMAKE_CURRENT_SOURCE_DIR}/src/${subpath}/*.cpp")

    ## Just compile a single file
    #list(FILTER sources EXCLUDE REGEX ".*/module\\.cpp$")
    #list(GET sources 0 sources)
    #list(APPEND sources "${CMAKE_CURRENT_SOURCE_DIR}/src/${subpath}/module.cpp")

    message(STATUS "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$")
    message(STATUS "${prefix} ${name} sources: ${sources}")

    set(BINDING_SOURCES ${sources})
    list(FILTER BINDING_SOURCES EXCLUDE REGEX ".*/module\\.cpp$")

    # keep just the first in the list and overwrite BINDING_SOURCES

    # Generate the function calls based on filenames
    set(BINDING_DECLARATIONS "")
    foreach(source_file ${BINDING_SOURCES})
      get_filename_component(filename ${source_file} NAME_WE)
      set(BINDING_DECLARATIONS "${BINDING_DECLARATIONS}extern void bind_${filename}(nb::module_ &m);\n")
      set(BINDING_INVOCATIONS "${BINDING_INVOCATIONS}    bind_${filename}(m);\n")
    endforeach()
    # make a temporary folder in the build directory to store the generated files
    set(generated_dir "${CMAKE_CURRENT_BINARY_DIR}/include/${subpath}")
    file(MAKE_DIRECTORY "${generated_dir}")
    # write contents into BINDING_DECLARATIONS.h and BINDING_INVOCATIONS.h
    file(WRITE "${generated_dir}/BINDING_DECLARATIONS.in" "${BINDING_DECLARATIONS}")
    file(WRITE "${generated_dir}/BINDING_INVOCATIONS.in" "${BINDING_INVOCATIONS}")

    set(target_name "pyigl${prefix_lc}_${name}")
    # Pyodide extensions target the runtime's exact Python ABI.
    set(abi_options STABLE_ABI)
    if(EMSCRIPTEN)
      set(abi_options)
    endif()
    nanobind_add_module(
      ${target_name} 
      ${abi_options}
      ${sources})

    # important for scikit-build
    install(TARGETS ${target_name} LIBRARY DESTINATION  "igl/${subpath}")


    if("${name}" STREQUAL "core")
      target_link_libraries(${target_name} PRIVATE igl::core)
    else()
      target_link_libraries(${target_name} PRIVATE igl::core igl${prefix_lc}::${name})
    endif()
    target_include_directories(${target_name} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include")
    target_include_directories(${target_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include")
    set(output_dir "${PYIGL_OUTPUT_DIRECTORY}/${subpath}")
    file(MAKE_DIRECTORY ${output_dir})
    file(WRITE "${output_dir}/__init__.py" "from .${target_name} import *\n")
    install(FILES "${output_dir}/__init__.py" DESTINATION "igl/${subpath}")

    set_target_properties(${target_name} PROPERTIES 
        LIBRARY_OUTPUT_DIRECTORY         "${output_dir}"
        RUNTIME_OUTPUT_DIRECTORY         "${output_dir}"
        LIBRARY_OUTPUT_DIRECTORY_RELEASE "${output_dir}"
        RUNTIME_OUTPUT_DIRECTORY_RELEASE "${output_dir}"
    )
    # WebAssembly stubs are generated inside Pyodide by the wheel repair step;
    # the host Python used by CMake cannot import the target extensions.
    if(NOT EMSCRIPTEN)
      set(PYI_OUTPUT "${output_dir}/${target_name}.pyi")
      nanobind_add_stub(
        ${target_name}_stub
        MODULE ${target_name}
        OUTPUT ${PYI_OUTPUT}
        PYTHON_PATH $<TARGET_FILE_DIR:${target_name}>
        DEPENDS ${target_name}
      )
      install(FILES "${PYI_OUTPUT}" DESTINATION "igl/${subpath}")
    endif()

    # just to add dependency?
    if("${name}" STREQUAL "core")
    else()
      target_link_libraries(pyigl_core INTERFACE ${target_name})
    endif()
  endif()
endfunction()

pyigl_include("" "core")
if(LIBIGL_COPYLEFT_CORE)
  pyigl_include("copyleft" "core")
endif()
if(LIBIGL_COPYLEFT_CGAL)
  pyigl_include("copyleft" "cgal")
endif()
if(LIBIGL_EMBREE)
  pyigl_include("" "embree")
endif()
if(LIBIGL_COPYLEFT_TETGEN)
  pyigl_include("copyleft" "tetgen")
endif()
if(LIBIGL_RESTRICTED_TRIANGLE)
  pyigl_include("restricted" "triangle")
endif()
if(LIBIGL_SPECTRA)
  pyigl_include("" "spectra")
endif()
if(LIBIGL_PREDICATES)
  pyigl_include("" "predicates")
endif()
if(LIBIGL_CYCODEBASE)
  pyigl_include("" "cycodebase")
endif()


