cmake_minimum_required(VERSION 3.18)

# Read version from VERSION.txt (single source of truth)
# NOTE: named VERSION.txt (not VERSION) to avoid clashing with the
#       C++20 <version> header on case-insensitive filesystems (macOS).
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION.txt" _version_raw)
string(STRIP "${_version_raw}" UFSECP_VERSION)

# Ensure 3-component semver (CMake needs MAJOR.MINOR.PATCH)
if(NOT UFSECP_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+")
    string(APPEND UFSECP_VERSION ".0")
endif()

project(UltrafastSecp256k1
    VERSION ${UFSECP_VERSION}
    DESCRIPTION "Ultra high-performance secp256k1 elliptic curve cryptography library"
    LANGUAGES CXX C
)

# Project metadata
set(PROJECT_HOMEPAGE_URL "https://github.com/shrec/UltrafastSecp256k1")
set(PROJECT_LICENSE "MIT")

# C++20 required
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Generate version header from template
configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.hpp.in"
    "${CMAKE_CURRENT_BINARY_DIR}/include/secp256k1/version.hpp"
    @ONLY
)

# Build options
option(SECP256K1_BUILD_CPU "Build CPU implementation" ON)
option(SECP256K1_BUILD_CUDA "Build CUDA GPU support" OFF)
option(SECP256K1_BUILD_ROCM "Build ROCm/HIP GPU support (AMD)" OFF)
option(SECP256K1_BUILD_OPENCL "Build OpenCL support" OFF)
option(SECP256K1_BUILD_METAL "Build Apple Metal GPU support" OFF)
option(SECP256K1_BUILD_TESTS "Build test suite" ON)
option(SECP256K1_BUILD_BENCH "Build benchmarks" ON)
option(SECP256K1_BUILD_EXAMPLES "Build example programs" ON)
option(SECP256K1_BUILD_JAVA "Build Java JNI bindings" ON)
option(SECP256K1_USE_ASM "Enable assembly optimizations (x64/RISC-V)" ON)

# Installation options
option(SECP256K1_INSTALL "Generate install target" ON)
option(SECP256K1_INSTALL_PKGCONFIG "Install pkg-config file" ON)

# Performance/safety tradeoffs
option(SECP256K1_SPEED_FIRST "Prioritize speed over safety checks" OFF)
option(SECP256K1_BUILD_SHARED "Build shared library" OFF)

# Bitcoin strict mode (default ON): public API rejects non-canonical inputs
option(UFSECP_BITCOIN_STRICT "Enforce BIP-340 strict encoding in public API (reject r>=p, s>=n)" ON)

# Ethereum module (default ON): Keccak-256, EIP-55, EIP-191, EIP-155, ecrecover
# Set OFF to exclude Ethereum functionality (Bitcoin-only build)
option(SECP256K1_BUILD_ETHEREUM "Build Ethereum module (Keccak, EIP-55/155/191, ecrecover)" ON)

# CT enforcement: deprecate non-CT signing functions at compile time
option(SECP256K1_REQUIRE_CT "Deprecate non-CT sign functions (compile warnings on fast:: signing)" OFF)

# x86-64 -march override: use "x86-64-v3" in CI with ccache to avoid SIGILL
# when cached objects from a different runner CPU are reused.  Empty = native.
set(SECP256K1_MARCH "" CACHE STRING "x86-64 -march override (empty = auto-detect)")

# Warning policy: promote warnings to errors (recommended for CI)
option(SECP256K1_WERROR "Treat compiler warnings as errors (-Werror / /WX)" OFF)
option(UFSECP_REFRESH_SOURCE_GRAPH "Refresh the repo source graph during builds" ON)

find_package(Python3 COMPONENTS Interpreter QUIET)

# Global compile definitions
if(SECP256K1_SPEED_FIRST)
    add_compile_definitions(SECP256K1_FAST_NO_SECURITY_CHECKS=1)
endif()

if(SECP256K1_REQUIRE_CT)
    add_compile_definitions(SECP256K1_REQUIRE_CT=1)
    message(STATUS "secp256k1-fast: CT enforcement enabled -- non-CT sign functions are deprecated")
endif()

