# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: Copyright (C) 2026 Max A. Vilkki a.k.a. codise
#
# libresidfp (GPL-2.0-or-later) from lib/libresidfp (v1.2.2): reSIDfp, the SID chip
# emulation behind libsidplayfp 3.x, which moved it into this separate library.
#
# Upstream builds with autotools. This mirrors src_libresidfp_la_SOURCES in its
# Makefile.am, and generated/ holds what configure would generate (see
# generated/README.md, refreshed by update_generated.sh).
#
# Only the installed headers (<residfp/residfp.h> and friends) are visible to users. The
# engine's own src/ stays private: its SID.h, Filter.h, Voice.h... would collide with
# classic reSID's sid.h, filter.h, voice.h on case-insensitive file systems.
#
# Size (Release, AppleClang 17, arm64): about 60 KB of code and constants per
# architecture before dead-stripping.

if(NOT DEFINED FLIPSID_LIB_DIR)
    set(FLIPSID_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../..")
endif()
set(residfpDir "${FLIPSID_LIB_DIR}/libresidfp")
set(residfpGenerated "${CMAKE_CURRENT_LIST_DIR}/generated")

if(NOT EXISTS "${residfpDir}/configure.ac")
    message(FATAL_ERROR
        "libresidfp: sources not found in ${residfpDir}.\n"
        "Run lib/fetch_externals.sh (lib\\fetch_externals.cmd on Windows), then configure again.")
endif()

# generated/ has to match the clone, or we'd compile one version's sources against
# another's configure output.
function(flipsid_residfp_check_version)
    file(READ "${residfpDir}/configure.ac" configureAc)
    file(READ "${residfpGenerated}/src/residfp/sidversion.h" versionHeader)
    foreach(part major minor level)
        string(REGEX MATCH "m4_define\\(\\[lib_${part}\\], \\[([0-9]+)\\]\\)" _ "${configureAc}")
        list(APPEND cloneVersion "${CMAKE_MATCH_1}")
    endforeach()
    foreach(part MAJ MIN LEV)
        string(REGEX MATCH "LIBRESIDFP_VERSION_${part} ([0-9]+)" _ "${versionHeader}")
        list(APPEND generatedVersion "${CMAKE_MATCH_1}")
    endforeach()
    list(JOIN cloneVersion "." cloneVersion)
    list(JOIN generatedVersion "." generatedVersion)
    if(NOT cloneVersion STREQUAL generatedVersion)
        message(FATAL_ERROR
            "libresidfp: lib/libresidfp is ${cloneVersion} but lib/cmake/libresidfp/generated "
            "is for ${generatedVersion}.\n"
            "Run lib/cmake/libresidfp/update_generated.sh ${cloneVersion} and check "
            "Makefile.am's source list against lib/cmake/libresidfp/CMakeLists.txt.")
    endif()
endfunction()
# file(READ) doesn't make CMake watch a file, so without this a pin bump or a refreshed
# generated/ would skip the check above until someone configured by hand.
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
    "${residfpDir}/configure.ac"
    "${residfpDir}/Makefile.am"
    "${residfpGenerated}/src/residfp/sidversion.h"
)
flipsid_residfp_check_version()

# src_libresidfp_la_SOURCES, compiled files only. Left out: src/resample/test.cpp and
# tests/ (upstream's unit tests).
set(residfpSources
    src/Dac.cpp
    src/EnvelopeGenerator.cpp
    src/ExternalFilter.cpp
    src/Filter.cpp
    src/Filter6581.cpp
    src/Filter8580.cpp
    src/FilterModelConfig.cpp
    src/FilterModelConfig6581.cpp
    src/FilterModelConfig8580.cpp
    src/Integrator6581.cpp
    src/Integrator8580.cpp
    src/OpAmp.cpp
    src/SID.cpp
    src/Spline.cpp
    src/State.cpp
    src/WaveformCalculator.cpp
    src/WaveformGenerator.cpp
    src/resample/SincResampler.cpp
    src/version.cc
    src/residfp/residfp.cpp
)
list(TRANSFORM residfpSources PREPEND "${residfpDir}/")

add_library(ext_residfp STATIC ${residfpSources})
add_library(ext::residfp ALIAS ext_residfp)

# configure asks for the newest standard the compiler has (C++23 down to C++11), without
# GNU extensions. C++20, our standard, has everything the sources check for
# (std::jthread, <numbers>, [[likely]]).
target_compile_features(ext_residfp PRIVATE cxx_std_20)
set_target_properties(ext_residfp PROPERTIES
    OUTPUT_NAME residfp
    CXX_EXTENSIONS OFF
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
)

