# SPDX-License-Identifier: GPL-3.0-or-later
# myMPD (c) 2018-2026 Juergen Mang <mail@jcgames.de>
# https://github.com/jcorporation/mympd

# minimal cmake version needed for new option handling
cmake_minimum_required(VERSION 3.13 FATAL_ERROR)
cmake_policy(SET CMP0003 NEW)

# myMPD is written in C
# supported compilers: gcc, clang
project(mympd
  VERSION 25.3.0
  LANGUAGES C
)

# Dependency versions
set(LIBMPDCLIENT_MIN_VERSION "2.26.0")
set(OPENSSL_MIN_VERSION "1.1.0")
set(LUA_MIN_VERSION "5.4.0")

# output binaries in bin directory
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

message("Cmake version: ${CMAKE_VERSION}")
message("Cmake src dir: ${PROJECT_SOURCE_DIR}")
message("Cmake build dir: ${CMAKE_CURRENT_BINARY_DIR}")
message("Cmake build type: ${CMAKE_BUILD_TYPE}")
message("Cmake generator: ${CMAKE_GENERATOR}")
message("Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}")
message("CMAKE_C_FLAGS: ${CMAKE_C_FLAGS}")
message("CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}")
message("Arch: ${CMAKE_C_LIBRARY_ARCHITECTURE}")

# reset cmake default flags
set(CMAKE_C_FLAGS_DEBUG "")
set(CMAKE_C_FLAGS_RELEASE "")
set(CMAKE_C_FLAGS_RELWITHDEBINFO "")
set(CMAKE_C_FLAGS_MINSIZEREL "")

# default is to deliver the assets from htdocs for Debug
# and embedded assets for all other build types
if(NOT DEFINED MYMPD_EMBEDDED_ASSETS)
  if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    set(MYMPD_EMBEDDED_ASSETS "OFF")
  else()
    set(MYMPD_EMBEDDED_ASSETS "ON")
  endif()
endif()

# available options
option(MYMPD_BUILD_TESTING "Enables building of unit tests, default OFF" "OFF")
option(MYMPD_DOC "Installs documentation, default ON" "ON")
option(MYMPD_DOC_HTML "Creates and installs the html documentation, default OFF" "OFF")
option(MYMPD_EMBEDDED_ASSETS "Embed assets in binary, default ON, OFF for Debug" "ON")
option(MYMPD_EMBEDDED_LIBMPDCLIENT "Use embedded libmpdclient, default OFF" "OFF")
option(MYMPD_ENABLE_EXPERIMENTAL "Enable experimental features, default OFF" "OFF")
option(MYMPD_ENABLE_FLAC "Enables flac support, default ON" "ON")
option(MYMPD_ENABLE_IPV6 "Enables IPv6, default ON" "ON")
option(MYMPD_ENABLE_LIBID3TAG "Enables libid3tag support, default ON" "ON")
option(MYMPD_ENABLE_LUA "Enables lua support, default ON" "ON")
option(MYMPD_ENABLE_UTF8 "Enables utf8 support with utf8proc, default ON" "ON")
option(MYMPD_MANPAGES "Creates and installs manpages, default ON" "ON")
option(MYMPD_MINIMAL "Enables minimal myMPD build, disables all MYMPD_ENABLE_* flags, default OFF" "OFF")
option(MYMPD_STARTUP_SCRIPT "Installs the startup script, default ON" "ON")
# sanitizer options
option(MYMPD_ENABLE_ASAN "Enables build with address sanitizer, default OFF" "OFF")
option(MYMPD_ENABLE_TSAN "Enables build with thread sanitizer, default OFF" "OFF")
option(MYMPD_ENABLE_UBSAN "Enables build with undefined behavior sanitizer, default OFF" "OFF")

if(MYMPD_MINIMAL)
  set(MYMPD_ENABLE_FLAC "OFF")
  set(MYMPD_ENABLE_IPV6 "OFF")
  set(MYMPD_ENABLE_LUA "OFF")
  set(MYMPD_ENABLE_LIBID3TAG "OFF")
  set(MYMPD_ENABLE_UTF8 "OFF")
endif()

if(MYMPD_ENABLE_EXPERIMENTAL)
  message("Experimental features enabled")
  add_compile_definitions("MYMPD_ENABLE_EXPERIMENTAL=ON")
endif()

