cmake_minimum_required(VERSION 3.21)

include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/read_project_version.cmake")
tyr_read_project_version(TYR_PROJECT_VERSION "${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml")

project(tyr VERSION "${TYR_PROJECT_VERSION}")

# Project configuration

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(GNUInstallDirs)

# Options

option(TYR_USE_LLD "Use LLVM lld with Clang when available." ON)
option(TYR_ENABLE_LTO "Enable link-time optimization for Release builds." ON)
option(
    TYR_HEADER_INSTANTIATION
    "Instantiate templates in in-tree translation units at higher compile time cost."
    OFF
)
option(TYR_ENABLE_INNER_PARALLELISM "Enable inner parallelism for expensive lifted rule evaluation." OFF)
option(TYR_ENABLE_SEMI_NAIVE "Enable semi-naive lifted Datalog evaluation." ON)
set(TYR_STATE_STORAGE_POLICY "Tree" CACHE STRING "Policy used for storing states.")
set(TYR_RELATION_STORAGE_POLICY "Word" CACHE STRING "Policy used for storing relation bindings.")
set_property(CACHE TYR_RELATION_STORAGE_POLICY PROPERTY STRINGS Word Bit)
option(TYR_BUILD_PYTYR "Build pytyr Python bindings." OFF)
option(TYR_BUILD_TESTS "Build Tyr tests." OFF)
option(TYR_BUILD_EXECUTABLES "Build Tyr executables." OFF)
option(TYR_BUILD_PROFILING "Build Tyr profiling targets." OFF)

# Build configuration

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(
        CMAKE_BUILD_TYPE "Release" CACHE STRING
        "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE
    )
endif()

add_compile_definitions(YGG_ENABLE_FMT_FORMATTERS=1)

if(TYR_HEADER_INSTANTIATION)
    add_compile_definitions(TYR_HEADER_INSTANTIATION)
endif()

if("${TYR_STATE_STORAGE_POLICY}" STREQUAL "Tree")
    add_compile_definitions(TYR_STATE_STORAGE_TREE)
elseif("${TYR_STATE_STORAGE_POLICY}" STREQUAL "Hashset")
    add_compile_definitions(TYR_STATE_STORAGE_HASHSET)
else()
    message(FATAL_ERROR "TYR_STATE_STORAGE_POLICY must be Tree or Hashset")
endif()

if(NOT TYR_RELATION_STORAGE_POLICY STREQUAL "Word" AND NOT TYR_RELATION_STORAGE_POLICY STREQUAL "Bit")
    message(FATAL_ERROR "TYR_RELATION_STORAGE_POLICY must be Word or Bit")
endif()

if(MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W1 /EHsc /bigobj /MP")
    string(APPEND CMAKE_EXE_LINKER_FLAGS " /IGNORE:4006,4044,4075")
else()
    set(CMAKE_POSITION_INDEPENDENT_CODE ON)
    add_compile_options(-Wall -Wextra)

    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g2 -ggdb")
    set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -g2 -fno-omit-frame-pointer -fno-lto")

    if(
        TYR_USE_LLD
        AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
    )
        find_program(LLD_LINKER NAMES ld.lld)
        if(LLD_LINKER)
            message(STATUS "Using lld: ${LLD_LINKER}")
            add_link_options(-fuse-ld=lld)
        else()
            message(STATUS "lld not found, falling back to default linker")
        endif()
    elseif(TYR_USE_LLD AND TYR_ENABLE_LTO AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        message(STATUS "Skipping lld with GNU because GCC LTO requires the GCC linker plugin")
    endif()

    if(TYR_ENABLE_LTO AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # Cap release LTO parallelism to reduce RAM spikes.
        set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto=2")
        set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -flto=2")
        set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -flto=2")
        set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} -flto=2")
    elseif(
        TYR_ENABLE_LTO
        AND (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
    )
        # ThinLTO reduces release compile and link costs compared with full LTO.
        set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -flto=thin")
        if(APPLE)
            set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -Wl,-mllvm,-threads=2")
            set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -Wl,-mllvm,-threads=2")
            set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} -Wl,-mllvm,-threads=2")
        else()
            set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} -Wl,--thinlto-jobs=2")
            set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} -Wl,--thinlto-jobs=2")
            set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} -Wl,--thinlto-jobs=2")
        endif()
    endif()
endif()

if(CMAKE_CONFIGURATION_TYPES)
    message(STATUS "Build configurations: ${CMAKE_CONFIGURATION_TYPES}")
else()
    message(STATUS "Build configuration: ${CMAKE_BUILD_TYPE}")
endif()