# generated/src first: siddefs-fp.h and residfp/sidversion.h exist only there.
target_include_directories(ext_residfp PRIVATE
    "${residfpGenerated}/src"
    "${residfpDir}/src"
)

# Users see the installed layout (<residfp/residfp.h>, residfp_defs.h, sidversion.h)
# through forwarding headers written here at configure time. They're generated rather
# than tracked because they need the absolute path of FLIPSID_LIB_DIR.
#
# The pragma: MSVC decides whether a header is external (/external:I) by the directory
# it's found in, not by who includes it, so the upstream headers would otherwise get our
# /W4. Clang and GCC already treat them as system headers.
set(residfpInclude "${CMAKE_CURRENT_BINARY_DIR}/include")
set(residfpForwardTemplate [=[
// Written by lib/cmake/libresidfp/CMakeLists.txt.
#ifdef _MSC_VER
#pragma warning(push, 0)
#endif
#include "@forwardTo@"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
]=])
function(flipsid_residfp_forward header forwardTo)
    file(CONFIGURE OUTPUT "${residfpInclude}/residfp/${header}"
        CONTENT "${residfpForwardTemplate}" @ONLY)
endfunction()
foreach(header residfp.h residfp_defs.h)
    flipsid_residfp_forward(${header} "${residfpDir}/src/residfp/${header}")
endforeach()
flipsid_residfp_forward(sidversion.h "${residfpGenerated}/src/residfp/sidversion.h")
target_include_directories(ext_residfp SYSTEM INTERFACE "${residfpInclude}")

# Private headers, for tests that have to reach into the library (the shared dither noise
# index, tests/engines_write_tee_test.cpp). C++20 because siddefs-fp.h picks features by
# __cplusplus, and the classes must come out as they do in the library.
add_library(ext_residfp_internal INTERFACE)
add_library(ext::residfp_internal ALIAS ext_residfp_internal)
target_include_directories(ext_residfp_internal SYSTEM INTERFACE
    "${residfpGenerated}/src"
    "${residfpDir}/src"
)
target_compile_features(ext_residfp_internal INTERFACE cxx_std_20)
target_link_libraries(ext_residfp_internal INTERFACE ext::residfp)

# The 6581 and 8580 filter tables are built on worker threads (std::jthread in C++20).
find_package(Threads REQUIRED)
target_link_libraries(ext_residfp PRIVATE Threads::Threads)

if(MSVC)
    # /Zc:__cplusplus: siddefs-fp.h picks features by __cplusplus, and stops with
    # "This is not a C++11 compiler" when MSVC reports its default 199711L.
    # /utf-8: a few comments aren't ASCII. /w: third-party warnings aren't ours to fix.
    target_compile_options(ext_residfp PRIVATE /Zc:__cplusplus /utf-8 /w)
else()
    # configure's RESIDFP_CXXFLAGS (added in libsidplayfp 2.15): fast math minus the
    # unsafe optimisations, which leaves finite-math-only and no errno: the compiler may
    # assume no NaN/Inf and inline sqrt, without reordering arithmetic.
    target_compile_options(ext_residfp PRIVATE -ffast-math -fno-unsafe-math-optimizations -w)
    if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        # configure adds it wherever accepted, which is GCC only (Clang rejects it). It
        # lets -O2 builds (RelWithDebInfo) vectorise loops such as the resampler's
        # convolution, as -O3 does anyway.
        target_compile_options(ext_residfp PRIVATE -ftree-loop-vectorize)
    endif()
endif()

# Skipped on purpose:
# - --with-simd (default none): -msse4/-mavx2 would raise the CPU baseline, and
#   RUNTIME_DISPATCH's convolve_<simd>() variants read an uninitialised accumulator in
#   1.2.2. The compiler vectorises the plain loop for SSE2 (x86_64) and NEON (arm64).
# - --enable-lto (default off): left to the top level (CMAKE_INTERPROCEDURAL_OPTIMIZATION).
# - --enable-debug: Debug keeps configure's release header values (inlining on) and only
#   drops NDEBUG, so the asserts work and Debug still runs about 4x real time for one SID
#   (M2), with ext::sidplayfp.

if(COMMAND flipsid_configure_third_party)
    flipsid_configure_third_party(ext_residfp)
endif()