# cmake modules
include(CheckCCompilerFlag)
include(CheckCSourceCompiles)
include(CheckIPOSupported)
include(GNUInstallDirs)

# custom cmake modules
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/cmake/")

# required dependencies
find_library(MATH_LIB m REQUIRED)
find_package(Threads REQUIRED)
find_package(PCRE2 REQUIRED)
find_package(OpenSSL REQUIRED)

if(MYMPD_EMBEDDED_LIBMPDCLIENT STREQUAL "ON")
  message("Using embedded libmpdclient")
else()
  find_package(LIBMPDCLIENT)
  if(LIBMPDCLIENT_FOUND)
    if(LIBMPDCLIENT_VERSION VERSION_LESS "${LIBMPDCLIENT_MIN_VERSION}")
      message(WARNING "libmpdclient ${LIBMPDCLIENT_VERSION} is too old, minimum version is ${LIBMPDCLIENT_MIN_VERSION}")
      message("Using embedded libmpdclient")
      set(MYMPD_EMBEDDED_LIBMPDCLIENT "ON")
      add_compile_definitions("MYMPD_EMBEDDED_LIBMPDCLIENT=ON")
    else()
      message("Using shared libmpdclient ${LIBMPDCLIENT_VERSION}")
      set(MYMPD_EMBEDDED_LIBMPDCLIENT "OFF")
    endif()
  else()
    message("libmpdclient not found, using embedded libmpdclient")
    set(MYMPD_EMBEDDED_LIBMPDCLIENT "ON")
    add_compile_definitions("MYMPD_EMBEDDED_LIBMPDCLIENT=ON")
  endif()
endif()

# check dependencies versions
if(OPENSSL_VERSION VERSION_LESS "${OPENSSL_MIN_VERSION}")
  message(FATAL_ERROR "myMPD requires an OpenSSL version greater or equal ${OPENSSL_MIN_VERSION}")
endif()

# optional dependencies
if(MYMPD_ENABLE_UTF8)
  message("Searching for utf8proc")
  find_package(UTF8PROC)
  if(NOT UTF8PROC_FOUND)
    message(WARNING "Utf8proc is disabled because it was not found")
    set(MYMPD_ENABLE_UTF8 "OFF")
  else()
    add_compile_definitions("MYMPD_ENABLE_UTF8=ON")
  endif()
else()
  message("Utf8proc is disabled by user")
endif()

if(MYMPD_ENABLE_LIBID3TAG)
  message("Searching for libid3tag")
  find_package(LIBID3TAG)
  if(NOT LIBID3TAG_FOUND)
    message(WARNING "Libid3tag is disabled because it was not found")
    set(MYMPD_ENABLE_LIBID3TAG "OFF")
  else()
    add_compile_definitions("MYMPD_ENABLE_LIBID3TAG=ON")
  endif()
else()
  message("Libid3tag is disabled by user")
endif()

if(MYMPD_ENABLE_FLAC)
  message("Searching for flac")
  find_package(FLAC)
  if(NOT FLAC_FOUND)
    message(WARNING "Flac is disabled because it was not found")
    set(MYMPD_ENABLE_FLAC "OFF")
  else()
    add_compile_definitions("MYMPD_ENABLE_FLAC=ON")
  endif()
else()
  message("Flac is disabled by user")
endif()

if(MYMPD_ENABLE_LUA)
  if(EXISTS "/etc/alpine-release")
    set(ENV{LUA_DIR} "/usr/lib/lua5.4")
  endif()
  message("Searching for lua")
  find_package(Lua)
  if(LUA_FOUND)
    if(NOT LUA_VERSION_STRING VERSION_GREATER_EQUAL "${LUA_MIN_VERSION}")
      message(WARNING "Lua is disabled because a version lower than ${LUA_MIN_VERSION} was found")
      set(MYMPD_ENABLE_LUA "OFF")
    else()
      add_compile_definitions("MYMPD_ENABLE_LUA=ON")
    endif()
  else()
    message(WARNING "Lua is disabled because it was not found")
    set(MYMPD_ENABLE_LUA "OFF")
  endif()
else()
  message("Lua is disabled by user")
endif()

# calculate paths
if(CMAKE_INSTALL_PREFIX MATCHES "/usr")
  set(SUBDIR "/${PROJECT_NAME}")
  set(SUBDIRLIB "/lib")
  set(SUBDIRCACHE "/cache")
