cmake_minimum_required(VERSION 3.16)
project(salmanoff VERSION 0.01.001 LANGUAGES CXX)

include(CMakeDependentOption)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DAPSS.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/DebugOpts.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake)
include(GNUInstallDirs)

# Set C++ standard
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Build type
if(NOT CMAKE_BUILD_TYPE)
	set(CMAKE_BUILD_TYPE Debug FORCE)
endif()

# Compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")

# Ensure installed directories use Debian-standard permissions instead of
# inheriting group-writable bits from the build tree.
set(CMAKE_INSTALL_DEFAULT_DIRECTORY_PERMISSIONS
	OWNER_READ OWNER_WRITE OWNER_EXECUTE
	GROUP_READ GROUP_EXECUTE
	WORLD_READ WORLD_EXECUTE)

# Mind oscillator configuration
set(MIND_VOSCILLATOR_PERIOD_MS 33 CACHE STRING "Mind's virtual osc clock rate (ms)")
if(NOT MIND_VOSCILLATOR_PERIOD_MS GREATER 0)
    message(FATAL_ERROR "MIND_VOSCILLATOR_PERIOD_MS must be a positive integer > 0")
endif()
math(EXPR MIND_VOSCILLATOR_FREQ_MS "1000 / ${MIND_VOSCILLATOR_PERIOD_MS}")

# Device manager reattacher configuration
set(MRNTT_DEVMGR_REATTACHER_PERIOD_MS 2000
	CACHE STRING "Device manager reattacher period (ms)")
if(NOT MRNTT_DEVMGR_REATTACHER_PERIOD_MS GREATER 0)
    message(FATAL_ERROR
		"MRNTT_DEVMGR_REATTACHER_PERIOD_MS must be a positive integer > 0")
endif()

# Stimulus buffer frame period configuration
set(STIMBUFF_FRAME_PERIOD_MS 33
	CACHE STRING "Stimulus buffer frame period (ms)")
if(NOT STIMBUFF_FRAME_PERIOD_MS GREATER 0)
    message(FATAL_ERROR
		"STIMBUFF_FRAME_PERIOD_MS must be a positive integer > 0")
endif()

# Stimulus buffer frame retry delay configuration
set(STIMBUFF_FRAME_RETRY_DELAY_MS 1
	CACHE STRING "Stimulus buffer frame retry delay (ms)")
if(NOT STIMBUFF_FRAME_RETRY_DELAY_MS GREATER 0)
    message(FATAL_ERROR
		"STIMBUFF_FRAME_RETRY_DELAY_MS must be a positive integer > 0")
endif()

# World thread configuration
option(WORLD_USE_BODY_THREAD
	"Use body thread for world component instead of separate world thread" OFF)

# Test configuration
option(ENABLE_TESTS "Enable building tests" OFF)

# Set the debug locks variable for config.h
if(ENABLE_DEBUG_LOCKS)
    set(CONFIG_ENABLE_DEBUG_LOCKS TRUE)
endif()

# Set the debug trace callables variable for config.h
if(ENABLE_DEBUG_TRACE_CALLABLES)
    set(CONFIG_DEBUG_TRACE_CALLABLES TRUE)
    # Suppress frame-address warnings when using __builtin_return_address()
	# with values above 0 (See callableTracer.h).
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-frame-address")
endif()

# Set the world thread variable for config.h
if(WORLD_USE_BODY_THREAD)
    set(CONFIG_WORLD_USE_BODY_THREAD TRUE)
endif()

# Set the timeout variable for config.h
set(CONFIG_DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS ${DEBUG_QUTEX_DEADLOCK_TIMEOUT_MS})
# Set the stimulus buffer frame period variable for config.h
set(CONFIG_STIMBUFF_FRAME_PERIOD_MS ${STIMBUFF_FRAME_PERIOD_MS})
# Set the stimulus buffer frame retry delay variable for config.h
set(CONFIG_STIMBUFF_FRAME_RETRY_DELAY_MS ${STIMBUFF_FRAME_RETRY_DELAY_MS})

# Configure config.h
configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/include/config.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/include/config.h
    @ONLY
)

# Include directories
include_directories(
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/smocore/include
    ${CMAKE_CURRENT_BINARY_DIR}/include
)

# Find core dependencies
# We cannot use header-only Boost.Asio because we need both our dlopen()'d
# libraries and the main binary to refer to the same instances of boost::asio's
# metadata. If we use header-only Boost.Asio, each dlopen()'d library will have
# its own copy of boost::asio's metadata, which will cause a segfault if
# boost::asio objects are used inside of a dlopen()'d library.
#
# Honestly, I never liked this whole "header-only" idea so I'm happy to be rid
# of it.
#
# Tell CMake we're linking against the shared library (not header-only)
set(Boost_USE_STATIC_LIBS OFF)
set(Boost_USE_HEADER_ONLY OFF)
find_package(Boost REQUIRED COMPONENTS system log)
# Define BOOST_ALL_DYN_LINK project-wide to ensure all Boost libraries use dynamic linking
add_compile_definitions(BOOST_ALL_DYN_LINK)

