cmake_minimum_required(VERSION 3.10)

project(neuralblender 
  VERSION 0.1.0
  DESCRIPTION "Amp modeling plugin based on RTNeural and NAM"
  LANGUAGES C CXX)

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

option(STANDALONE      "Build standalone JACK client" ON)
option(LV2             "Build LV2 plugin" ON)
option(GUI             "Enable GUI" ON)
option(DEBUG           "Debug build" OFF)
option(TRACE           "Debug (mostly) UI code" OFF)
option(TEST_FEATURES   "Test features that aren't 100% ready yet" OFF)
option(NOASAN          "Disable asan on debug build" ON)
option(GZIP_SUPPORT    "Enable gzip-compressed model/IR loading" ON)
option(USE_NATIVE_ARCH "Enable CPU-specific optimizations (-march=native)" ON)

find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
find_program(XXD_EXECUTABLE xxd REQUIRED)

# RTNeural vendors Eigen's Eigen/ and unsupported/ trees in separate module
# directories. Recreate Eigen's normal include layout in the build tree so
# both <Eigen/...> and <unsupported/Eigen/...> resolve correctly. These must
# be copied rather than symlinked because unsupported headers use relative
# includes and compilers resolve those from the symlink target directory.
set(BUNDLED_EIGEN_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/eigen3")
file(REMOVE
  "${BUNDLED_EIGEN_INCLUDE_DIR}/Eigen"
  "${BUNDLED_EIGEN_INCLUDE_DIR}/unsupported"
)
file(COPY
  "${PROJECT_SOURCE_DIR}/external/RTNeural/modules/Eigen/Eigen"
  DESTINATION "${BUNDLED_EIGEN_INCLUDE_DIR}"
)
file(COPY
  "${PROJECT_SOURCE_DIR}/external/RTNeural/modules/unsupported"
  DESTINATION "${BUNDLED_EIGEN_INCLUDE_DIR}"
)

pkg_check_modules(FFTW3F REQUIRED IMPORTED_TARGET fftw3f)
pkg_check_modules(SNDFILE REQUIRED IMPORTED_TARGET sndfile)
pkg_check_modules(CURL QUIET IMPORTED_TARGET libcurl)
if (CURL_FOUND)
  message(STATUS "libcurl: found, online fetcher enabled")
else()
  message(STATUS "libcurl: not found, online fetcher disabled")
endif()
#pkg_check_modules(SAMPLERATE REQUIRED IMPORTED_TARGET samplerate)

find_library(FFTW3F_STATIC_LIBRARY
  NAMES libfftw3f.a
  HINTS ${FFTW3F_LIBRARY_DIRS}
)

if (NOT FFTW3F_STATIC_LIBRARY)
  message(FATAL_ERROR "Static FFTW3F library not found")
endif()

if (GZIP_SUPPORT)
  find_package(ZLIB)
  if (NOT ZLIB_FOUND)
    message(WARNING "zlib not found, disabling gzip support")
    set(GZIP_SUPPORT OFF)
  endif()
endif()

set(NEURALBLENDER_ONLINE_SOURCES)
if (CURL_FOUND)
  list(APPEND NEURALBLENDER_ONLINE_SOURCES src/online.cpp)
endif()

function(add_neuralblender_gui_library target_name)
  add_library(${target_name} STATIC
    src/ui.cpp
    src/nbtk.cpp
    src/native_x11.cpp
    src/timestamp.cpp
    src/meter.cpp
    src/tuner.cpp
    src/eqgraph.cpp
    src/plugin.cpp
    src/neuralblender_plugin_ui.cpp
    ${NEURALBLENDER_ONLINE_SOURCES}
  )

  add_dependencies(${target_name}
    generate_inline_data
    generate_timestamp
  )

  set_target_properties(${target_name} PROPERTIES
    POSITION_INDEPENDENT_CODE ON
  )

  target_include_directories(${target_name} PUBLIC
    ${PROJECT_SOURCE_DIR}/src
    ${PROJECT_SOURCE_DIR}/external
    ${CMAKE_CURRENT_BINARY_DIR}
  )

  target_link_libraries(${target_name} PUBLIC
    neuralblender_core
    neuralblender_config
    PkgConfig::X11
    PkgConfig::CAIRO
  )

  if (CURL_FOUND)
    target_link_libraries(${target_name} PUBLIC PkgConfig::CURL)
  endif()

  if (TARGET PkgConfig::XRANDR)
    target_link_libraries(${target_name} PUBLIC PkgConfig::XRANDR)
    target_compile_definitions(${target_name} PRIVATE NBTK_HAVE_XRANDR=1)
  endif()

  target_compile_definitions(${target_name} PUBLIC HAVE_GUI=1)

  if (ARGN)
    target_compile_definitions(${target_name} PRIVATE ${ARGN})
  endif()
endfunction()

if (TRACE)
  add_compile_definitions(CMDLINE_DEBUG)
endif()

if (TEST_FEATURES)
  add_compile_definitions(TEST_FEATURES)
endif()

if (NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# Timestamp header
set(TIMESTAMP_FILE ${CMAKE_CURRENT_BINARY_DIR}/timestamp.h)
set(BUILD_VERSION_FILE ${CMAKE_CURRENT_BINARY_DIR}/VERSION)
set_source_files_properties(${TIMESTAMP_FILE} PROPERTIES GENERATED TRUE)
set(INLINE_DATA_CPP ${CMAKE_CURRENT_BINARY_DIR}/data/data.cpp)
set(INLINE_DATA_H ${CMAKE_CURRENT_BINARY_DIR}/data/data.h)
set_source_files_properties(${INLINE_DATA_CPP} ${INLINE_DATA_H} PROPERTIES GENERATED TRUE)

add_custom_target(generate_timestamp ALL
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
          "${CMAKE_CURRENT_SOURCE_DIR}/VERSION"
          "${BUILD_VERSION_FILE}"
  COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/timestamp.sh"
          "${TIMESTAMP_FILE}" "${CMAKE_INSTALL_PREFIX}"
  WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
  COMMENT "Updating build timestamp"
)

add_custom_target(generate_inline_data
  COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/data/generate_inline_data.sh"
  BYPRODUCTS ${INLINE_DATA_CPP} ${INLINE_DATA_H}
  WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
  COMMENT "Checking inline data"
  VERBATIM
)

set_source_files_properties(
  ${INLINE_DATA_CPP}
  ${INLINE_DATA_H}
  PROPERTIES GENERATED TRUE
)

add_library(neuralblender_config STATIC
  src/state.cpp
)

set_target_properties(neuralblender_config PROPERTIES
  POSITION_INDEPENDENT_CODE ON
)

target_include_directories(neuralblender_config PUBLIC
  ${PROJECT_SOURCE_DIR}/src
  ${PROJECT_SOURCE_DIR}/external
)

target_compile_definitions(neuralblender_config PUBLIC
  PREFIX="${CMAKE_INSTALL_PREFIX}"
)

add_library(neuralblender_data OBJECT
  ${INLINE_DATA_CPP}
)

add_dependencies(neuralblender_data generate_inline_data)

set_target_properties(neuralblender_data PROPERTIES
  POSITION_INDEPENDENT_CODE ON
)

target_include_directories(neuralblender_data PUBLIC
  ${CMAKE_CURRENT_BINARY_DIR}
  ${CMAKE_CURRENT_BINARY_DIR}/data
)

set(NEURALBLENDER_CORE_SOURCES
  src/neuralblender.cpp
  
  external/NAM/get_dsp.cpp
  external/NAM/nam_file.cpp
  external/NAM/dsp.cpp
  external/NAM/container.cpp
  external/NAM/convnet.cpp
  external/NAM/lstm.cpp
  external/NAM/linear.cpp
  external/NAM/conv1d.cpp
  external/NAM/activations.cpp
  external/NAM/ring_buffer.cpp
  external/NAM/util.cpp
  external/NAM/wavenet/model.cpp
  external/NAM/wavenet/a2_fast.cpp
  external/NAM/wavenet/slimmable.cpp
)

if (GZIP_SUPPORT)
  list(APPEND NEURALBLENDER_CORE_SOURCES src/gzip.cpp)
endif()

add_library(neuralblender_core OBJECT
  ${NEURALBLENDER_CORE_SOURCES}
)

add_library(neuralblender_meter_data OBJECT
  src/meter.cpp
  src/tuner.cpp
)

set_target_properties(neuralblender_core PROPERTIES
  POSITION_INDEPENDENT_CODE ON
)

set_target_properties(neuralblender_meter_data PROPERTIES
  POSITION_INDEPENDENT_CODE ON
)

add_dependencies(neuralblender_core generate_inline_data)

target_include_directories(neuralblender_core PUBLIC
  ${CMAKE_CURRENT_BINARY_DIR}
  ${CMAKE_CURRENT_BINARY_DIR}/data
  ${PROJECT_SOURCE_DIR}/external
  ${PROJECT_SOURCE_DIR}/external/RTNeural
  ${PROJECT_SOURCE_DIR}/external/NAM
  ${PROJECT_SOURCE_DIR}/external/nlohmann
  ${PROJECT_SOURCE_DIR}
)

target_include_directories(neuralblender_core SYSTEM PUBLIC
  ${BUNDLED_EIGEN_INCLUDE_DIR}
)

target_include_directories(neuralblender_meter_data PUBLIC
  ${CMAKE_CURRENT_BINARY_DIR}
  ${PROJECT_SOURCE_DIR}
  ${PROJECT_SOURCE_DIR}/external
  ${PROJECT_SOURCE_DIR}/src
)

target_compile_definitions(neuralblender_core PUBLIC
  NEURALBLENDER_VERSION="${PROJECT_VERSION}"
  NAM_SAMPLE_FLOAT
  DSP_SAMPLE_FLOAT
  HAVE_FFTW
  HAVE_SNDFILE
#  HAVE_SAMPLERATE
)

target_compile_definitions(neuralblender_meter_data PRIVATE
  METER_DATA_ONLY
)

target_link_libraries(neuralblender_core PUBLIC
#  PkgConfig::FFTW3F
  ${FFTW3F_STATIC_LIBRARY}
  PkgConfig::SNDFILE
#  PkgConfig::SAMPLERATE
  neuralblender_config
)

if (USE_NATIVE_ARCH)
  if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    message(STATUS "Native CPU optimizations enabled (-march=native)")
    target_compile_options(neuralblender_core PUBLIC -march=native)
  else()
    message(WARNING "USE_NATIVE_ARCH is enabled, but no native architecture flags are defined for ${CMAKE_CXX_COMPILER_ID}")
  endif()
endif()

if (GZIP_SUPPORT)
  target_compile_definitions(neuralblender_core PUBLIC HAVE_GZIP)
  target_link_libraries(neuralblender_core PUBLIC ZLIB::ZLIB)
endif()

if (DEBUG)
  message(STATUS "Debug enabled")
  target_compile_definitions(neuralblender_core PUBLIC DEBUG)
  set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)

  if (NOT NOASAN)
    target_compile_options(neuralblender_core PUBLIC
      -fsanitize=address
      -fno-omit-frame-pointer
      -g
    )
    target_link_options(neuralblender_core PUBLIC
      -fsanitize=address
    )
  endif()
else()
  target_compile_definitions(neuralblender_core PUBLIC NDEBUG)
endif()

if (STANDALONE)
  pkg_check_modules(JACK IMPORTED_TARGET jack)

  if (JACK_FOUND)
    message(STATUS "JACK: found")

    add_executable(neuralblender
      src/standalone.cpp
      src/timestamp.cpp
      $<TARGET_OBJECTS:neuralblender_data>
      $<TARGET_OBJECTS:neuralblender_meter_data>
    )
    
    target_link_libraries(neuralblender PRIVATE
      neuralblender_core
      Threads::Threads
      PkgConfig::JACK
    )

    target_compile_definitions(neuralblender PRIVATE STANDALONE)
    add_dependencies(neuralblender generate_timestamp)

    install(TARGETS neuralblender RUNTIME DESTINATION bin)
  else()
    message(WARNING "JACK development files not found, standalone target disabled")
  endif()
endif()

set(UNINSTALL_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake")
file(WRITE "${UNINSTALL_SCRIPT}" [=[
if (NOT CMAKE_INSTALL_MANIFEST)
  message(FATAL_ERROR "CMAKE_INSTALL_MANIFEST is not set")
endif()

if (NOT EXISTS "${CMAKE_INSTALL_MANIFEST}")
  message(FATAL_ERROR "Install manifest not found: ${CMAKE_INSTALL_MANIFEST}")
endif()

file(STRINGS "${CMAKE_INSTALL_MANIFEST}" files)
foreach (file ${files})
  if (EXISTS "${file}" OR IS_SYMLINK "${file}")
    message(STATUS "Removing ${file}")
    file(REMOVE "${file}")
  else()
    message(STATUS "Already removed ${file}")
  endif()
endforeach()
]=])

if (NOT TARGET uninstall)
  add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND}
      -D CMAKE_INSTALL_MANIFEST=${CMAKE_CURRENT_BINARY_DIR}/install_manifest.txt
      -P ${UNINSTALL_SCRIPT}
    COMMENT "Uninstalling files from install_manifest.txt"
  )