else()
  # for install in /opt
  set(SUBDIR "")
  set(SUBDIRLIB "")
  set(SUBDIRCACHE "")
endif()

message("Executables in: ${CMAKE_INSTALL_FULL_BINDIR}")

if(CMAKE_INSTALL_PREFIX STREQUAL "/usr/local")
  set(MYMPD_WORK_DIR "/${CMAKE_INSTALL_LOCALSTATEDIR}${SUBDIRLIB}${SUBDIR}")
  set(MYMPD_CACHE_DIR "/${CMAKE_INSTALL_LOCALSTATEDIR}${SUBDIRCACHE}${SUBDIR}")
else()
  set(MYMPD_WORK_DIR "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}${SUBDIRLIB}${SUBDIR}")
  set(MYMPD_CACHE_DIR "${CMAKE_INSTALL_FULL_LOCALSTATEDIR}${SUBDIRCACHE}${SUBDIR}")
endif()

message("Workdir: ${MYMPD_WORK_DIR}")
message("Cachedir: ${MYMPD_CACHE_DIR}")

# create assets and set doc root
set(ENV{MYMPD_BUILDDIR} "${CMAKE_CURRENT_BINARY_DIR}")
set(ENV{MYMPD_ENABLE_LUA} "${MYMPD_ENABLE_LUA}")

if(MYMPD_EMBEDDED_ASSETS)
  message("Embedding assets in binary")
  set(MYMPD_DOC_ROOT "${MYMPD_WORK_DIR}/empty")
  set(MYMPD_LUALIBS_PATH "")
  execute_process(COMMAND "${PROJECT_SOURCE_DIR}/build.sh" createassets RESULT_VARIABLE RC_CREATE_ASSETS)
  # remove object files with embedded assets
  file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/src/CMakeFiles/mympd.dir/webserver/mg_user_data.c.o")
  file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/src/CMakeFiles/mympd.dir/mympd_api/scripts.c.o")
  add_compile_definitions("MYMPD_EMBEDDED_ASSETS=ON")
else()
  message("Serving assets from filesystem")
  set(MYMPD_DOC_ROOT "${PROJECT_SOURCE_DIR}/htdocs")
  set(MYMPD_LUALIBS_PATH "${CMAKE_CURRENT_BINARY_DIR}/contrib/lualibs")
  execute_process(COMMAND "${PROJECT_SOURCE_DIR}/build.sh" copyassets RESULT_VARIABLE RC_CREATE_ASSETS)
endif()

if(RC_CREATE_ASSETS GREATER 0)
  message(FATAL_ERROR "Creating assets failed")
endif()

message("Document root: ${MYMPD_DOC_ROOT}")
message("Docdir: ${CMAKE_INSTALL_FULL_DOCDIR}")

# translation files
if(MYMPD_EMBEDDED_ASSETS)
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/bg-BG.json.gz")
    set(I18N_bg_BG "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/de-DE.json.gz")
    set(I18N_de_DE "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/en-US.json.gz")
    set(I18N_en_US "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/es-AR.json.gz")
    set(I18N_es_AR "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/es-ES.json.gz")
    set(I18N_es_ES "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/es-VE.json.gz")
    set(I18N_es_VE "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/fi-FI.json.gz")
    set(I18N_fi_FI "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/fr-FR.json.gz")
    set(I18N_fr_FR "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/it-IT.json.gz")
    set(I18N_it_IT "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/ja-JP.json.gz")
    set(I18N_ja_JP "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/ko-KR.json.gz")
    set(I18N_ko_KR "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/nl-NL.json.gz")
    set(I18N_nl_NL "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/pl-PL.json.gz")
    set(I18N_pl_PL "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/ru-RU.json.gz")
    set(I18N_ru_RU "ON")
  endif()
  if(EXISTS "${PROJECT_BINARY_DIR}/htdocs/assets/i18n/zh-Hans.json.gz")
    set(I18N_zh_Hans "ON")
  endif()
endif()

# create html documentation
if(MYMPD_DOC_HTML)
  execute_process(COMMAND "${PROJECT_SOURCE_DIR}/build.sh" doc "${PROJECT_BINARY_DIR}/htmldoc" RESULT_VARIABLE RC_CREATE_DOC)
endif()
if(RC_CREATE_DOC GREATER 0)
  message(FATAL_ERROR "Creating documentation failed")
endif()