if(UFSECP_BITCOIN_STRICT)
    add_compile_definitions(UFSECP_BITCOIN_STRICT=1)
endif()

if(SECP256K1_BUILD_ETHEREUM)
    add_compile_definitions(SECP256K1_BUILD_ETHEREUM=1)
    message(STATUS "secp256k1-fast: Ethereum module enabled (Keccak, EIP-55/155/191, ecrecover)")
else()
    message(STATUS "secp256k1-fast: Ethereum module disabled (Bitcoin-only build)")
endif()

# GLV window width override (4-7). Empty = platform default (5 on x86/ARM/RISC-V, 4 on ESP32/WASM).
set(SECP256K1_GLV_WINDOW_WIDTH "" CACHE STRING "GLV window width (4-7, empty = platform default)")
if(SECP256K1_GLV_WINDOW_WIDTH)
    if(NOT SECP256K1_GLV_WINDOW_WIDTH MATCHES "^[0-9]+$")
        message(FATAL_ERROR
            "SECP256K1_GLV_WINDOW_WIDTH must be an integer in [4,7]. Got: '${SECP256K1_GLV_WINDOW_WIDTH}'")
    endif()
    if(SECP256K1_GLV_WINDOW_WIDTH LESS 4 OR SECP256K1_GLV_WINDOW_WIDTH GREATER 7)
        message(FATAL_ERROR
            "SECP256K1_GLV_WINDOW_WIDTH out of range [4,7]. Got: ${SECP256K1_GLV_WINDOW_WIDTH}")
    endif()
    add_compile_definitions(SECP256K1_GLV_WINDOW_WIDTH=${SECP256K1_GLV_WINDOW_WIDTH})
    message(STATUS "secp256k1-fast: GLV window width override: ${SECP256K1_GLV_WINDOW_WIDTH}")
endif()

# Resolve effective target architecture (handles macOS cross-compilation).
# On macOS, CMAKE_SYSTEM_PROCESSOR reflects the HOST, not the TARGET when
# cross-compiling via CMAKE_OSX_ARCHITECTURES (e.g. ARM64 host -> x86_64 target).
# See: https://github.com/shrec/UltrafastSecp256k1/issues/101
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
    if(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64")
        set(_SECP_TARGET_ARCH "x86_64")
    elseif(CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
        set(_SECP_TARGET_ARCH "arm64")
    else()
        set(_SECP_TARGET_ARCH "${CMAKE_SYSTEM_PROCESSOR}")
    endif()
else()
    set(_SECP_TARGET_ARCH "${CMAKE_SYSTEM_PROCESSOR}")
endif()

# Platform detection
if(_SECP_TARGET_ARCH MATCHES "x86_64|AMD64|X64")
    set(SECP256K1_PLATFORM "x86_64")
elseif(_SECP_TARGET_ARCH MATCHES "riscv64|RISCV64")
    set(SECP256K1_PLATFORM "riscv64")
elseif(_SECP_TARGET_ARCH MATCHES "aarch64|ARM64|arm64")
    set(SECP256K1_PLATFORM "aarch64")
else()
    set(SECP256K1_PLATFORM "generic")
endif()

message(STATUS "secp256k1-fast: Detected platform: ${SECP256K1_PLATFORM}")

# Compiler-specific flags
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    # Platform-specific -march flag
    if(SECP256K1_PLATFORM STREQUAL "riscv64")
        set(MARCH_FLAG "-march=rv64gc_zba_zbb")
    elseif(SECP256K1_PLATFORM STREQUAL "x86_64")
        if(SECP256K1_MARCH)
            set(MARCH_FLAG "-march=${SECP256K1_MARCH}")
        elseif(APPLE AND CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64" AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|X64")
            # macOS cross-compilation (ARM64 host -> single-arch x86_64 target)
            set(MARCH_FLAG "-march=x86-64-v3")
        else()
            set(MARCH_FLAG "-march=native")
        endif()
    else()
        set(MARCH_FLAG "")
    endif()
    
    add_compile_options(
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wall>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wextra>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wpedantic>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wconversion>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wshadow>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wformat=2>
        $<$<COMPILE_LANGUAGE:C,CXX>:-Wundef>
        $<$<COMPILE_LANGUAGE:C,CXX>:${MARCH_FLAG}>
        $<$<AND:$<CONFIG:Release>,$<COMPILE_LANGUAGE:C,CXX>>:-O3>
        $<$<AND:$<CONFIG:Release>,$<COMPILE_LANGUAGE:C,CXX>>:-DNDEBUG>
    )
    if(SECP256K1_WERROR)
        add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Werror>)
    endif()
    # GCC warns about unsigned __int128 under -Wpedantic (ISO C++ extension).
    # This project fundamentally requires __int128 for 64-bit ECC arithmetic.
    # Clang treats __int128 as a built-in type and does not warn.
    if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Wno-pedantic>)
    endif()
    # macOS SDK headers (CFBase.h) trigger -Welaborated-enum-base with -Wpedantic
    if(APPLE)
        add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Wno-elaborated-enum-base>)
    endif()