endif()

if (GUI)
  pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
  pkg_check_modules(XRANDR QUIET IMPORTED_TARGET xrandr)
  pkg_check_modules(CAIRO REQUIRED IMPORTED_TARGET cairo)
  message(STATUS "X11: found")
  if (XRANDR_FOUND)
    message(STATUS "XRandR: found")
  else()
    message(STATUS "XRandR: not found, using root-window bounds")
  endif()
  message(STATUS "Cairo: found")

  add_neuralblender_gui_library(neuralblender_gui)
endif()

if (TARGET neuralblender AND GUI)
  target_link_libraries(neuralblender PRIVATE neuralblender_gui)
  target_compile_definitions(neuralblender PRIVATE GUI HAVE_GUI=1)
endif()
  
if (LV2)
  pkg_check_modules(LV2_DEPS REQUIRED IMPORTED_TARGET lv2)
  message(STATUS "LV2: found")

  add_custom_target(generate_ttl ALL
    COMMAND bash "${CMAKE_CURRENT_SOURCE_DIR}/lv2/generate_ttl.sh" "$<BOOL:${GUI}>"
    WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
    COMMENT "Generating TTL files"
  )

  add_library(neuralblender_lv2 MODULE
    src/lv2.cpp
    src/plugin.cpp
    #src/configfile.cpp
    src/timestamp.cpp
    $<TARGET_OBJECTS:neuralblender_data>
    $<TARGET_OBJECTS:neuralblender_meter_data>
  )

  target_compile_options(neuralblender_lv2 PRIVATE
    -fpermissive
  )
  
  target_link_libraries(neuralblender_lv2 PRIVATE
    neuralblender_core
    Threads::Threads
    PkgConfig::LV2_DEPS
  )
  target_compile_definitions(neuralblender_lv2 PRIVATE LV2)
  add_dependencies(neuralblender_lv2
    generate_timestamp
    generate_ttl
  )

  target_link_options(neuralblender_lv2 PRIVATE
    "LINKER:--version-script=${PROJECT_SOURCE_DIR}/lv2/neuralblender.map"
  )

  set_target_properties(neuralblender_lv2 PROPERTIES
    PREFIX ""
    OUTPUT_NAME "neuralblender"
    C_VISIBILITY_PRESET hidden
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
  )

  # Keep plugin-local symbols hidden from the host process.
  install(TARGETS neuralblender_lv2
    LIBRARY DESTINATION lib/lv2/neuralblender.lv2
  )

  install(FILES
     ${CMAKE_CURRENT_BINARY_DIR}/lv2/manifest.ttl
     ${CMAKE_CURRENT_BINARY_DIR}/lv2/neuralblender.ttl
    DESTINATION lib/lv2/neuralblender.lv2
  )

  if(GUI)    
    add_neuralblender_gui_library(neuralblender_gui_lv2 LV2)

    add_library(neuralblender_ui MODULE
      src/lv2_ui.cpp
      $<TARGET_OBJECTS:neuralblender_data>
      $<TARGET_OBJECTS:neuralblender_meter_data>
    )

    set_target_properties(neuralblender_ui PROPERTIES
      PREFIX ""
      OUTPUT_NAME "neuralblender_ui"
      C_VISIBILITY_PRESET hidden
      CXX_VISIBILITY_PRESET hidden
      VISIBILITY_INLINES_HIDDEN ON
    )

    set_target_properties(neuralblender_gui_lv2 PROPERTIES
      C_VISIBILITY_PRESET hidden
      CXX_VISIBILITY_PRESET hidden
      VISIBILITY_INLINES_HIDDEN ON
    )
    
    target_link_libraries(neuralblender_ui PRIVATE
      neuralblender_gui_lv2
      neuralblender_config
      PkgConfig::LV2_DEPS
    )
    target_compile_definitions(neuralblender_ui PRIVATE LV2 LV2_UI)

    target_link_options(neuralblender_ui PRIVATE
      "LINKER:--version-script=${PROJECT_SOURCE_DIR}/lv2/neuralblender_ui.map"
    )

    install(TARGETS neuralblender_ui
      LIBRARY DESTINATION lib/lv2/neuralblender.lv2
    )
  endif()
endif()

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/sample_nam/"
  DESTINATION share/neuralblender/nam
)

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/sample_ir/"
  DESTINATION share/neuralblender/ir
)

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/sample_presets/"
  DESTINATION share/neuralblender/presets
)

install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/data/logo-shadow-256x256.png"
  DESTINATION share/icons/hicolor/256x256/apps
  RENAME neuralblender.png
)

install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/data/neuralblender.desktop"
  DESTINATION share/applications
)

set(MAKEPACKAGE_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/makepackage.sh")

add_custom_target(deb
  COMMAND sh "${MAKEPACKAGE_SCRIPT}" "deb"
  WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
  COMMENT "Building Debian package"
  USES_TERMINAL
)

add_custom_target(tar
  COMMAND sh "${MAKEPACKAGE_SCRIPT}" "tar.gz"
  WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
  COMMENT "Building Debian package"
  USES_TERMINAL
)