# configure some files - version and path information
configure_file(src/compile_time.h.in "${PROJECT_BINARY_DIR}/compile_time.h")
configure_file(src/webserver/embedded_files.c.in "${PROJECT_BINARY_DIR}/embedded_files.c")
configure_file(src/scripts/lualibs.c.in "${PROJECT_BINARY_DIR}/lualibs.c")
configure_file(cmake/Install.cmake.in cmake/Install.cmake @ONLY)
configure_file(contrib/initscripts/mympd.service.system.in contrib/initscripts/system/mympd.service @ONLY)
configure_file(contrib/initscripts/mympd.service.user.in contrib/initscripts/user/mympd.service @ONLY)
configure_file(contrib/initscripts/mympd.sysVinit.in contrib/initscripts/mympd.sysVinit @ONLY)
configure_file(contrib/initscripts/mympd.openrc.in contrib/initscripts/mympd.openrc @ONLY)
configure_file(contrib/initscripts/mympd.freebsdrc.in contrib/initscripts/mympd.freebsdrc @ONLY)

# Global compile definitions
add_compile_definitions(
  "MPACK_HAS_CONFIG=1"
  "MG_MAX_HTTP_HEADERS=50"
)

if(MYMPD_ENABLE_IPV6)
  add_compile_definitions("MYMPD_ENABLE_IPV6=ON")
endif()

# Debug defines
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_compile_definitions("MYMPD_DEBUG=ON")
else()
  add_compile_definitions("NDEBUG")
endif()

# global compile options
if(CMAKE_BUILD_TYPE MATCHES "(Debug|Release|RelWithDebInfo|MinSizeRel)")
  set(C_OPTS
    "-D_GLIBCXX_ASSERTIONS"
    "-D_LIBCXX_ENABLE_ASSERTIONS"
    "-fcf-protection=full"
    "-fdata-sections"
    "-ffunction-sections"
    "-fno-delete-null-pointer-checks"
    "-fno-omit-frame-pointer"
    "-fno-plt"
    "-fno-strict-aliasing"
    "-fno-strict-overflow"
    "-fstack-clash-protection"
    "-fstack-protector-strong"
    "-fstrict-flex-arrays=3"
    "-ftrivial-auto-var-init=zero"
    "-mbranch-protection=standard"
    "-mno-omit-leaf-frame-pointer"
    "-mshstk"
    "-pedantic"
    "-std=gnu17"
    "-Wall"
    "-Werror"
    "-Wextra"
    "-Wformat=2"
    "-Wformat-security"
    "-Wformat-nonliteral"
    "-Winit-self"
    "-Wmissing-include-dirs"
    "-Wnested-externs"
    "-Wold-style-definition"
    "-Wredundant-decls"
    "-Wshadow"
    "-Wsign-compare"
    "-Wstrict-prototypes"
    "-Wundef"
    "-Wuninitialized"
    "-Wunused-parameter"
    "-Wvla"
    "-Wwrite-strings"
  )
  # check and set strict global compile flags
  foreach(FLAG IN ITEMS ${C_OPTS})
    message("Checking for compiler flag ${FLAG}")
    unset(COMPILER_SUPPORTS_FLAG CACHE)
    unset(COMPILER_SUPPORTS_FLAG)
    check_c_compiler_flag("${FLAG}" COMPILER_SUPPORTS_FLAG)
    if(COMPILER_SUPPORTS_FLAG)
      add_compile_options("${FLAG}")
    endif()
  endforeach()

  if(NOT MYMPD_ENABLE_ASAN AND NOT MYMPD_ENABLE_UBSAN)
    # incompatible with address sanitizers
    if(NOT CMAKE_C_FLAGS MATCHES "_FORTIFY_SOURCE")
      add_compile_options(
        "-D_FORTIFY_SOURCE=2"
      )
    endif()
  endif()
else()
  # if CMAKE_BUILD_TYPE is neither Release nor Debug,
  # do not alter compile options
endif()

# sanitizers
set(ASAN_FLAGS
  "-fsanitize=address"
  "-fsanitize=leak"
)

set(UBSAN_FLAGS
  "-fsanitize=undefined"
)

set(TSAN_FLAGS
  "-fsanitize=thread"
)

if(MYMPD_ENABLE_ASAN)
  message("Compiling with address sanitizer")
  add_compile_options(
    ${ASAN_FLAGS}
    "-fno-omit-frame-pointer"
  )
  add_compile_definitions("MYMPD_ENABLE_ASAN=ON")