elseif(MSVC)
    add_compile_options(
        /W4
        /permissive-
        $<$<CONFIG:Release>:/O2>
        $<$<CONFIG:Release>:/GL>
    )
    if(SECP256K1_WERROR)
        add_compile_options(/WX)
    endif()
endif()

# Subdirectories
if(SECP256K1_BUILD_CPU)
    # Enable testing so cpu/CMakeLists.txt BUILD_TESTING guard works
    if(SECP256K1_BUILD_TESTS OR SECP256K1_BUILD_BENCH)
        set(BUILD_TESTING ON CACHE BOOL "Enable testing" FORCE)
        include(CTest)   # generates DartConfiguration.tcl for -T MemCheck
    endif()
    add_subdirectory(cpu)
endif()

if(SECP256K1_BUILD_CUDA)
    # On some platforms (notably Windows), nvcc requires an MSVC host compiler (cl.exe).
    # If it's not available, we'd rather fall back to CPU-only than fail configuration.
    include(CheckLanguage)
    check_language(CUDA)
    if(CMAKE_CUDA_COMPILER)
        enable_language(CUDA)
        add_subdirectory(cuda)
    else()
        message(WARNING "SECP256K1_BUILD_CUDA=ON but CUDA toolchain was not found or failed to configure (e.g. nvcc cannot find cl.exe). Falling back to CPU-only build.")
    endif()
endif()

if(SECP256K1_BUILD_OPENCL)
    find_package(OpenCL QUIET)
    if(NOT OpenCL_FOUND AND WIN32)
        # Fallback: look for system OpenCL.lib (installed by GPU drivers)
        find_library(_OCL_FALLBACK OpenCL
            PATHS "$ENV{SYSTEMROOT}/System32" "C:/Windows/System32"
        )
        if(_OCL_FALLBACK)
            set(OpenCL_FOUND TRUE)
            message(STATUS "OpenCL: using system fallback (${_OCL_FALLBACK})")
        endif()
    endif()
    if(OpenCL_FOUND OR (WIN32 AND _OCL_FALLBACK))
        add_subdirectory(opencl)
    else()
        message(WARNING "SECP256K1_BUILD_OPENCL=ON but OpenCL not found. Skipping OpenCL build.")
    endif()
endif()

# ROCm/HIP build -- reuses cuda/ sources with portable math fallbacks
if(SECP256K1_BUILD_ROCM)
    # CMake 3.21+ has native HIP language support
    cmake_minimum_required(VERSION 3.21)
    find_package(hip QUIET)
    if(hip_FOUND)
        message(STATUS "ROCm/HIP found: ${hip_VERSION}")
        enable_language(HIP)
        add_subdirectory(cuda cuda_rocm)  # reuse sources, separate build dir
    else()
        message(WARNING "SECP256K1_BUILD_ROCM=ON but HIP SDK not found. Skipping ROCm build.")
    endif()
endif()

# Apple Metal backend -- macOS / iOS / visionOS
# Host-side type tests always build; GPU runtime only on Apple
if(SECP256K1_BUILD_METAL)
    if(APPLE)
        enable_language(OBJCXX)
        find_library(_METAL_FW Metal)
        find_library(_FOUNDATION_FW Foundation)
        if(_METAL_FW AND _FOUNDATION_FW)
            message(STATUS "Metal framework found -- building Metal backend (GPU + host tests)")
            add_subdirectory(metal)
        else()
            message(WARNING "SECP256K1_BUILD_METAL=ON but Metal.framework not found. Building host tests only.")
            add_subdirectory(metal)
        endif()
    else()
        message(STATUS "SECP256K1_BUILD_METAL=ON on non-Apple platform -- building host tests only")
        add_subdirectory(metal)
    endif()
