cmake_minimum_required(VERSION 3.16)
# NOTE: keep the default languages (C and CXX). The vendored zstd/lz4 fallback
# compiles .c sources, so restricting to CXX only would break standalone builds.
project(cloudini_lib VERSION 1.4.0)

# When another project embeds this one (add_subdirectory / FetchContent), build
# only the library: no forced build type, no ament packaging, no tests, tools,
# benchmarks, PCL or install rules unless the parent asks for them.
# (PROJECT_IS_TOP_LEVEL needs CMake 3.21.)
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
  set(CLOUDINI_IS_TOP_LEVEL ON)
else()
  set(CLOUDINI_IS_TOP_LEVEL OFF)
endif()

# Make installed executables relocatable: find the co-installed shared library
# through an $ORIGIN-relative RPATH (bin/ -> ../lib). conda/conda-forge tooling
# manages RPATHs on its own, so this mainly benefits a plain `cmake --install`.
if(APPLE)
  set(CMAKE_INSTALL_RPATH "@loader_path/../lib")
else()
  set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
endif()

# If not specified, set the default build type to Release
if (CLOUDINI_IS_TOP_LEVEL AND NOT CMAKE_BUILD_TYPE)
  message(STATUS "No build type selected, default to Release")
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo" "MinSizeRel")
endif()
message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}")

option(CLOUDINI_BUILD_TOOLS "Build Tools" ${CLOUDINI_IS_TOP_LEVEL})
option(CLOUDINI_FORCE_VENDORED_DEPS "Force vendored dependencies" ON)
option(CLOUDINI_WITH_PCL "Build the PCL conversions when PCL is found" ${CLOUDINI_IS_TOP_LEVEL})
option(CLOUDINI_INSTALL "Generate install rules" ${CLOUDINI_IS_TOP_LEVEL})


# create compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

##################################################################################
## Third party dependeciens

# add cmake folder to path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

if(CLOUDINI_IS_TOP_LEVEL)
  find_package(ament_cmake QUIET)
else()
  # A ROS parent has found ament_cmake already; embedded, we are not a package.
  set(ament_cmake_FOUND FALSE)
endif()
if(ament_cmake_FOUND)
  message(STATUS "Found ament_cmake")
  set(CLOUDINI_FORCE_VENDORED_DEPS OFF)
endif()

include(FetchContent)
include(cmake/CPM.cmake)

include(cmake/find_or_download_zstd.cmake)
find_or_download_zstd(${CLOUDINI_FORCE_VENDORED_DEPS})

include(cmake/find_or_download_lz4.cmake)
find_or_download_lz4(${CLOUDINI_FORCE_VENDORED_DEPS})

##################################################################################

# ----  Trick to pass an absolute path to C++ code  ----
get_filename_component(DATA_PATH "${CMAKE_CURRENT_LIST_DIR}/samples" ABSOLUTE)
configure_file (data_path.hpp.in "${CMAKE_CURRENT_BINARY_DIR}/include/data_path.hpp" @ONLY)
add_library(data_path INTERFACE IMPORTED)
target_include_directories(data_path INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/include)

##################################################################################

if(CLOUDINI_WITH_PCL)
  find_package(PCL COMPONENTS common io QUIET)
endif()

if(PCL_FOUND)
  message(STATUS "PCL found in system")
  add_definitions(${PCL_DEFINITIONS})
  set(PCL_SRC src/pcl_conversion.cpp)
else()
  message(STATUS "PCL NOT found")
endif()

##################################################################################

# add address sanitizer in debug mode
# if(CMAKE_BUILD_TYPE MATCHES "Debug")
#   if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
#     message(STATUS "Adding address sanitizer")
#     set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
#     set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
#   endif()
# endif()

option(CLOUDINI_BUILD_BENCHMARKS "Build benchmarks" ${CLOUDINI_IS_TOP_LEVEL})

# Use SHARED library when building with ament, STATIC otherwise
SET(CLOUDINI_SRC
  src/chunk_writer.cpp
  src/cloudini.cpp
  src/codec_common.cpp
  src/field_encoder.cpp
  src/field_decoder.cpp
  src/ros_msg_utils.cpp
  src/v4_codec.cpp
  src/v5_codec.cpp
  src/v6_codec.cpp
)

if(ament_cmake_FOUND)
  # ROS/ament always builds shared so the plugin pluginlib can dlopen it.
  add_library(cloudini_lib SHARED
    ${CLOUDINI_SRC}
    ${PCL_SRC}
  )