set(ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
add_definitions(-DROOT_DIR="${CMAKE_CURRENT_SOURCE_DIR}")

# Dependencies

set(TYR_YGGDRASIL_REQUIRED_VERSION 0.2.0)
set(TYR_LOKI_REQUIRED_VERSION 1.2.0)

include("${PROJECT_SOURCE_DIR}/cmake/bootstrap_pyyggdrasil.cmake")

find_package(
    yggdrasil ${TYR_YGGDRASIL_REQUIRED_VERSION}
    CONFIG REQUIRED
    PATHS ${CMAKE_PREFIX_PATH}
    NO_DEFAULT_PATH
)
if(yggdrasil_FOUND)
    message(STATUS "Found yggdrasil: ${yggdrasil_DIR} (found version ${yggdrasil_VERSION})")
endif()

yggdrasil_find_python_native_prefix(pypddl LOKI_NATIVE_PREFIX)
yggdrasil_register_native_dependency_prefix("${YGGDRASIL_NATIVE_PREFIX}")
yggdrasil_register_native_dependency_prefix("${LOKI_NATIVE_PREFIX}")
yggdrasil_register_python_native_runtime_prefix("pypddl/native" "${LOKI_NATIVE_PREFIX}")
yggdrasil_register_python_native_runtime_prefix("pyyggdrasil" "${YGGDRASIL_NATIVE_PREFIX}")

find_package(argparse CONFIG REQUIRED PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(argparse_FOUND)
    message(STATUS "Found argparse: ${argparse_DIR} (found version ${argparse_VERSION})")
endif()

find_package(Boost 1.74.0 REQUIRED COMPONENTS iostreams json PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(Boost_FOUND)
    message(STATUS "Found Boost: ${Boost_DIR} (found version ${Boost_VERSION})")
endif()

find_package(gtl CONFIG REQUIRED PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(gtl_FOUND)
    message(STATUS "Found gtl: ${gtl_DIR}")
endif()

find_package(cista CONFIG REQUIRED PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(cista_FOUND)
    message(STATUS "Found cista: ${cista_DIR} (found version ${cista_VERSION})")
endif()

find_package(fmt CONFIG REQUIRED PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(fmt_FOUND)
    message(STATUS "Found fmt: ${fmt_DIR} (found version ${fmt_VERSION})")
endif()

# Prefer -pthread over -lpthread on platforms that support it.
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(Threads_FOUND)
    message(STATUS "Found Threads: ${CMAKE_THREAD_LIBS_INIT}")
endif()

find_package(TBB CONFIG REQUIRED PATHS ${CMAKE_PREFIX_PATH} NO_DEFAULT_PATH)
if(TBB_FOUND)
    include_directories(${TBB_INCLUDE_DIRS})
    message(STATUS "Found TBB: ${TBB_DIR} (found version ${TBB_VERSION})")
endif()

find_package(
    loki ${TYR_LOKI_REQUIRED_VERSION}
    CONFIG REQUIRED COMPONENTS parsers
    PATHS ${CMAKE_PREFIX_PATH}
    NO_DEFAULT_PATH
)
if(loki_FOUND)
    message(STATUS "Found loki: ${loki_DIR} (found version ${loki_VERSION})")
endif()

# Benchmark setup

if(TYR_BUILD_TESTS OR TYR_BUILD_PROFILING)
    # Benchmark PDDL is resolved from the pypddl-datasets cache and downloaded on demand at configure time.
    find_package(Python 3.11 REQUIRED COMPONENTS Interpreter)
    execute_process(
        COMMAND
            "${Python_EXECUTABLE}"
            -c
            "import pypddl_datasets, sys; sys.stdout.write(str(pypddl_datasets.data_root()))"
        OUTPUT_VARIABLE TYR_BENCHMARKS_DIR
        RESULT_VARIABLE TYR_BENCHMARKS_RESULT
    )
    if(NOT TYR_BENCHMARKS_RESULT EQUAL 0)
        message(FATAL_ERROR "Could not resolve the benchmark data root; install 'pypddl-datasets>=0.0.12,<0.1' into the configuring Python (${Python_EXECUTABLE}).")
    endif()
    message(STATUS "Benchmark data: ${TYR_BENCHMARKS_DIR}")
    add_compile_definitions(BENCHMARKS_DIR="${TYR_BENCHMARKS_DIR}")
endif()

# Targets

add_subdirectory(src)

if(TYR_BUILD_PYTYR)
    add_subdirectory(python/src)
endif()

if(TYR_BUILD_EXECUTABLES)
    add_subdirectory(exe)
endif()

if(TYR_BUILD_PROFILING)
    add_subdirectory(profiling)
endif()

if(TYR_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# Installation and package export

include(CMakePackageConfigHelpers)

set(TYR_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/tyr")
set(TYR_INSTALL_CMAKE_HELPERDIR "${TYR_INSTALL_CMAKEDIR}/cmake")

install(
    DIRECTORY "${PROJECT_SOURCE_DIR}/include/tyr"
    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
    COMPONENT tyr
)

install(
    DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/cmake/"
    DESTINATION "${TYR_INSTALL_CMAKE_HELPERDIR}"
    COMPONENT tyr
)

write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/tyrConfigVersion.cmake"
    VERSION ${tyr_VERSION}
    COMPATIBILITY SameMinorVersion
)

configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/tyrConfig.cmake"
    INSTALL_DESTINATION "${TYR_INSTALL_CMAKEDIR}"
    NO_CHECK_REQUIRED_COMPONENTS_MACRO
)

install(
    FILES
        "${CMAKE_CURRENT_BINARY_DIR}/tyrConfig.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/tyrConfigVersion.cmake"
    DESTINATION "${TYR_INSTALL_CMAKEDIR}"
    COMPONENT tyr
)