find_package(PkgConfig REQUIRED)
find_package(FLEX REQUIRED)
find_package(BISON REQUIRED)

# It's important to note that as of 2025-12-02, RustICL on the RPi5 suddenly
# began requiring that the user running Smo be a member of the "render" group.
# We need to take that into account when we eventually build installer packages.
# Users may also need to be members of the "video" group.

# Find OpenCL 1.2 or higher: try find_package first, fall back to pkg-config
find_package(OpenCL 1.2 QUIET)
if(OpenCL_FOUND)
	# Normalize find_package variables to match pkg_check_modules naming
	set(OPENCL_FOUND TRUE)
	set(OPENCL_INCLUDE_DIRS ${OpenCL_INCLUDE_DIRS})
	# Handle both OpenCL_LIBRARY (singular) and OpenCL_LIBRARIES (plural)
	if(OpenCL_LIBRARIES)
		set(OPENCL_LIBRARIES ${OpenCL_LIBRARIES})
	else()
		set(OPENCL_LIBRARIES ${OpenCL_LIBRARY})
	endif()
	set(OPENCL_LIBRARY_DIRS "")
	message(STATUS "Found OpenCL using find_package")

	# Check if version is available and validate
	if(OpenCL_VERSION)
		if(OpenCL_VERSION VERSION_LESS "1.2")
			message(FATAL_ERROR
				"OpenCL version ${OpenCL_VERSION} found, but 1.2 or higher is required")
		endif()
		message(STATUS "OpenCL version: ${OpenCL_VERSION}")
	else()
		message(WARNING
			"OpenCL version could not be determined. "
			"Version 1.2+ is required at runtime.")
	endif()
else()
	# Fall back to pkg-config
	pkg_check_modules(OPENCL OpenCL)
	if(NOT OPENCL_FOUND)
		message(FATAL_ERROR
			"Failed to find OpenCL: both find_package and "
			"pkg_check_modules failed. Try installing the "
			"'ocl-icd-opencl-dev' package (or the appropriate "
			"OpenCL development package for your system)."
		)
	endif()
	message(STATUS "Found OpenCL using pkg-config")
	message(WARNING
		"OpenCL version could not be determined via pkg-config. "
		"Version 1.2+ is required at runtime.")
endif()

# Need dlopen() and dlsym()
find_library(DL_LIBRARY NAMES dl ldl)
if(NOT DL_LIBRARY)
    message(FATAL_ERROR "Dynamic linking library (libdl/libldl) not found")
endif()

# Add third-party dependencies
if(ENABLE_TESTS)
    add_subdirectory(third_party)
endif()
add_subdirectory(compile)
add_subdirectory(libspinscale)
# Add core components
add_subdirectory(smocore)
add_subdirectory(commonLibs)
add_subdirectory(stimBuffApis)
add_subdirectory(wilzorApis)
add_subdirectory(devices)

# Main executable
add_executable(salmanoff main.cpp)
target_link_libraries(salmanoff
	Boost::system Boost::log
    smocore
    ${DL_LIBRARY}
    attachmentSupport
)

# Verify Boost dynamic dependencies after build
add_custom_command(TARGET salmanoff POST_BUILD
	COMMAND ${CMAKE_COMMAND} -DVERIFY_FILE="$<TARGET_FILE:salmanoff>"
		-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/VerifyBoostDynamic.cmake
	COMMENT "Verifying Boost dynamic dependencies for salmanoff"
)

# Add all registered DAPSS targets as dependencies
add_all_daps_dependencies()

# Add tests if enabled
if(ENABLE_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

install(TARGETS salmanoff DESTINATION ${CMAKE_INSTALL_BINDIR})

# Install device configuration files (preprocessed .daps files)
install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/devices/
    DESTINATION ${CMAKE_INSTALL_DATADIR}/salmanoff/devices
	DIRECTORY_PERMISSIONS
		OWNER_READ OWNER_WRITE OWNER_EXECUTE
		GROUP_READ GROUP_EXECUTE
		WORLD_READ WORLD_EXECUTE
	FILE_PERMISSIONS
		OWNER_READ OWNER_WRITE
		GROUP_READ
		WORLD_READ
    FILES_MATCHING PATTERN "*.daps"
)

# Install documentation
install(FILES README.md DESTINATION ${CMAKE_INSTALL_DOCDIR})
install(FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})

# Install example configurations if they exist
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/examples")
    install(DIRECTORY examples/ DESTINATION ${CMAKE_INSTALL_DATADIR}/salmanoff/examples)
endif()

# Include CPack configuration
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/CPackConfig.cmake)
include(CPack)
