cmake_minimum_required(VERSION 3.24)
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" sprite_version LIMIT_COUNT 1)
# CMake accepts the numeric release; Python metadata retains the PEP 440 suffix.
string(REGEX MATCH "^[0-9]+\\.[0-9]+(\\.[0-9]+)?" sprite_release "${sprite_version}")
project(Sprite VERSION ${sprite_release} LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
set(default_pakcs AUTO)
if(DEFINED ENV{PAKCS})
  set(default_pakcs "$ENV{PAKCS}")
endif()
set(default_frontend AUTO)
if(DEFINED ENV{CURRY_FRONTEND})
  set(default_frontend "$ENV{CURRY_FRONTEND}")
endif()
set(WITH_PAKCS "${default_pakcs}" CACHE STRING "Optional PAKCS oracle")
set(WITH_CURRY_FRONTEND "${default_frontend}" CACHE STRING "Curry front end (2.0.0)")
set(WITH_ICURRY "$ENV{ICURRY}" CACHE FILEPATH "Optional icurry translator")
set(WITH_CCACHE "$ENV{CCACHE}" CACHE FILEPATH "Optional compiler launcher (ccache)")
set(default_postinstall "${CMAKE_CXX_COMPILER}")
if(DEFINED ENV{CXX_POSTINSTALL})
  set(default_postinstall "$ENV{CXX_POSTINSTALL}")
endif()
set(WITH_CXX_POSTINSTALL "${default_postinstall}" CACHE FILEPATH "Compiler for generated modules")
set(CURRY2ICURRY_TOOL frontend CACHE STRING "Default translation route")
set(GC wdgc CACHE STRING "Collector: wdgc or mps")
set_property(CACHE GC PROPERTY STRINGS wdgc mps)
option(TRACE "Enable computation tracing" OFF)
option(COUNTERS "Enable scheduler counters" OFF)
option(GC_WRITE_COUNTERS "Count writes into old nodes" OFF)
option(ENABLE_ICURRY_CACHE "Enable the user ICurry cache by default" OFF)
option(ENABLE_PARSED_JSON_CACHE "Enable the parsed JSON cache by default" OFF)
option(SPRITE_REBUILD_ICY "Rebuild pinned ICurry products" OFF)
set(JOBS auto CACHE STRING "Parallelism of the Curry library build")
if(NOT JOBS MATCHES "^([1-9][0-9]*|auto)$")
  message(FATAL_ERROR "JOBS must be a positive integer or auto")
endif()
if(NOT CURRY2ICURRY_TOOL MATCHES "^(frontend|icurry)$")
  message(FATAL_ERROR "CURRY2ICURRY_TOOL must be frontend or icurry")
endif()
if(CURRY2ICURRY_TOOL STREQUAL icurry AND NOT WITH_ICURRY)
  message(FATAL_ERROR "CURRY2ICURRY_TOOL=icurry needs WITH_ICURRY")
endif()
if(WITH_ICURRY)
  find_program(SPRITE_ICURRY NAMES "${WITH_ICURRY}" REQUIRED NO_CACHE)
  set(WITH_ICURRY "${SPRITE_ICURRY}")
endif()
if(NOT GC MATCHES "^(wdgc|mps)$")
  message(FATAL_ERROR "GC must be wdgc or mps")
endif()
find_package(Python 3.11.4 REQUIRED COMPONENTS Interpreter Development.Module)
set(PYBIND11_FINDPYTHON ON)
add_subdirectory(extern/pybind11 EXCLUDE_FROM_ALL)
find_package(Threads REQUIRED)
if(WITH_CCACHE)
  find_program(SPRITE_CCACHE NAMES "${WITH_CCACHE}" REQUIRED NO_CACHE)
  set(WITH_CCACHE "${SPRITE_CCACHE}")
  set(CMAKE_C_COMPILER_LAUNCHER "${SPRITE_CCACHE}")
  set(CMAKE_CXX_COMPILER_LAUNCHER "${SPRITE_CCACHE}")
endif()
if(WITH_PAKCS STREQUAL "AUTO")
  find_program(SPRITE_PAKCS NAMES pakcs HINTS "${CMAKE_SOURCE_DIR}/install/tools")
  set(WITH_PAKCS "")
  if(SPRITE_PAKCS)
    file(REAL_PATH "${SPRITE_PAKCS}" WITH_PAKCS)
  endif()
endif()
if(WITH_CURRY_FRONTEND STREQUAL "AUTO")
  set(WITH_CURRY_FRONTEND "")
  get_filename_component(pakcs_bin "${WITH_PAKCS}" DIRECTORY)
  find_program(SPRITE_FRONTEND NAMES pakcs-frontend curry-frontend
    HINTS "$ENV{SPRITE_FRONTEND}" "${pakcs_bin}" "${CMAKE_SOURCE_DIR}/install/tools")
  if(DEFINED ENV{SPRITE_FRONTEND})
    set(WITH_CURRY_FRONTEND "$ENV{SPRITE_FRONTEND}")
  elseif(SPRITE_FRONTEND)
    file(REAL_PATH "${SPRITE_FRONTEND}" WITH_CURRY_FRONTEND)
  endif()
endif()
if(NOT WITH_CURRY_FRONTEND)
  message(FATAL_ERROR "Building library interfaces needs the Curry front end. Set WITH_CURRY_FRONTEND or SPRITE_FRONTEND.")
endif()
foreach(tool WITH_PAKCS WITH_CURRY_FRONTEND WITH_ICURRY WITH_CXX_POSTINSTALL)
  if(${tool})
    unset(resolved_tool)
    find_program(resolved_tool NAMES "${${tool}}" REQUIRED NO_CACHE)
    if(tool STREQUAL "WITH_CXX_POSTINSTALL")
      # A compiler alias can be a ccache symlink; its basename matters.
      set(${tool} "${resolved_tool}")
    else()
      file(REAL_PATH "${resolved_tool}" ${tool})
    endif()
  endif()
endforeach()
execute_process(COMMAND "${Python_EXECUTABLE}" -c "import json, zlib"
  COMMAND_ERROR_IS_FATAL ANY)
execute_process(COMMAND "${WITH_CURRY_FRONTEND}" --numeric-version
  OUTPUT_VARIABLE frontend_version OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
if(NOT frontend_version STREQUAL "2.0.0")
  message(FATAL_ERROR "Expected Curry front end 2.0.0, found ${frontend_version}")
endif()
if(WITH_PAKCS)
  execute_process(COMMAND "${WITH_PAKCS}" --compiler-name OUTPUT_VARIABLE pakcs_name
    OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
  if(NOT pakcs_name STREQUAL "pakcs")
    message(FATAL_ERROR "WITH_PAKCS must name the PAKCS Curry system")
  endif()
  execute_process(COMMAND "${WITH_PAKCS}" --numeric-version OUTPUT_VARIABLE pakcs_version
    OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
  if(NOT pakcs_version STREQUAL "3.4.1")
    message(FATAL_ERROR "Expected PAKCS 3.4.1, found ${pakcs_version}")
  endif()
endif()

if(WITH_ICURRY)
  set(probe "${CMAKE_CURRENT_BINARY_DIR}/icurry-probe")
  file(MAKE_DIRECTORY "${probe}")
  file(REMOVE_RECURSE "${probe}/.curry")
  file(WRITE "${probe}/sample.curry" "main :: Int\nmain = 42\n")
  execute_process(COMMAND "${WITH_ICURRY}" sample WORKING_DIRECTORY "${probe}"
    RESULT_VARIABLE icurry_result OUTPUT_VARIABLE icurry_output ERROR_VARIABLE icurry_error)
  if(NOT icurry_result EQUAL 0 OR NOT EXISTS "${probe}/.curry/pakcs-3.4.1/sample.icy")
    message(FATAL_ERROR "WITH_ICURRY failed its translation probe: ${icurry_output}${icurry_error}")
  endif()
endif()

# gc/*.cpp are include fragments selected by memory.cpp, not translation units.
# Keep the historical translation-unit order: the module registry is
# initialized before the builtin modules register their tables.
file(GLOB runtime_sources CONFIGURE_DEPENDS src/curryrt/*.cpp)
foreach(dir currylib currylib/prelude graph state)
  file(GLOB sources CONFIGURE_DEPENDS "src/curryrt/${dir}/*.cpp")
  list(APPEND runtime_sources ${sources})
endforeach()
set(image "${CMAKE_CURRENT_BINARY_DIR}/stage")
add_library(curryrt SHARED ${runtime_sources})
target_include_directories(curryrt PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_link_libraries(curryrt PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
# Linux linkage is retained here; Windows is a later portability step.
target_link_options(curryrt PRIVATE -Wl,-Bsymbolic-functions -Wl,--no-undefined)
set_target_properties(curryrt PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${image}/lib")
if(GC STREQUAL mps)
  if(NOT EXISTS "${CMAKE_SOURCE_DIR}/extern/mps/code/mps.h")
    message(FATAL_ERROR "GC=mps needs the extern/mps submodule")
  endif()
  target_sources(curryrt PRIVATE src/curryrt/graph/mpsgc.c)
  target_include_directories(curryrt PRIVATE extern/mps/code)
  target_compile_definitions(curryrt PUBLIC SPRITE_GC_MPS PRIVATE CONFIG_VAR_HOT)
elseif(GC_WRITE_COUNTERS)
  target_compile_definitions(curryrt PUBLIC SPRITE_GC_WRITE_COUNTERS)
endif()
if(TRACE)
  target_compile_definitions(curryrt PUBLIC SPRITE_TRACE_ENABLED)
endif()
if(COUNTERS)
  target_compile_definitions(curryrt PUBLIC SPRITE_SCHEDULER_COUNTERS)
endif()
file(GLOB binding_sources CONFIGURE_DEPENDS src/python/backends/cxx/curryrtbindings/*.cpp)
pybind11_add_module(_curryrtbindings MODULE NO_EXTRAS ${binding_sources})
target_link_libraries(_curryrtbindings PRIVATE curryrt)
foreach(target curryrt _curryrtbindings)
  target_compile_options(${target} PRIVATE -Wall -Wno-register
    "$<$<CONFIG:Release>:-O2;-fno-stack-protector;-fno-plt;-fno-semantic-interposition>")
endforeach()
set_target_properties(_curryrtbindings PROPERTIES
  LIBRARY_OUTPUT_DIRECTORY "${image}/python/curry/backends/cxx/curryrtbindings"
  BUILD_WITH_INSTALL_RPATH TRUE
  INSTALL_RPATH "$ORIGIN/../../../../../lib;$ORIGIN/../../../_sprite/lib")

# Immutable runtime facts contain no installation prefix. The Python step
# prepares the traditional tree using these facts and the just-built package.
execute_process(COMMAND "${WITH_CXX_POSTINSTALL}" -dumpfullversion
  OUTPUT_VARIABLE compiler_version OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
execute_process(COMMAND "${WITH_CXX_POSTINSTALL}" -dumpmachine
  OUTPUT_VARIABLE compiler_target OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
set(cxx_flavor release)
if(CMAKE_BUILD_TYPE STREQUAL Debug)
  set(cxx_flavor debug)
endif()
set(write_counters 0)
if(GC STREQUAL wdgc AND GC_WRITE_COUNTERS)
  set(write_counters 1)
endif()
execute_process(COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/curry/module_names.py"
  OUTPUT_VARIABLE module_names OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ERROR_IS_FATAL ANY)
set(unsupported "Curry.Compiler.Distribution Data.IORef Debug.Trace System.CPUTime System.Environment System.IO System.IO.Unsafe Test.Prop Test.Prop.Types")
file(MAKE_DIRECTORY "${image}/sysconfig")
function(sprite_var name value)
  file(CONFIGURE OUTPUT "${image}/sysconfig/${name}" CONTENT "${value}\n" @ONLY)
endfunction()
sprite_var(curry2icurry_tool "${CURRY2ICURRY_TOOL}")
sprite_var(currylib_module_names "${module_names}")
sprite_var(currylib_unsupported_modules "${unsupported}")
sprite_var(currylib_version "3.4.1")
sprite_var(cxx_compiler "${compiler_version} ${compiler_target}")
sprite_var(cxx_flavor "${cxx_flavor}")
sprite_var(cxx_gc "${GC}")
sprite_var(cxx_gc_write_counters "${write_counters}")
sprite_var(default_sprite_cache_file "cache.db")
foreach(name ENABLE_ICURRY_CACHE ENABLE_PARSED_JSON_CACHE)
  string(TOLOWER "${name}" resource)
  if(${name})
    sprite_var(${resource} "1")
  else()
    sprite_var(${resource} "0")
  endif()
endforeach()
sprite_var(frontend_flags "--extended -D__PAKCS__=304")
sprite_var(frontend_subdir "pakcs-3.4.1")
sprite_var(intermediate_subdir "sprite-pakcs-3.4.1")
find_program(READELF readelf)
set(ld_interpreter "")
if(READELF)
  execute_process(COMMAND "${READELF}" -l "${Python_EXECUTABLE}" OUTPUT_VARIABLE elf_headers)
  string(REGEX MATCH "Requesting program interpreter: ([^]]+)" unused "${elf_headers}")
  set(ld_interpreter "${CMAKE_MATCH_1}")
endif()
sprite_var(ld_interpreter_path "${ld_interpreter}")
sprite_var(python_package_name "curry")
set(wheel_args "")
if(SKBUILD)
  list(APPEND wheel_args --wheel)
endif()
add_custom_target(currylib ALL
  COMMAND "${Python_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/scripts/build-package.py"
    --source "${CMAKE_SOURCE_DIR}" --prefix "${image}"
    --frontend "${WITH_CURRY_FRONTEND}" --pakcs "${WITH_PAKCS}"
    --icurry "${WITH_ICURRY}" --cxx "${WITH_CXX_POSTINSTALL}"
    --ccache "${WITH_CCACHE}" ${wheel_args} --jobs "${JOBS}" --rebuild-icy "${SPRITE_REBUILD_ICY}"
  DEPENDS _curryrtbindings USES_TERMINAL VERBATIM)
if(SKBUILD)
  install(FILES LICENSE DESTINATION curry/_sprite/licenses)
  install(FILES extern/pybind11/LICENSE DESTINATION curry/_sprite/licenses RENAME pybind11-LICENSE)
  install(DIRECTORY "${image}/python/curry/" DESTINATION curry
    PATTERN __pycache__ EXCLUDE)
  foreach(dir lib include sysconfig curry)
    install(DIRECTORY "${image}/${dir}" DESTINATION curry/_sprite
      PATTERN "*.gch" EXCLUDE PATTERN __pycache__ EXCLUDE)
  endforeach()
else()
  install(DIRECTORY "${image}/" DESTINATION . USE_SOURCE_PERMISSIONS
    PATTERN "*.gch" EXCLUDE)
  install(CODE "
    execute_process(COMMAND \"${Python_EXECUTABLE}\" \"${CMAKE_SOURCE_DIR}/scripts/install-package.py\"
      \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}\" COMMAND_ERROR_IS_FATAL ANY)
  ")
endif()