endif()

if(SECP256K1_BUILD_EXAMPLES)
    add_subdirectory(examples)
endif()

# -- GPU Host Ops Layer (Layer 2) -------------------------------------------
# Must be after cuda/, opencl/, metal/ so backend targets exist.
if(SECP256K1_BUILD_CUDA OR SECP256K1_BUILD_OPENCL OR SECP256K1_BUILD_METAL)
    add_subdirectory(gpu)
endif()

# -- Stable C ABI layer (ufsecp_*) -----------------------------------------
# Built before audit/ so that ufsecp_static is available for fuzz tests.
option(SECP256K1_BUILD_CABI "Build the stable ufsecp_* C ABI library" ON)
if(SECP256K1_BUILD_CABI AND SECP256K1_BUILD_CPU)
    add_subdirectory(include/ufsecp)
    message(STATUS "  C ABI (ufsecp):   ON")
endif()

if(SECP256K1_BUILD_JAVA AND SECP256K1_BUILD_CABI AND SECP256K1_BUILD_CPU)
    find_package(JNI QUIET)
    if(JNI_FOUND)
        add_subdirectory(bindings/java)
        message(STATUS "  Java JNI:         ON")
    else()
        message(STATUS "  Java JNI:         OFF (JNI not found)")
    endif()
endif()

# -- Audit infrastructure (standalone CTest targets + unified runner) -------
# All audit-specific targets live in audit/ to keep the library source clean.
if(SECP256K1_BUILD_CPU AND BUILD_TESTING)
    add_subdirectory(audit)
endif()

# NOTE: bench_compare removed -- use cpu/bench/bench_unified instead
#       (bench_unified has full apple-to-apple in a single binary, no FetchContent)

# -- Cross-library differential test -----------------------------------------
# Moved to audit/CMakeLists.txt -- enable with -DSECP256K1_BUILD_CROSS_TESTS=ON

# -- Parser fuzz tests ------------------------------------------------------
# Moved to audit/CMakeLists.txt -- enable with -DSECP256K1_BUILD_FUZZ_TESTS=ON

# -- MuSig2 + FROST protocol tests -----------------------------------------
# Moved to audit/CMakeLists.txt -- enable with -DSECP256K1_BUILD_PROTOCOL_TESTS=ON

# Export targets
if(SECP256K1_INSTALL)
    include(GNUInstallDirs)
    include(CMakePackageConfigHelpers)

    # Install public headers
    install(DIRECTORY cpu/include/secp256k1
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
        FILES_MATCHING PATTERN "*.hpp"
    )

    # Install generated version header
    install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/secp256k1/version.hpp"
        DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/secp256k1
    )

    # Generate package config
    write_basic_package_version_file(
        "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast-config-version.cmake"
        VERSION ${PROJECT_VERSION}
        COMPATIBILITY SameMajorVersion
    )
    
    configure_package_config_file(
        "${CMAKE_CURRENT_SOURCE_DIR}/cmake/secp256k1-fast-config.cmake.in"
        "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast-config.cmake"
        INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/secp256k1-fast
    )
    
    # Install targets
    install(
        FILES
            "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast-config.cmake"
            "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast-config-version.cmake"
        DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/secp256k1-fast
    )
    
    # Pkg-config
    if(SECP256K1_INSTALL_PKGCONFIG)
        configure_file(
            "${CMAKE_CURRENT_SOURCE_DIR}/secp256k1-fast.pc.in"
            "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast.pc"
            @ONLY
        )
        install(
            FILES "${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast.pc"
            DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig
        )
    endif()
endif()

