#
# LDASTools frameCPP - A library implementing the LIGO/Virgo frame specification
#
# Copyright (C) 2018 California Institute of Technology
#
# LDASTools frameCPP is free software; you may redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 (GPLv2) of the
# License or at your discretion, any later version.
#
# LDASTools frameCPP is distributed in the hope that it will be useful, but
# without any warranty or even the implied warranty of merchantability
# or fitness for a particular purpose. See the GNU General Public
# License (GPLv2) for more details.
#
# Neither the names of the California Institute of Technology (Caltech),
# The Massachusetts Institute of Technology (M.I.T), The Laser
# Interferometer Gravitational-Wave Observatory (LIGO), nor the names
# of its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# You should have received a copy of the licensing terms for this
# software included in the file LICENSE located in the top-level
# directory of this package. If you did not, you can view a copy at
# http://dcc.ligo.org/M1500244/LICENSE
#

function( framecpp_target_gwf )
  if ( NOT BUILD_TESTING OR ( CMAKE_CROSSCOMPILING AND NOT CMAKE_CROSSCOMPILING_EMULATOR ) )
    return( )
  endif( )

  set(options
    TEST_FRAME_MODE
    USE_FRAMEL
    )
  set(oneValueArgs
    BUILDER
    TARGET
    DESCRIPTION
    GPS_START
    DT
    )
  set(multiValueArgs
    VERSIONS
    EXTRA_ARGS
    )

  cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} )

  cx_scheme_sanitizer_set_environment(
    OUTPUT sanitizer_environment_variables
  )

  # Explicitly add ASAN_OPTIONS with suppression file for build-time execution
  # cx_scheme_sanitizer_set_environment doesn't always include suppressions for custom commands
  if(EXISTS ${CMAKE_SOURCE_DIR}/asan_suppressions.txt AND CX_SCHEME_SANITIZERS_ACTIVE MATCHES "address")
    # Disable ODR violation detection for build-time frame generation
    # ODR violations from boost::serialization::singleton 'lock' variable cannot be reliably
    # suppressed using suppressions file (symbol name is too generic and path matching unreliable)
    # These are harmless - same header instantiated in multiple DSOs (libframecppu.so, libframecppcmn.so)
    list(APPEND sanitizer_environment_variables "ASAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/asan_suppressions.txt:detect_odr_violation=0")
  endif()
  if ( NOT ARG_BUILDER )
    set( ARG_BUILDER framecpp_sample )
  endif( )
  if ( NOT ARG_GPS_START )
    set( ARG_GPS_START 600000000 )
  endif( )
  if ( NOT ARG_DT )
    set( ARG_DT 1 )
  endif( )
  foreach ( ver ${ARG_VERSIONS} )
    set( filename Z-${ARG_DESCRIPTION}${ver}-${ARG_GPS_START}-${ARG_DT}.gwf )
    list( APPEND
      filenames
      "${CMAKE_CURRENT_BINARY_DIR}/${filename}"
      )
    if ( ARG_TEST_FRAME_MODE )
      set( cmd $<TARGET_FILE:${ARG_BUILDER}> --test-frame-mode --version ${ver} --description ${ARG_DESCRIPTION}${ver} --start-time ${ARG_GPS_START} --duration ${ARG_DT} )
      if ( ARG_USE_FRAMEL )
        list(APPEND cmd --use-framel)
      endif( )
      if ( ARG_EXTRA_ARGS )
        list(APPEND cmd ${ARG_EXTRA_ARGS})
      endif( )
    elseif( ARG_BUILDER STREQUAL "framecpp_sample_c" )
      set( cmd $<TARGET_FILE:${ARG_BUILDER}> )
    else( )
      set( cmd $<TARGET_FILE:${ARG_BUILDER}>
        --version ${ver}
		    --description ${ARG_DESCRIPTION}${ver}
		    --start-time ${ARG_GPS_START}
		    --duration ${ARG_DT}
        )
      if ( ARG_USE_FRAMEL )
        list(APPEND cmd --use-framel)
      endif( )
    endif( )
    add_custom_command(
      OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${filename}
      DEPENDS ${ARG_BUILDER}
      COMMAND
      ${CMAKE_COMMAND} -E env ${sanitizer_environment_variables}
	    ${cmd}
      )
  endforeach( )
  add_custom_target(
    ${ARG_TARGET}
    ALL
    DEPENDS ${filenames}
    )
