cmake_minimum_required(VERSION 3.16)
project(jpype-native)

option(ENABLE_TRACING "Enable tracing in the extension" OFF)
option(ENABLE_COVERAGE "Enable compilation for coverage measurement" OFF)
option(ENABLE_BUILD_JAR "Enable building of the Java JAR" ON)
option(BUILD_TEST_HARNESS "Build the Java test harness" ON)
# Optionally set Python GIL disabled flag
# set(PY_GIL_DISABLED ON)
set(CMAKE_CXX_STANDARD 14)

find_package(Python3 COMPONENTS Interpreter Development.Module REQUIRED)
find_package(Python3 COMPONENTS Development.Embed)
find_package(Java REQUIRED COMPONENTS Development)
find_package(JNI)

if(JNI_FOUND)
    message(STATUS "System JNI found successfully.")
else()
    message(STATUS "System JNI headers not found. Falling back to bundled jni.h.")
    set(JNI_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/native/jni_include")
endif()

message(STATUS "Final JNI include dirs: ${JNI_INCLUDE_DIRS}")


if(SKBUILD)
    message(DEBUG "SKBuild invocation")
    set(DEST_JAR ${SKBUILD_PLATLIB_DIR})
    set(DEST_EXTENSION ${SKBUILD_PLATLIB_DIR})
else()
    # in-place build, set output to cmakes output dir.
    set(DEST_JAR ${CMAKE_CURRENT_BINARY_DIR})
    set(DEST_EXTENSION ${CMAKE_CURRENT_BINARY_DIR})
endif()

message(STATUS "Destination Python extension: ${DEST_EXTENSION}")
message(STATUS "Destination JAR: ${DEST_JAR}")

if(ENABLE_BUILD_JAR)
    message(DEBUG "build jar")

    find_program(ANT_EXECUTABLE ant REQUIRED)
    # this is the build artifact of Ant.
    set(JAR_OUTPUT "${CMAKE_SOURCE_DIR}/native/build/lib/org.jpype.jar")

    # we also need to copy the JAR_OUTPUT to CMAKE_CURRENT_BINARY_DIR
    add_custom_command(
            OUTPUT "${JAR_OUTPUT}"
            COMMAND "${ANT_EXECUTABLE}" -f "${CMAKE_SOURCE_DIR}/native/build.xml"
            WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
            COMMENT "Building ${JAR_OUTPUT} with Apache Ant"
    )
    # link ant command with output jar
    add_custom_target(jpype_jar ALL DEPENDS "${JAR_OUTPUT}")

    # Copy the Ant build artifact to current cmake output dir (required for editable installs).
    set(DEST_FILE "${CMAKE_CURRENT_BINARY_DIR}/org.jpype.jar")
    add_custom_command(
            OUTPUT "${DEST_FILE}"
            COMMAND ${CMAKE_COMMAND} -E copy "${JAR_OUTPUT}" "${DEST_FILE}"
            DEPENDS "${JAR_OUTPUT}"   # ensures it only runs after the source exists
            COMMENT "Copying ${JAR_OUTPUT} to internal destination"
    )
    add_custom_target(copy_jar ALL DEPENDS "${DEST_FILE}")
    # Explicitly link targets so MSBuild doesn't run them in parallel.
    add_dependencies(copy_jar jpype_jar)
    # finally install the jar to DEST_JAR, which makes it available for wheel building.
    install(
            FILES "${DEST_FILE}"
            DESTINATION ${DEST_JAR}
    )
endif()


if(ENABLE_TRACING)
    add_compile_definitions(JP_TRACING_ENABLE)
endif()

if(ENABLE_COVERAGE)
    if(NOT CMAKE_BUILD_TYPE)
        message(STATUS "ENABLE_COVERAGE set — forcing Debug build for coverage correctness")
        set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
    elseif(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
        message(WARNING "ENABLE_COVERAGE set but CMAKE_BUILD_TYPE is ${CMAKE_BUILD_TYPE}; coverage may be unreliable")
    endif()

    add_compile_definitions(JP_INSTRUMENTATION)
    message(STATUS "Adding coverage instrumentation flags")
    if(MSVC)
        warn("Coverage not supported on windows.")
    endif()
    # Enable debug symbols
    add_compile_options(-ggdb)

    # Enable coverage instrumentation (GCC/Clang)
    add_compile_options(-O0 --coverage -ftest-coverage)
    add_link_options(--coverage)
endif()

if(BUILD_TEST_HARNESS)
    find_program(ANT_EXECUTABLE ant REQUIRED)
    
    # We define a representative output file to help CMake track dependencies
    set(TEST_SENTINEL "${CMAKE_SOURCE_DIR}/test/classes/org/jpype/JPypeTest.class")

    add_custom_command(
        OUTPUT "${TEST_SENTINEL}"
        COMMAND "${ANT_EXECUTABLE}" -f "test/build.xml"
        WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
        DEPENDS jpype_jar  # Ensure the main org.jpype.jar is built first
        COMMENT "Building JPype Java Test Harness via Ant"
    )

    add_custom_target(build_tests ALL DEPENDS "${TEST_SENTINEL}")
endif()


# C++ Python extension
include_directories(
        native/common/include
        native/python/include
        ${JNI_INCLUDE_DIRS}
)

set(JPYPE_NATIVE_SRC
        native/common/jp_array.cpp
        native/common/jp_arrayclass.cpp
        native/common/jp_booleantype.cpp
        native/common/jp_boxedtype.cpp
        native/common/jp_buffer.cpp
        native/common/jp_buffertype.cpp
        native/common/jp_bytetype.cpp
        native/common/jp_chartype.cpp
        native/common/jp_class.cpp
        native/common/jp_classhints.cpp
        native/common/jp_classloader.cpp
        native/common/jp_classtype.cpp
        native/common/jp_context.cpp
        native/common/jp_convert.cpp
        native/common/jp_doubletype.cpp
        native/common/jp_encoding.cpp
        native/common/jp_exception.cpp
        native/common/jp_field.cpp
        native/common/jp_floattype.cpp
        native/common/jp_functional.cpp
        native/common/jp_gc.cpp
        native/common/jp_inttype.cpp
        native/common/jp_javaframe.cpp
        native/common/jp_longtype.cpp
        native/common/jp_method.cpp
        native/common/jp_methoddispatch.cpp
        native/common/jp_monitor.cpp
        native/common/jp_numbertype.cpp
        native/common/jp_objecttype.cpp
        native/common/jp_platform.cpp
        native/common/jp_primitivetype.cpp
        native/common/jp_proxy.cpp
        native/common/jp_reference_queue.cpp
        native/common/jp_shorttype.cpp
        native/common/jp_stringtype.cpp
        native/common/jp_tracer.cpp
        native/common/jp_typefactory.cpp
        native/common/jp_typemanager.cpp
        native/common/jp_value.cpp
        native/common/jp_voidtype.cpp
        native/python/jp_pythontypes.cpp
        native/python/pyjp_array.cpp
        native/python/pyjp_buffer.cpp
        native/python/pyjp_char.cpp
        native/python/pyjp_class.cpp
        native/python/pyjp_classhints.cpp
        native/python/pyjp_field.cpp
        native/python/pyjp_method.cpp
        native/python/pyjp_module.cpp
        native/python/pyjp_monitor.cpp
        native/python/pyjp_number.cpp
        native/python/pyjp_object.cpp
        native/python/pyjp_package.cpp
        native/python/pyjp_proxy.cpp
        native/python/pyjp_value.cpp
)

Python3_add_library(_jpype MODULE ${JPYPE_NATIVE_SRC})
target_link_libraries(_jpype PRIVATE Python3::Module)
# Set target properties for scikit-build export
set_target_properties(_jpype PROPERTIES
        PREFIX ""  # remove 'lib' prefix
)
set_target_properties(_jpype PROPERTIES OUTPUT_NAME "_jpype")

# Apply Windows-specific configuration
if(WIN32)
    # Define WIN32 macro (Windows-specific code)
    target_compile_definitions(_jpype PRIVATE WIN32=1)
    # Required: .pyd suffix
    set_target_properties(_jpype PROPERTIES SUFFIX ".pyd")

    # Required: export all symbols for Python module
    set_target_properties(_jpype PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)


    # Ensure /MD and exception model are set
    target_compile_options(_jpype PRIVATE /O2 /EHsc /MD)
else()
    # Additional libraries for non-Windows
    target_link_libraries(_jpype PRIVATE
            dl # dynamic linker
    )
    # even on OSX Python uses .so suffix instead of .dylib
    set_target_properties(_jpype PROPERTIES SUFFIX ".so")
endif()


# Only stage the extension into the wheel if scikit-build-core is building
if(SKBUILD)
    message(STATUS "install _jpype extension to ${DEST_EXTENSION}")
    install(TARGETS _jpype
            LIBRARY DESTINATION ${DEST_EXTENSION}
            RUNTIME DESTINATION ${DEST_EXTENSION}
    )
else()
    message(DEBUG "no skbuild env")
endif()