else()
  # Standalone builds honor BUILD_SHARED_LIBS (static by default).
  add_library(cloudini_lib
    ${CLOUDINI_SRC}
    ${PCL_SRC}
  )
endif()

# Same name as the installed (non-ament) target, so consumers link
# cloudini::cloudini_lib whether Cloudini is installed or embedded.
add_library(cloudini::cloudini_lib ALIAS cloudini_lib)

target_compile_features(cloudini_lib PUBLIC cxx_std_20)

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  target_compile_options(cloudini_lib PRIVATE -Wall -Wextra)
endif()

target_include_directories(cloudini_lib
  PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# When cloudini_lib is a shared library (ROS/ament, or a standalone build with
# BUILD_SHARED_LIBS=ON such as the conda package), prefer the *shared* variants
# of zstd/lz4 when available: a non-PIC system libzstd.a / liblz4.a cannot be
# embedded into a .so. Vendored builds only expose the *_static targets, which
# are used as the fallback.
get_target_property(_cloudini_lib_type cloudini_lib TYPE)
if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET zstd::libzstd_shared)
  set(CLOUDINI_ZSTD_TARGET zstd::libzstd_shared)
else()
  set(CLOUDINI_ZSTD_TARGET zstd::libzstd_static)
endif()
if(_cloudini_lib_type STREQUAL "SHARED_LIBRARY" AND TARGET LZ4::lz4_shared)
  set(CLOUDINI_LZ4_TARGET LZ4::lz4_shared)
else()
  set(CLOUDINI_LZ4_TARGET LZ4::lz4_static)
endif()

# PointcloudEncoder uses std::thread. PUBLIC: a static cloudini_lib leaves the
# pthread references for whoever links the final binary.
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

target_link_libraries(cloudini_lib
  PRIVATE
    $<BUILD_INTERFACE:${CLOUDINI_LZ4_TARGET}>
    $<BUILD_INTERFACE:${CLOUDINI_ZSTD_TARGET}>
  PUBLIC
    Threads::Threads
    ${PCL_COMMON_LIBRARIES}
    ${PCL_IO_LIBRARIES}
)

if(NOT EMSCRIPTEN)
  if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|i686")
    if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
      target_compile_options(cloudini_lib PRIVATE -msse4.1)
    endif()
  endif()

  find_package(benchmark QUIET)
  if(CLOUDINI_BUILD_BENCHMARKS AND benchmark_FOUND AND PCL_FOUND)
    add_subdirectory(benchmarks)
  endif()

  if(CLOUDINI_IS_TOP_LEVEL)  # include(CTest) would define BUILD_TESTING in the parent's cache
    include(CTest)
    message( STATUS "BUILD_TESTING:   ${BUILD_TESTING} " )
    if (BUILD_TESTING)
      add_subdirectory(test)
    endif()
  endif()