endfunction( )

set(LDAS_DIRECTORY_BUILD_LIB_FRAMECPP ${CMAKE_BINARY_DIR} )

set( INCLUDES
  ${CMAKE_CURRENT_SOURCE_DIR}
  ${CMAKE_BINARY_DIR}/include
  ${CMAKE_BINARY_DIR}
  ${LDASTOOLS_INCLUDE_DIRS}
  )

set( LDADD
  framecpp
  framecppcmn
  ${LDASTOOLSAL_LIBRARIES}
  # Boost::filesystem and Boost::system now PUBLIC in framecpp
  )

# Note: DebugLevelManager is now part of ldastoolsal
# frameCPP utilities use FrameCPPDebugManager wrapper (header-only, in OOInterface)
#if HAVE_LIBZ
#LDADD += -lz
#endif

#bin_PROGRAMS =
#noinst_PROGRAMS =
#TESTS =

#========================================================================
# Programs based on the latest supported frame spec
#========================================================================

#------------------------------------------------------------------------

set( framecpp_compare_SOURCES framecmp.cc )
set( framecpp_compare_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_compare_LDADD  PUBLIC framecpp )
cx_target_executable( framecpp_compare
  SOURCES ${framecpp_compare_SOURCES}
  DEFINES ${framecpp_compare_CPPFLAGS}
  LDADD
    ${framecpp_compare_LDADD}
    Boost::program_options
    Threads::Threads
    # Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_compare )

#------------------------------------------------------------------------

set( framecpp_dump_descriptions_SOURCES framecpp_dump_descriptions.cc )
set( framecpp_dump_descriptions_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_dump_descriptions_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_dump_descriptions
  SOURCES ${framecpp_dump_descriptions_SOURCES}
  DEFINES ${framecpp_dump_descriptions_CPPFLAGS}
  LDADD
    ${framecpp_dump_descriptions_LDADD}
    Boost::program_options
    Threads::Threads
    # Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_dump_descriptions )

#------------------------------------------------------------------------

set( framecpp_verify_descriptions_SOURCES framecpp_verify_descriptions.cc )
set( framecpp_verify_descriptions_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_verify_descriptions_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_verify_descriptions
  SOURCES ${framecpp_verify_descriptions_SOURCES}
  DEFINES ${framecpp_verify_descriptions_CPPFLAGS}
  LDADD
    ${framecpp_verify_descriptions_LDADD}
    Boost::program_options
    Threads::Threads
    # Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_verify_descriptions )

#------------------------------------------------------------------------
# framecpp_dump_definition - Dumps frameCPP structure definitions
#------------------------------------------------------------------------

set( framecpp_dump_definition_SOURCES framecpp_dump_definition.cc )
set( framecpp_dump_definition_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_dump_definition
  SOURCES ${framecpp_dump_definition_SOURCES}
  LDADD
    ${framecpp_dump_definition_LDADD}
    Boost::program_options
    Threads::Threads
    # Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )

#------------------------------------------------------------------------
# framel_dump_definition - Dumps FrameL structure definitions from frame files
# Only built if FrameL library is available
#------------------------------------------------------------------------

if(FRAMEL_FOUND)
  set( framel_dump_definition_SOURCES framel_dump_definition.cc )
  cx_target_executable( framel_dump_definition
    SOURCES ${framel_dump_definition_SOURCES}
    LDADD
      ${FRAMEL_LIBRARIES}
      Boost::program_options
      Boost::system
      Threads::Threads
    INCLUDE_DIRECTORIES BEFORE PRIVATE
      ${FRAMEL_INCLUDE_DIRS}
      ${INCLUDES}
      ${BOOST_CPPFLAGS}
    )

  # Install Python comparison script
  install(PROGRAMS compare_definitions.py
          DESTINATION ${CMAKE_INSTALL_BINDIR})

  message(STATUS "FrameL found: Building framel_dump_definition utility and installing compare_definitions.py")
else()
  message(STATUS "FrameL not found: Skipping framel_dump_definition utility")
endif()

#------------------------------------------------------------------------
# framecpp_dump_pdf_definition - Dumps PDF-derived structure definitions
# Uses hard-coded definitions extracted from PDF spec documents
#------------------------------------------------------------------------

set( framecpp_dump_pdf_definition_SOURCES
  framecpp_dump_pdf_definition.cc
  PDFDefinitions.cc
)
cx_target_executable( framecpp_dump_pdf_definition
  SOURCES ${framecpp_dump_pdf_definition_SOURCES}
  LDADD
    Boost::system
    Threads::Threads
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES}
    ${BOOST_CPPFLAGS}
  )

#------------------------------------------------------------------------

#bin_PROGRAMS += framecpp_sample
set( framecpp_sample_SOURCES FrSample.cc )

# Add FrameL sources if available
if(FRAMEL_FOUND)
  list(APPEND framecpp_sample_SOURCES FrSampleFrameL.cc)
endif()

set( framecpp_sample_CPPFLAGS
  # -UFR_SAMPLE_VERSION
  )

list( APPEND noinst_HEADERS
  FrSample.hh
  FrSample3.tcc
  FrSample4.tcc
  FrSample6.tcc
  FrSample7.tcc
  FrSample8.tcc
  FrSample9.tcc
  )

# Add FrameL header if available
if(FRAMEL_FOUND)
  list(APPEND noinst_HEADERS FrSampleFrameL.hh)
endif()
cx_target_executable( framecpp_sample
  SOURCES ${framecpp_sample_SOURCES}
  DEFINES ${framecpp_sample_CPPFLAGS}
  LDADD ${LDADD}
     Boost::program_options
     Boost::log
     Boost::log_setup
     Threads::Threads
     $<$<BOOL:${FRAMEL_FOUND}>:${FRAMEL_LIBRARIES}>
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${Boost_INCLUDE_DIRS}
    $<$<BOOL:${FRAMEL_FOUND}>:${FRAMEL_INCLUDE_DIRS}>
  )

# Add FrameL compile definition if found
if(FRAMEL_FOUND)
  target_compile_definitions(framecpp_sample PRIVATE FRAMEL_FOUND)
endif()

#------------------------------------------------------------------------

set( framecpp_info_SOURCES framecpp_info.cc )
set( framecpp_info_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_info_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_info
  SOURCES ${framecpp_info_SOURCES}
  DEFINES ${framecpp_info_CPPFLAGS}
  LDADD
    ${framecpp_info_LDADD}
    Boost::program_options
    framecppcmn
    Threads::Threads
    # Boost::filesystem and Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_info )

#------------------------------------------------------------------------

set( framecpp_find_SOURCES framecpp_find.cc )
set( framecpp_find_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_find_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_find
  SOURCES ${framecpp_find_SOURCES}
  DEFINES ${framecpp_find_CPPFLAGS}
  LDADD
    ${framecpp_find_LDADD}
    Boost::program_options
    framecppcmn
    Threads::Threads
    # Boost::filesystem and Boost::system now PUBLIC in framecpp
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_find )

#------------------------------------------------------------------------

# Install Python script for finding .gwf files (legacy - replaced by C++ implementation above)
install(PROGRAMS framecpp_find.py
        DESTINATION ${CMAKE_INSTALL_BINDIR}
        RENAME framecpp_find_py)

#------------------------------------------------------------------------

set( framecpp_inspector_SOURCES framecpp_inspector.cc )
set( framecpp_inspector_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_inspector_LDADD PUBLIC framecpp )
cx_target_executable( framecpp_inspector
  SOURCES ${framecpp_inspector_SOURCES}
  DEFINES ${framecpp_inspector_CPPFLAGS}
  LDADD
    ${framecpp_inspector_LDADD}
    Boost::program_options
    Threads::Threads
    # Boost::filesystem, Boost::system, Boost::serialization now PUBLIC
  INCLUDE_DIRECTORIES BEFORE PRIVATE
    ${INCLUDES} ${BOOST_CPPFLAGS}
  )
cl_target_test_help( framecpp_inspector )

#------------------------------------------------------------------------
# framecpp_dump_objects - Low-level binary frame object dumper
#------------------------------------------------------------------------

set( framecpp_dump_objects_SOURCES FrDumpObjects.cc DynamicTOCPrinter.cc LegacyTOCPrinter.cc )
list( APPEND noinst_HEADERS DynamicTOCPrinter.hh LegacyTOCPrinter.hh StandardOptions.hh )

cx_target_executable( framecpp_dump_objects
  SOURCES ${framecpp_dump_objects_SOURCES}
  LDADD ${LDADD} Threads::Threads
  INCLUDE_DIRECTORIES BEFORE PRIVATE ${INCLUDES} ${BOOST_CPPFLAGS}
)

#------------------------------------------------------------------------
# framecpp_dump_toc - Formatted TOC dumper
#------------------------------------------------------------------------

set( framecpp_dump_toc_SOURCES dump_toc.cc LegacyTOCPrinter.cc ModernTOCPrinter.cc )
list( APPEND noinst_HEADERS ModernTOCPrinter.hh )

cx_target_executable( framecpp_dump_toc
  SOURCES ${framecpp_dump_toc_SOURCES}
  LDADD ${LDADD} Boost::program_options Threads::Threads
  INCLUDE_DIRECTORIES BEFORE PRIVATE ${INCLUDES} ${BOOST_CPPFLAGS}
)
cl_target_test_help( framecpp_dump_toc )

#------------------------------------------------------------------------

set( framecpp_sample_c_SOURCES framecpp_sample_c.c )
set( framecpp_sample_c_CFLAGS ${CFLAGS_NON_FATAL_WARNINGS} )
set( framecpp_sample_c_LDADD  framecppc
  # Boost::system now PUBLIC in framecpp (via framecppc)
 )
cx_target_executable( framecpp_sample_c
  NOINST
  SOURCES ${framecpp_sample_c_SOURCES}
  DEFINES ${framecpp_sample_c_CPPFLAGS}
  LDADD ${framecpp_sample_c_LDADD}
  INCLUDE_DIRECTORIES BEFORE PRIVATE
  ${INCLUDES} ${BOOST_CPPFLAGS}
  )

##=========================================================================
## Test frames
##=========================================================================
framecpp_target_gwf(
  TARGET test_frame_set_1
  TEST_FRAME_MODE
  DESCRIPTION R_std_test_frame_ver
  VERSIONS 3 4 5 6 7 8 9
  EXTRA_ARGS --channel-adc-type int_2u --channel-adc-type int_2s --channel-adc-type int_4u --channel-adc-type int_4s --channel-adc-type int_8u --channel-adc-type int_8s --channel-adc-type real_4 --channel-adc-type real_8 --channel-adc-type complex_8 --channel-adc-type complex_16 --channel-adc-type char --channel-adc-type char_u
)
# Complementary FrameL version (only v3-8)
if(FRAMEL_FOUND)
  framecpp_target_gwf(
    TARGET test_framel_set_1
    BUILDER framel_sample
    TEST_FRAME_MODE
    DESCRIPTION R_std_test_framel_ver
    VERSIONS 3 4 5 6 7 8
  )
endif()

framecpp_target_gwf(
  TARGET test_frame_set_2
  DESCRIPTION std_test_frame_ver
  GPS_START 700000000
  # VERSIONS 6 7 8 9
  VERSIONS 9
)
# Complementary FrameL version (only v3-8)
if(FRAMEL_FOUND)
  framecpp_target_gwf(
    TARGET test_framel_set_2
    BUILDER framel_sample
    DESCRIPTION std_test_framel_ver
    GPS_START 700000000
    VERSIONS 3 4 5 6 7 8
  )
endif()

framecpp_target_gwf(
  TARGET test_frame_set_3
  BUILDER framecpp_sample_c
  DESCRIPTION Sample_C_ver
  VERSIONS 8 9
)

#------------------------------------------------------------------------

cx_scheme_sanitizer_set_environment( )

#------------------------------------------------------------------------
if (FRAMEL_FOUND)
  #----------------------------------------------------------------------
  # Verify that the frames generated by framecpp can be read and
  #  and processed by FrameL
  #----------------------------------------------------------------------
  # Split FRAMEL_VERSION into its components
  string(REPLACE "." ";" FRAMEL_VERSION_LIST ${FRAMEL_VERSION})
  list(GET FRAMEL_VERSION_LIST 0 FRAMEL_MAJOR_VERSION)

  # Loop from the major version down to 3
  while(${FRAMEL_MAJOR_VERSION} GREATER_EQUAL 3)
    # Skip v5: FrameL never implemented this version
    if (${FRAMEL_MAJOR_VERSION} EQUAL 5)
      math(EXPR FRAMEL_MAJOR_VERSION "${FRAMEL_MAJOR_VERSION} - 1")
      continue( )
    endif( )

    # Skip v3 and v4: Known incompatibility issues between frameCPP and FrameL
    #
    # KNOWN ISSUES:
    # - v4: File checksum algorithm mismatch
    #   * frameCPP writes checksum: 0x8f8ff353
    #   * FrCheck computes: 0x6d40dd0a
    #   * File structure is valid (framecpp_verify passes)
    #   * Suggests different XOR checksum calculation algorithms
    #
    # - v3: FrSerData serialization format incompatibility
    #   * FrCheck reports: "Record length error: nBytes=27537 nBytesR=5873"
    #   * File structure is valid (framecpp_verify passes)
    #   * Suggests field ordering or size differences in FrSerData
    #
    # IMPACT:
    # - frameCPP can read/write/validate v3 and v4 files correctly
    # - Cross-library validation with FrameL fails for these legacy versions
    # - v6-v8 cross-validation works correctly (production versions)
    #
    # TODO: Revisit if v3/v4 cross-compatibility becomes required
    #       Investigation needed into checksum algorithms and FrSerData format
    if (${FRAMEL_MAJOR_VERSION} EQUAL 3 OR ${FRAMEL_MAJOR_VERSION} EQUAL 4)
      math(EXPR FRAMEL_MAJOR_VERSION "${FRAMEL_MAJOR_VERSION} - 1")
      continue( )
    endif( )

    # FrCheck -i Z-R_std_test_frame_ver${FRAMEL_MAJOR_VERSION}-600000000-1.gwf
    # Use wrapper script to show context from framecpp_dump_objects
    cx_target_test(
      verify_${FRAMEL_MAJOR_VERSION}_via_fr_check
      COMMAND ${CMAKE_SOURCE_DIR}/test/frcheck_with_context.py
        $<TARGET_FILE:framecpp_dump_objects>
        ${FR_CHECK}
        Z-R_std_test_frame_ver${FRAMEL_MAJOR_VERSION}-600000000-1.gwf
      FIXTURES_REQUIRED test_frame_set_1
      )
    math(EXPR FRAMEL_MAJOR_VERSION "${FRAMEL_MAJOR_VERSION} - 1")
  endwhile()

endif( )
