cmake_minimum_required(VERSION 3.15)
project(py-d4 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

if(MSVC)
    add_compile_definitions(NOMINMAX)
    add_compile_definitions(popen=_popen)
    add_compile_definitions(pclose=_pclose)
endif()

if(POLICY CMP0097)
    cmake_policy(SET CMP0097 NEW)
endif()

# -----------------------------------------------------------------------------
# 1. Fetch (or reuse) D4
# -----------------------------------------------------------------------------
include(FetchContent)
set(FETCHCONTENT_BASE_DIR "${CMAKE_CURRENT_BINARY_DIR}/_deps" CACHE PATH "Directory for FetchContent" FORCE)

# Allow overriding with a local D4 source directory (avoids network fetch)
# Usage: cmake -DD4_LOCAL_PATH=/path/to/d4 ..
set(D4_LOCAL_PATH "" CACHE PATH "Path to a local D4 source directory (skips FetchContent clone)")

# Allow overriding with a local optree source directory (avoids network fetch)
# Usage: cmake -DOPTREE_LOCAL_PATH=/path/to/optree .. or export OPTREE_LOCAL_PATH=/path/to/optree
set(OPTREE_LOCAL_PATH "" CACHE PATH "Path to a local optree source directory")
if (DEFINED ENV{OPTREE_LOCAL_PATH} AND NOT OPTREE_LOCAL_PATH)
    set(OPTREE_LOCAL_PATH "$ENV{OPTREE_LOCAL_PATH}")
endif()

if (D4_LOCAL_PATH AND EXISTS "${D4_LOCAL_PATH}/CMakeLists.txt")
    message(STATUS "Using local D4 source at: ${D4_LOCAL_PATH}")
    FetchContent_Declare(
        d4
        SOURCE_DIR "${D4_LOCAL_PATH}"
    )
else()
    # Propagate token to optree (D4's internal dependency) environment variable
    if (DEFINED ENV{GITLAB_TOKEN_LOGICAL} AND NOT DEFINED ENV{GITLAB_TOKEN})
        set(ENV{GITLAB_TOKEN} "$ENV{GITLAB_TOKEN_LOGICAL}")
    elseif (DEFINED ENV{GITLAB_TOKEN_D4} AND NOT DEFINED ENV{GITLAB_TOKEN})
        set(ENV{GITLAB_TOKEN} "$ENV{GITLAB_TOKEN_D4}")
    endif()

    if (DEFINED ENV{GITLAB_TOKEN} AND NOT DEFINED ENV{GITLAB_TOKEN_LOGICAL})
        set(ENV{GITLAB_TOKEN_LOGICAL} "$ENV{GITLAB_TOKEN}")
    elseif (DEFINED ENV{GITLAB_TOKEN_D4} AND NOT DEFINED ENV{GITLAB_TOKEN_LOGICAL})
        set(ENV{GITLAB_TOKEN_LOGICAL} "$ENV{GITLAB_TOKEN_D4}")
    endif()

    # Inject token or custom URL from environment variable if available
    if (DEFINED ENV{D4_GIT_URL} AND NOT "$ENV{D4_GIT_URL}" STREQUAL "")
        set(D4_GIT_URL "$ENV{D4_GIT_URL}")
        message(STATUS "Using custom D4 Git URL from environment variable: ${D4_GIT_URL}")
    elseif (DEFINED ENV{GITLAB_TOKEN_D4})
        set(D4_GIT_URL "https://oauth2:$ENV{GITLAB_TOKEN_D4}@gitlab.univ-artois.fr/logical/d4.git")
        message(STATUS "Using authenticated GitLab URL for D4 (token from env GITLAB_TOKEN_D4)")
    elseif (DEFINED ENV{GITLAB_TOKEN_LOGICAL})
        set(D4_GIT_URL "https://oauth2:$ENV{GITLAB_TOKEN_LOGICAL}@gitlab.univ-artois.fr/logical/d4.git")
        message(STATUS "Using authenticated GitLab URL for D4 (token from env GITLAB_TOKEN_LOGICAL)")
    else()
        set(D4_GIT_URL "https://gitlab.univ-artois.fr/logical/d4.git")
        message(STATUS "No GITLAB_TOKEN_D4 or GITLAB_TOKEN_LOGICAL env variable set — using unauthenticated URL for D4")
    endif()

    # Allow overriding D4 git tag/branch via CMake variable or environment variable
    set(D4_GIT_TAG "master" CACHE STRING "Git tag/branch for fetching D4")
    if (DEFINED ENV{D4_GIT_TAG} AND NOT "$ENV{D4_GIT_TAG}" STREQUAL "")
        set(D4_GIT_TAG "$ENV{D4_GIT_TAG}")
    endif()
    message(STATUS "D4 Git tag/branch set to: ${D4_GIT_TAG}")

    FetchContent_Declare(
        d4
        GIT_REPOSITORY ${D4_GIT_URL}
        GIT_TAG        ${D4_GIT_TAG}
        GIT_SHALLOW    TRUE
        GIT_SUBMODULES ""
    )
endif()

# Fetch, patch and integrate D4 as a subdirectory.
FetchContent_GetProperties(d4)
if(NOT d4_POPULATED)
    # CMP0169: FetchContent_Populate with declared details was deprecated in
    # CMake 3.30. We set the policy to OLD so that the call below continues
    # to work until we can migrate to a cleaner solution.
    if(POLICY CMP0169)
        cmake_policy(SET CMP0169 OLD)
    endif()
    FetchContent_Populate(d4)

    set(D4_CMAKELISTS "${d4_SOURCE_DIR}/CMakeLists.txt")
    if(EXISTS "${D4_CMAKELISTS}")
        file(READ "${D4_CMAKELISTS}" _d4_cmake)

        # Patch: do not force static ZLIB inside the python wrapper (which compiles a shared module)
        string(REPLACE
            "set(ZLIB_USE_STATIC_LIBS ON)"
            "if(CMAKE_PROJECT_NAME STREQUAL \"D4\")\n            set(ZLIB_USE_STATIC_LIBS ON)\n        endif()"
            _d4_cmake "${_d4_cmake}")

        # Patch: use local optree source directory if specified and exists
        if (OPTREE_LOCAL_PATH AND EXISTS "${OPTREE_LOCAL_PATH}/CMakeLists.txt")
            string(REPLACE
                "GIT_REPOSITORY \${OPTREE_GIT_URL}\n    GIT_TAG        main"
                "SOURCE_DIR \"${OPTREE_LOCAL_PATH}\""
                _d4_cmake "${_d4_cmake}")
        endif()

        # Patch: remove unused variable 'nb' in DpllStyleMethod.hpp to avoid compiler warnings
        set(D4_DPLL_STYLE_METHOD "${d4_SOURCE_DIR}/src/methods/DpllStyleMethod.hpp")
        if(EXISTS "${D4_DPLL_STYLE_METHOD}")
            file(READ "${D4_DPLL_STYLE_METHOD}" _dpll_content)
            string(REPLACE
                "unsigned nb = 0, sizeAssum = m_solver->sizeAssumption();"
                "unsigned sizeAssum = m_solver->sizeAssumption();"
                _dpll_content "${_dpll_content}")
            string(REPLACE
                "int tmp = m_nbCallCall;"
                "// int tmp = m_nbCallCall;"
                _dpll_content "${_dpll_content}")
            file(WRITE "${D4_DPLL_STYLE_METHOD}" "${_dpll_content}")
        endif()

        file(WRITE "${D4_CMAKELISTS}" "${_d4_cmake}")
    endif()

    # Patch D4 option headers to fix include paths on older branches (like master)
    file(GLOB _option_files "${d4_SOURCE_DIR}/src/options/methods/Option*.hpp")
    foreach(_opt_file ${_option_files})
        if(EXISTS "${_opt_file}")
            file(READ "${_opt_file}" _opt_content)
            string(REPLACE
                "options/solvers/OptionSolver.hpp"
                "solvers/OptionSolver.hpp"
                _opt_content "${_opt_content}")
            file(WRITE "${_opt_file}" "${_opt_content}")
        endif()
    endforeach()

    add_subdirectory(${d4_SOURCE_DIR} ${d4_BINARY_DIR})
    add_subdirectory("${d4_SOURCE_DIR}/3rdParty/cadical" "${CMAKE_CURRENT_BINARY_DIR}/cadical_build")
    target_compile_definitions(cadical PRIVATE NCLOSEFROM)
endif()

message(STATUS "D4 source dir: ${d4_SOURCE_DIR}")
message(STATUS "D4 binary dir: ${d4_BINARY_DIR}")

# -----------------------------------------------------------------------------
# 2. Setup Nanobind & Python
# -----------------------------------------------------------------------------
find_package(Python 3.8 COMPONENTS Interpreter Development.Module REQUIRED)

find_package(nanobind CONFIG QUIET)
if (NOT nanobind_FOUND)
    message(STATUS "nanobind not found. Fetching it...")
    FetchContent_Declare(
        nanobind
        GIT_REPOSITORY https://github.com/wjakob/nanobind.git
        GIT_TAG        v2.0.0
    )
    FetchContent_MakeAvailable(nanobind)
endif()

# Find or fetch nlohmann_json
find_package(nlohmann_json QUIET)
if (NOT nlohmann_json_FOUND)
    message(STATUS "nlohmann_json not found. Fetching it...")
    FetchContent_Declare(
        json
        GIT_REPOSITORY https://github.com/nlohmann/json.git
        GIT_TAG        v3.11.3
    )
    FetchContent_MakeAvailable(json)
endif()

# -----------------------------------------------------------------------------
# 3. Auto-generate C++ definitions and Python bindings
# -----------------------------------------------------------------------------
add_custom_target(
    py_d4_generate_bindings
    COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_binding_hpp.py" "${d4_SOURCE_DIR}"
    COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_bindings.py" "${d4_SOURCE_DIR}"
    COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_runner_bindings.py" "${d4_SOURCE_DIR}"
    COMMENT "Auto-generating options C++ definitions and Python bindings..."
    VERBATIM
)

# -----------------------------------------------------------------------------
# 4. Define Nanobind Module
# -----------------------------------------------------------------------------
nanobind_add_module(
    _py_d4
    NB_STATIC
    src/wrapper.cpp
    src/generated_bindings.cpp
    src/generated_runner_bindings.cpp
    "${d4_SOURCE_DIR}/api/solver/Solver.cpp"
    "${d4_SOURCE_DIR}/c++/parser/ParserDimacs.cpp"
    "${d4_SOURCE_DIR}/c++/parser/ParserCircuit.cpp"
)

# Ensure bindings are generated before compiling the module
add_dependencies(_py_d4 py_d4_generate_bindings)

# Link against the d4::d4 target, nlohmann_json, and cadical
target_link_libraries(_py_d4 PRIVATE d4::d4 nlohmann_json::nlohmann_json cadical)

if(WIN32)
    # Link psapi for GetProcessMemoryInfo used in CaDiCaL
    target_link_libraries(_py_d4 PRIVATE psapi)
    
    # Link gmpxx on Windows (needed for GMP ostream operators)
    find_library(GMPXX_LIBRARY NAMES gmpxx libgmpxx REQUIRED)
    target_link_libraries(_py_d4 PRIVATE ${GMPXX_LIBRARY})
endif()

# Include paths: our src/ + D4's source tree (for headers)
target_include_directories(_py_d4 PRIVATE
    "${CMAKE_CURRENT_SOURCE_DIR}/src"
    "${d4_SOURCE_DIR}/src"
)
install(TARGETS _py_d4 DESTINATION py_d4)

# Generate Python autocomplete/stub files (.pyi) post-build
add_custom_command(
    TARGET _py_d4 POST_BUILD
    COMMAND "${Python_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_stubs.py" _py_d4 "${CMAKE_CURRENT_BINARY_DIR}"
    WORKING_DIRECTORY $<TARGET_FILE_DIR:_py_d4>
    COMMENT "Generating autocomplete/type stub file (_py_d4.pyi)..."
    VERBATIM
)

# Install the generated stub file
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_py_d4.pyi" DESTINATION py_d4 OPTIONAL)