else()

  set(CLD_EXPORTED_FUNCTIONS
      "_malloc"
      "_free"
      "_cldn_GetHeaderAsYAML"
      "_cldn_GetHeaderAsYAMLFromDDS"
      "_cldn_ComputeCompressedSize"
      "_cldn_GetDecompressedSize"
      "_cldn_DecodeCompressedMessage"
      "_cldn_DecodeCompressedData"
      "_cldn_ConvertCompressedMsgToPointCloud2Msg"
      "_cldn_EncodePointcloudData"
      "_cldn_EncodePointcloudMessage"
    )
  set(QUOTED_FUNCTIONS "")
  FOREACH(Item IN LISTS CLD_EXPORTED_FUNCTIONS)
    LIST(APPEND QUOTED_FUNCTIONS "'${Item}'")
  ENDFOREACH()

  STRING(JOIN "," CLD_EXPORTED_FUNCTIONS_STR ${QUOTED_FUNCTIONS})

  # WASM module target used for Python bindings and Wasmtime in general
  add_executable(cloudini_wasm src/wasm_functions.cpp)
  target_compile_features(cloudini_wasm PRIVATE cxx_std_20)
  target_include_directories(cloudini_wasm PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
  target_link_libraries(cloudini_wasm PRIVATE cloudini_lib)

  set_target_properties(cloudini_wasm PROPERTIES
    LINK_FLAGS "-s DISABLE_EXCEPTION_CATCHING -s ASSERTIONS -s EXPORTED_FUNCTIONS=[${CLD_EXPORTED_FUNCTIONS_STR}] -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','HEAPU8','HEAP8','UTF8ToString'] -s MODULARIZE=1 -s EXPORT_NAME='CloudiniModule' -s INITIAL_MEMORY=67108864 -s MAXIMUM_MEMORY=134217728 -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_STACK=1048576"
  )

  # WASM module target used for Foxglove Studio (single file)
  add_executable(cloudini_wasm_single src/wasm_functions.cpp)
  target_compile_features(cloudini_wasm_single PRIVATE cxx_std_20)
  target_include_directories(cloudini_wasm_single PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
  target_link_libraries(cloudini_wasm_single PRIVATE cloudini_lib)
  set_target_properties(cloudini_wasm_single PROPERTIES
    LINK_FLAGS "-s SINGLE_FILE=1 -s NO_DISABLE_EXCEPTION_CATCHING -s ASSERTIONS -s EXPORTED_FUNCTIONS=[${CLD_EXPORTED_FUNCTIONS_STR}] -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap','HEAPU8','HEAP8','UTF8ToString'] -s MODULARIZE=1 -s EXPORT_NAME='CloudiniModule' -s INITIAL_MEMORY=67108864 -s MAXIMUM_MEMORY=134217728 -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_STACK=1048576"
  )
endif(NOT EMSCRIPTEN)

if(CLOUDINI_BUILD_TOOLS AND NOT ament_cmake_FOUND)
  if(EMSCRIPTEN)
    message(STATUS "Disabling tools build for Emscripten")
  else()
    include(cmake/find_or_download_mcap.cmake)
    find_or_download_mcap()
    add_subdirectory(tools)
  endif()
endif()

if(CLOUDINI_INSTALL)
  INSTALL(TARGETS cloudini_lib
          EXPORT cloudini_libTargets
          ARCHIVE DESTINATION lib
          LIBRARY DESTINATION lib
          RUNTIME DESTINATION bin
          INCLUDES DESTINATION include
    )

  INSTALL( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
      DESTINATION include
      FILES_MATCHING PATTERN "*.h*")

  if(ament_cmake_FOUND)
    ament_export_targets(cloudini_libTargets HAS_LIBRARY_TARGET)
    ament_export_dependencies(PCL Threads)
    ament_package()
  else()
    # Standalone (non-ament) install: generate a CMake package config so downstream
    # projects can do find_package(cloudini_lib) and link cloudini::cloudini_lib.
    # Static archives retain compressor references: ship vendored archives or
    # rediscover system libraries in the installed package config.
    include(CMakePackageConfigHelpers)

    set(CLOUDINI_SYSTEM_LIBRARIES "")
    if(_cloudini_lib_type STREQUAL "STATIC_LIBRARY")
      foreach(_dependency IN ITEMS lz4 zstd)
        set(_vendored_target "${_dependency}_static")
        if(_dependency STREQUAL "zstd")
          set(_vendored_target libzstd_static)
        endif()
        if(TARGET ${_vendored_target})
          set_target_properties(${_vendored_target} PROPERTIES OUTPUT_NAME cloudini_${_dependency})
          install(TARGETS ${_vendored_target} EXPORT cloudini_libTargets ARCHIVE DESTINATION lib)
          target_link_libraries(cloudini_lib INTERFACE
            $<INSTALL_INTERFACE:$<LINK_ONLY:cloudini::${_vendored_target}>>)
        else()
          list(APPEND CLOUDINI_SYSTEM_LIBRARIES ${_dependency})
        endif()
      endforeach()
    endif()

    install(EXPORT cloudini_libTargets
      FILE cloudini_libTargets.cmake
      NAMESPACE cloudini::
      DESTINATION lib/cmake/cloudini_lib)

    set(CLOUDINI_HAS_PCL 0)
    if(PCL_FOUND)
      set(CLOUDINI_HAS_PCL 1)
    endif()

    configure_package_config_file(
      ${CMAKE_CURRENT_SOURCE_DIR}/cmake/cloudini_libConfig.cmake.in
      ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake
      INSTALL_DESTINATION lib/cmake/cloudini_lib)

    write_basic_package_version_file(
      ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake
      VERSION ${PROJECT_VERSION}
      COMPATIBILITY SameMajorVersion)

    install(FILES
      ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfig.cmake
      ${CMAKE_CURRENT_BINARY_DIR}/cloudini_libConfigVersion.cmake
      DESTINATION lib/cmake/cloudini_lib)
  endif()
endif()