# -- CPack packaging ---------------------------------------------------------
set(UFSECP_SOURCE_GRAPH_TOOL "${CMAKE_CURRENT_SOURCE_DIR}/tools/source_graph_kit/source_graph.py")
if(UFSECP_REFRESH_SOURCE_GRAPH)
    if(Python3_Interpreter_FOUND AND EXISTS "${UFSECP_SOURCE_GRAPH_TOOL}")
        add_custom_target(ufsecp_source_graph_refresh ALL
            COMMAND "${Python3_EXECUTABLE}" "${UFSECP_SOURCE_GRAPH_TOOL}" build -i --best-effort
            WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
            COMMENT "Refreshing UltrafastSecp256k1 source graph incrementally"
            USES_TERMINAL
            VERBATIM
        )
    else()
        message(STATUS "secp256k1-fast: source graph refresh disabled (missing Python3 interpreter or source_graph.py)")
    endif()
endif()

set(CPACK_PACKAGE_NAME "UltrafastSecp256k1")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "shrec")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY
    "High-performance secp256k1 ECC library with stable C ABI (ufsecp)")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/shrec/UltrafastSecp256k1")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
set(CPACK_PACKAGE_FILE_NAME
    "${CPACK_PACKAGE_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${_SECP_TARGET_ARCH}")
set(CPACK_STRIP_FILES ON)

# Generate ZIP on all platforms, plus TGZ + DEB + RPM on Unix
if(WIN32)
    set(CPACK_GENERATOR "ZIP")
else()
    set(CPACK_GENERATOR "TGZ;ZIP;DEB;RPM")
endif()

# DEB-specific
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "shrec <shrec@users.noreply.github.com>")
set(CPACK_DEBIAN_PACKAGE_SECTION "libs")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.17)")
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)

# Map target arch -> DEB architecture (critical for cross-compilation where
# dpkg --print-architecture returns the HOST arch, not the TARGET arch).
if(_SECP_TARGET_ARCH MATCHES "aarch64|ARM64|arm64")
    set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
elseif(_SECP_TARGET_ARCH MATCHES "x86_64|AMD64")
    set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
endif()

# RPM-specific
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "System Environment/Libraries")
set(CPACK_RPM_FILE_NAME RPM-DEFAULT)
set(CPACK_RPM_PACKAGE_REQUIRES "glibc >= 2.17")

# Source package
set(CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set(CPACK_SOURCE_IGNORE_FILES
    "/build[^/]*/" "/[.]git/" "/[.]github/" "/[.]vscode/" "[.]DS_Store$" "[.]user$")

include(CPack)

# Summary
message(STATUS "")
message(STATUS "+===========================================================+")
message(STATUS "|   UltrafastSecp256k1 Configuration                        |")
message(STATUS "+===========================================================+")
message(STATUS "  Version:          ${PROJECT_VERSION}")
message(STATUS "  Platform:         ${SECP256K1_PLATFORM}")
message(STATUS "  C++ Standard:     ${CMAKE_CXX_STANDARD}")
message(STATUS "  Build Type:       ${CMAKE_BUILD_TYPE}")
message(STATUS "")
message(STATUS "  Components:")
message(STATUS "    CPU:            ${SECP256K1_BUILD_CPU}")
message(STATUS "    CUDA:           ${SECP256K1_BUILD_CUDA}")
message(STATUS "    ROCm/HIP:       ${SECP256K1_BUILD_ROCM}")
message(STATUS "    OpenCL:         ${SECP256K1_BUILD_OPENCL}")
message(STATUS "    Metal:          ${SECP256K1_BUILD_METAL}")
message(STATUS "    Tests:          ${SECP256K1_BUILD_TESTS}")
message(STATUS "    Benchmarks:     ${SECP256K1_BUILD_BENCH}")

message(STATUS "    Examples:       ${SECP256K1_BUILD_EXAMPLES}")
message(STATUS "    Java JNI:       ${SECP256K1_BUILD_JAVA}")
message(STATUS "")
message(STATUS "  Optimizations:")
message(STATUS "    Assembly:       ${SECP256K1_USE_ASM}")
message(STATUS "    Speed First:    ${SECP256K1_SPEED_FIRST}")
message(STATUS "    Bitcoin Strict: ${UFSECP_BITCOIN_STRICT}")
message(STATUS "")
message(STATUS "===========================================================")
message(STATUS "")