elseif(MYMPD_ENABLE_UBSAN)
  message("Compiling with undefined behavior sanitizer")
  add_compile_options(
    ${UBSAN_FLAGS}
    "-fno-omit-frame-pointer"
  )
  add_compile_definitions("MYMPD_ENABLE_UBSAN=ON")
elseif(MYMPD_ENABLE_TSAN)
  message("Compiling with thread sanitizer")
  add_compile_options(
    ${TSAN_FLAGS}
    "-fno-omit-frame-pointer"
  )
  add_compile_definitions("MYMPD_ENABLE_TSAN=ON")
endif()

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
  add_compile_options(
    "-ggdb"
    "-Og"
  )
  set(CMAKE_INTERPROCEDURAL_OPTIMIZATION FALSE)
elseif(CMAKE_BUILD_TYPE MATCHES "(Release|RelWithDebInfo|MinSizeRel)")
  add_compile_options(
    "-fPIE"
    "-O2"
  )
  if(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
    message("Generating binary with debug symbols")
    add_compile_options("-g")
  endif()
  # IPO/LTO support
  check_ipo_supported(RESULT IPO_SUPPORTED OUTPUT IPO_OUTPUT)
  if(IPO_SUPPORTED)
    message(STATUS "IPO is supported: ${IPO_OUTPUT}")
    set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
  else()
    message(STATUS "IPO is not supported: ${IPO_OUTPUT}")
  endif()
else()
  # if CMAKE_BUILD_TYPE is not Debug, Release, RelWithDebInfo or MinSizeRel
  # do not alter compile options
endif()

# linker flags
if(MYMPD_ENABLE_ASAN)
  add_link_options(${ASAN_FLAGS})
elseif(MYMPD_ENABLE_UBSAN)
  add_link_options(${UBSAN_FLAGS})
elseif(MYMPD_ENABLE_TSAN)
  add_link_options(${TSAN_FLAGS})
endif()

if(CMAKE_BUILD_TYPE MATCHES "(Release|RelWithDebInfo|MinSizeRel)")
  add_link_options(
    "-pie"
    "-Wl,-z,nodlopen"
    "-Wl,-z,noexecstack"
    "-Wl,-z,relro"
    "-Wl,-z,now"
    "-Wl,--as-needed"
    "-Wl,--no-copy-dt-needed-entries"
    "-Wl,--gc-sections"
  )
  if(CMAKE_BUILD_TYPE MATCHES "(Release|MinSizeRel)")
    message("Generating stripped binary")
    add_link_options("-s")
  endif()
else()
  # if CMAKE_BUILD_TYPE is not Release, RelWithDebInfo or MinSizeRel
  # do not alter link options
endif()

# distributed libraries
add_subdirectory("dist")

# the main mympd target
add_subdirectory("src")

# command line utilities
add_subdirectory("cli_tools")

# link all together
target_link_libraries(mympd
  mpack
  mongoose
  rax
  sds
  ${CMAKE_THREAD_LIBS_INIT}
  ${MATH_LIB}
  ${PCRE2_LIBRARIES}
  ${OPENSSL_LIBRARIES}
)

# link optional dependencies
if(MYMPD_ENABLE_UTF8)
  target_link_libraries(mympd ${UTF8PROC_LIBRARIES})
endif()
if(MYMPD_ENABLE_LIBID3TAG)
  target_link_libraries(mympd ${LIBID3TAG_LIBRARIES})
endif()
if(MYMPD_ENABLE_FLAC)
  target_link_libraries(mympd ${FLAC_LIBRARIES})
endif()
if(MYMPD_ENABLE_LUA)
  target_link_libraries(mympd ${LUA_LIBRARIES})
endif()
if(MYMPD_EMBEDDED_LIBMPDCLIENT)
  target_link_libraries(mympd mpdclient)
else()
  target_link_libraries(mympd ${LIBMPDCLIENT_LIBRARIES})
endif()

# install
install(TARGETS mympd DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})
install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/cmake/Install.cmake)
install(PROGRAMS ${PROJECT_SOURCE_DIR}/cli_tools/mympd-config/mympd-config DESTINATION ${CMAKE_INSTALL_FULL_BINDIR})

# test
if(MYMPD_BUILD_TESTING)
  include(CTest)
  add_subdirectory(test)
endif()
