cmake_minimum_required(VERSION 3.18...3.27)

# Warn if the user invokes CMake directly
if (NOT SKBUILD)
  message(WARNING "\
  This CMake file is meant to be executed using 'scikit-build-core'.
  Running it directly will almost certainly not produce the desired
  result. If you are a user trying to install this package, use the
  command below, which will install all necessary build dependencies,
  compile the package in an isolated environment, and then install it.
  =====================================================================
   $ pip install .
  =====================================================================
  If you are a software developer, and this is your own package, then
  it is usually much more efficient to install the build dependencies
  in your environment once and use the following command that avoids
  a costly creation of a new virtual environment at every compilation:
  =====================================================================
   $ pip install pybind11 scikit-build-core[pyproject]
   $ pip install --no-build-isolation -ve .
  =====================================================================
  You may optionally add -Ceditable.rebuild=true to auto-rebuild when
  the package is imported. Otherwise, you need to rerun the above
  after editing C++ files.")
endif()



# Find Python and pybind11 first, as we need the interpreter
set(PYBIND11_FINDPYTHON ON)
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
find_package(pybind11 CONFIG REQUIRED)

message(STATUS "DEBUG: Python_FOUND: ${Python_FOUND}")
message(STATUS "DEBUG: Python_Interpreter_FOUND: ${Python_Interpreter_FOUND}")
message(STATUS "DEBUG: Python_Development_FOUND: ${Python_Development_FOUND}")
message(STATUS "DEBUG: Python_Development.Module_FOUND: ${Python_Development.Module_FOUND}")
message(STATUS "DEBUG: Python_LIBRARIES: ${Python_LIBRARIES}")
message(STATUS "DEBUG: Python_LIBRARY_DIRS: ${Python_LIBRARY_DIRS}")
message(STATUS "DEBUG: Python_INCLUDE_DIRS: ${Python_INCLUDE_DIRS}")
message(STATUS "DEBUG: Python_EXECUTABLE: ${Python_EXECUTABLE}")
message(STATUS "DEBUG: Python_VERSION: ${Python_VERSION}")

# TODO(psobot): This shouldn't be necessary, but our CI build fails on Windows without it.
if(WIN32 AND NOT DEFINED Python_LIBRARY)
    # Dynamically construct the Python_LIBRARY path using version info from find_package(Python)
    if (NOT Python_VERSION)
        message(FATAL_ERROR "Python_VERSION not set after find_package(Python). Cannot construct Python library path.")
    endif()

    # Split the version string into major, minor, and patch parts
    string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.(.+)" _ ${Python_VERSION})
    set(Python_VERSION_MAJOR ${CMAKE_MATCH_1})
    set(Python_VERSION_MINOR ${CMAKE_MATCH_2})
    set(Python_VERSION_PATCH ${CMAKE_MATCH_3})

    set(DYNAMIC_PYTHON_LIB_FILENAME "python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}.lib")
    set(EXPECTED_PYTHON_VERSION_DIR "C:/hostedtoolcache/windows/Python/${Python_VERSION}/x64/libs")
    set(Python_LIBRARY "${EXPECTED_PYTHON_VERSION_DIR}/${DYNAMIC_PYTHON_LIB_FILENAME}")

    message(STATUS "DEBUG: On Windows, constructed DYNAMIC_PYTHON_LIB_FILENAME: ${DYNAMIC_PYTHON_LIB_FILENAME}")
    message(STATUS "DEBUG: On Windows, constructed Python_LIBRARY path: ${Python_LIBRARY}")
endif()

# Set up ccache if available
find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
    message(STATUS "Using ccache: ${CCACHE_PROGRAM}")
endif()

# Extract version from pedalboard/version.py
execute_process(
    COMMAND ${CMAKE_COMMAND} -E echo "with open('pedalboard/version.py') as f: exec(f.read()); print(MAJOR, MINOR, PATCH, sep='.')"
    COMMAND ${Python_EXECUTABLE} -
    OUTPUT_VARIABLE PEDALBOARD_VERSION
    OUTPUT_STRIP_TRAILING_WHITESPACE
)

project(pedalboard 
    VERSION ${PEDALBOARD_VERSION}
    LANGUAGES CXX C
    DESCRIPTION "A Python library for adding effects to audio."
)

# Global flags
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# Debug/Release configuration
option(DEBUG "Build with debug symbols" OFF)

if(DEBUG)
    add_compile_definitions(DEBUG=1 _DEBUG=1)
    add_compile_options(-O0 -g)
else()
    if(MSVC)
        add_compile_options(/Ox)
    else()
        add_compile_options(-O3)
    endif()
endif()

# Add flags based on compiler/platform
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    add_compile_options(-Wall)
endif()

# Set up includes and library sources
set(ALL_INCLUDES
    "vendors/pybind11/include/"
    "JUCE/modules/"
    "JUCE/modules/juce_audio_processors/format_types/VST3_SDK/"
    "JUCE/modules/juce_audio_formats/codecs/"
    "vendors/"
    "vendors/libgsm/inc/"
    "vendors/lame/include/"
    "vendors/lame/libmp3lame/"
    "vendors/lame/"
)

# Add any paths containing subfolders that need to be included
file(GLOB GSM_INCLUDE_DIRS "vendors/libgsm/inc")
list(APPEND ALL_INCLUDES ${GSM_INCLUDE_DIRS})

# Platform-specific settings
if(APPLE)
    # macOS specific flags
    add_compile_definitions(MACOS=1 HAVE_VDSP=1)

    # MacOS Frameworks
    find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
    find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
    find_library(AUDIOTOOLBOX_FRAMEWORK AudioToolbox REQUIRED)
    find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
    find_library(COREAUDIO_FRAMEWORK CoreAudio REQUIRED)
    find_library(COREAUDIOKIT_FRAMEWORK CoreAudioKit REQUIRED)
    find_library(COREMIDI_FRAMEWORK CoreMIDI REQUIRED)
    find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
    find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
    find_library(QUARTZ_FRAMEWORK QuartzCore REQUIRED)
    find_library(WEBKIT_FRAMEWORK WebKit REQUIRED)

    set(MACOS_FRAMEWORKS
        ${ACCELERATE_FRAMEWORK}
        ${APPKIT_FRAMEWORK}
        ${AUDIOTOOLBOX_FRAMEWORK}
        ${COCOA_FRAMEWORK}
        ${COREAUDIO_FRAMEWORK}
        ${COREAUDIOKIT_FRAMEWORK}
        ${COREMIDI_FRAMEWORK}
        ${FOUNDATION_FRAMEWORK}
        ${IOKIT_FRAMEWORK}
        ${QUARTZ_FRAMEWORK}
        ${WEBKIT_FRAMEWORK}
    )

    add_compile_definitions(JUCE_PLUGINHOST_AU=1)
    
    # No threading code needed on macOS, vDSP does this for us
    add_compile_definitions(NO_THREADING)

    # File extension handling for Objective-C++
    list(APPEND CMAKE_FIND_LIBRARY_SUFFIXES ".mm")
elseif(WIN32)
    # Windows specific flags
    add_compile_definitions(WINDOWS=1 USE_BUILTIN_FFT NO_THREADING)
    add_compile_definitions(JUCE_DLL_BUILD=1)

    # python_bindings.cpp generates a very large number of COFF sections (from
    # the pybind11 template instantiations); /bigobj raises the per-object
    # section limit so MSVC doesn't fail with error C1128.
    add_compile_options(/bigobj)
    
    # Windows-specific libraries
    list(APPEND PLATFORM_LIBRARIES
        kernel32 user32 gdi32 winspool comdlg32 advapi32 shell32 
        ole32 oleaut32 uuid odbc32 odbccp32
    )
elseif(UNIX AND NOT APPLE)
    # Linux specific flags
    add_compile_definitions(LINUX=1)
    
    # FFTW support for Linux
    add_compile_definitions(
        HAVE_FFTW3=1
        LACK_SINCOS=1 
        FFTW_DOUBLE_ONLY=1
        USE_PTHREADS
    )
    
    # Linux Dependencies
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(FREETYPE REQUIRED freetype2)
    find_package(ALSA REQUIRED)
    
    include_directories(${FREETYPE_INCLUDE_DIRS})
    list(APPEND PLATFORM_LIBRARIES ${FREETYPE_LIBRARIES} ${ALSA_LIBRARIES})
    
    # Processor-specific optimizations (x86 vs ARM)
    if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64")
        add_compile_definitions(HAVE_NEON=1)
    else()
        # Use -march=native for local builds to optimize for the current CPU,
        # but use a portable baseline for CI builds to avoid "Illegal instruction" errors
        # when ccache restores objects built on different runner hardware.
        if(DEFINED ENV{USE_PORTABLE_SIMD})
            add_compile_options(-mavx)
        else()
            add_compile_options(-march=native)
        endif()
        add_compile_definitions(HAVE_AVX)
    endif()
    
    # Additional Linux flags
    list(APPEND ALL_INCLUDES "vendors/fftw3/api/" "vendors/fftw3/")
endif()

# JUCE-related flags
list(APPEND JUCE_COMPILE_DEFINITIONS
    JUCE_DISPLAY_SPLASH_SCREEN=1
    JUCE_USE_DARK_SPLASH_SCREEN=1
    JUCE_MODULE_AVAILABLE_juce_audio_basics=1
    JUCE_MODULE_AVAILABLE_juce_audio_formats=1
    JUCE_MODULE_AVAILABLE_juce_audio_processors=1
    JUCE_MODULE_AVAILABLE_juce_core=1
    JUCE_MODULE_AVAILABLE_juce_data_structures=1
    JUCE_MODULE_AVAILABLE_juce_dsp=1
    JUCE_MODULE_AVAILABLE_juce_events=1
    JUCE_MODULE_AVAILABLE_juce_graphics=1
    JUCE_MODULE_AVAILABLE_juce_gui_basics=1
    JUCE_MODULE_AVAILABLE_juce_gui_extra=1
    JUCE_MODULE_AVAILABLE_juce_audio_devices=1
    JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1
    JUCE_STRICT_REFCOUNTEDPOINTER=1
    JUCE_STANDALONE_APPLICATION=1
    JUCER_LINUX_MAKE_6D53C8B4=1
    JUCE_APP_VERSION=1.0.0
    JUCE_APP_VERSION_HEX=0x10000
    JucePlugin_Build_VST=0
    JucePlugin_Build_VST3=0
    JucePlugin_Build_AU=0
    JucePlugin_Build_AUv3=0
    JucePlugin_Build_RTAS=0
    JucePlugin_Build_AAX=0
    JucePlugin_Build_Standalone=0
    JucePlugin_Build_Unity=0
    JUCE_DISABLE_JUCE_VERSION_PRINTING=1
    JUCE_WEB_BROWSER=0
    JUCE_USE_CURL=0
    JUCE_USE_MP3AUDIOFORMAT=0
    JUCE_USE_FLAC=0
    JUCE_MODAL_LOOPS_PERMITTED=1
)

if(DEFINED ENV{CIBW_BUILD} AND "$ENV{CIBW_BUILD}" MATCHES "musllinux")
    # For Alpine/musllinux compatibility:
    add_compile_definitions(
        _NL_IDENTIFICATION_LANGUAGE=0x42
        _NL_IDENTIFICATION_TERRITORY=0x43
        _LARGEFILE64_SOURCE
    )
    # musl doesn't provide execinfo.h; use our stub in vendors/
    include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/vendors)
endif()

# Rubber Band library flags
list(APPEND RUBBERBAND_COMPILE_DEFINITIONS
    USE_BQRESAMPLER=1
    _HAS_STD_BYTE=0
    NOMINMAX
    ALREADY_CONFIGURED
)

# Helper function to collect source files
function(collect_sources)
    # Collect all C++ source files
    file(GLOB_RECURSE CPP_SOURCES "pedalboard/*.cpp")
    
    # Handle platform-specific sources
    if(APPLE)
        file(GLOB_RECURSE OBJC_SOURCES "pedalboard/*.mm")
        # Exclude specific files that shouldn't be compiled directly
        list(FILTER OBJC_SOURCES EXCLUDE REGEX ".*juce_mac_PatchedWindowing\\.mm$")
        
        foreach(OBJC_SOURCE ${OBJC_SOURCES})
            get_filename_component(BASENAME ${OBJC_SOURCE} NAME_WE)
            list(FILTER CPP_SOURCES EXCLUDE REGEX ".*/${BASENAME}\\.cpp$")
        endforeach()
        list(APPEND CPP_SOURCES ${OBJC_SOURCES})
    endif()

    # We explicitly patch include_juce_gui_basics.{mm,cpp} with juce_patched_gui_basics.cpp,
    # so remove the latter from the list of C++ sources
    list(FILTER CPP_SOURCES EXCLUDE REGEX ".*juce_patched_gui_basics\\.cpp$")
    
    # Collect RubberBand sources
    file(GLOB RUBBERBAND_SOURCES "vendors/rubberband/single/*.cpp")
    
    # Collect general vendor sources
    file(GLOB VENDOR_SOURCES "vendors/*.c" "vendors/*.cpp")
    
    # LAME/mpglib sources
    file(GLOB LAME_SOURCES "vendors/lame/libmp3lame/*.c" "vendors/lame/libmp3lame/vector/*.c" "vendors/lame/mpglib/*.c")
    
    # libgsm sources (excluding toast files)
    file(GLOB GSM_SOURCES "vendors/libgsm/src/*.c")
    list(FILTER GSM_SOURCES EXCLUDE REGEX ".*toast.*")
    
    # FFTW sources for Linux
    if(UNIX AND NOT APPLE AND NOT DEFINED CIBW_BUILD OR NOT CIBW_BUILD MATCHES "musllinux")
        file(GLOB_RECURSE FFTW_SOURCES "vendors/fftw3/*.c")
        
        # Exclude files depending on architecture
        if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64")
            list(FILTER FFTW_SOURCES EXCLUDE REGEX ".*(avx|/sse).*")
        else()
            list(FILTER FFTW_SOURCES EXCLUDE REGEX ".*neon.*")
        endif()
        
        # Exclude unused FFTW components
        list(FILTER FFTW_SOURCES EXCLUDE REGEX ".*(altivec|vsx|mpi|threads|tests|tools|/support|common/|libbench|sse2|avx2|avx512|kcvi|avx-128-fma|generic-simd).*")
    endif()
    
    # Combine all sources
    set(ALL_SOURCES 
        ${CPP_SOURCES} 
        ${RUBBERBAND_SOURCES} 
        ${VENDOR_SOURCES} 
        ${LAME_SOURCES} 
        ${GSM_SOURCES}
        ${FFTW_SOURCES}
        PARENT_SCOPE
    )
endfunction()

# Call the function to collect all source files
collect_sources()

# Create the Python module
pybind11_add_module(pedalboard_native ${ALL_SOURCES})

# Add include directories
target_include_directories(pedalboard_native PRIVATE ${ALL_INCLUDES})

if(APPLE)
    # Objective-C++ support for macOS, but only when compiling C++, not C:
    target_compile_options(pedalboard_native PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-xobjective-c++>)
endif()

# Add JUCE and other compile definitions
target_compile_definitions(pedalboard_native PRIVATE ${JUCE_COMPILE_DEFINITIONS})
target_compile_definitions(pedalboard_native PRIVATE ${RUBBERBAND_COMPILE_DEFINITIONS})

# LAME config
if(WIN32)
    target_compile_definitions(pedalboard_native PRIVATE 
        HAVE_MPGLIB 
        HAVE_XMMINTRIN_H
    )
    target_compile_options(pedalboard_native PRIVATE
        "/FI${CMAKE_CURRENT_SOURCE_DIR}/vendors/lame_config.h"
    )
else()
    target_compile_definitions(pedalboard_native PRIVATE 
        HAVE_MPGLIB
    )
    target_compile_options(pedalboard_native PRIVATE
        "-include${CMAKE_CURRENT_SOURCE_DIR}/vendors/lame_config.h"
    )
endif()

# Link libraries
if(APPLE)
    target_link_libraries(pedalboard_native PRIVATE ${MACOS_FRAMEWORKS})
elseif(WIN32)
    target_link_libraries(pedalboard_native PRIVATE ${PLATFORM_LIBRARIES})
    # Link Python libraries
    if(Python_LIBRARIES)
        # This is the ideal case, FindPython worked as expected.
        message(STATUS "DEBUG: Linking against Python_LIBRARIES: ${Python_LIBRARIES}")
        target_link_libraries(pedalboard_native PRIVATE ${Python_LIBRARIES})
    elseif(DEFINED Python_LIBRARY AND EXISTS "${Python_LIBRARY}")
        # Fallback: Python_LIBRARIES is empty, but we have an explicit Python_LIBRARY path.
        message(STATUS "DEBUG: Python_LIBRARIES is empty. Linking directly against Python_LIBRARY: ${Python_LIBRARY}")
        target_link_libraries(pedalboard_native PRIVATE "${Python_LIBRARY}")
    else()
        # Critical error: No way to link Python library on Windows.
        message(FATAL_ERROR "DEBUG: On Windows, Python_LIBRARIES is empty AND Python_LIBRARY is not set or path does not exist ('${Python_LIBRARY}'). Cannot link Python.")
    endif()
elseif(UNIX AND NOT APPLE)
    # Add libatomic for atomic operations on Linux
    target_link_libraries(pedalboard_native PRIVATE ${PLATFORM_LIBRARIES} atomic)

    # Add missing FFTW-specific C flags for Linux
    target_compile_options(pedalboard_native PRIVATE
        $<$<COMPILE_LANGUAGE:C>:
        -DHAVE_UINTPTR_T
        -DPACKAGE="FFTW"
        -DVERSION="0"
        -DPACKAGE_VERSION="00000"
        -DFFTW_CC="clang"
        "-includestring.h"
        "-includestdint.h"
        "-include${CMAKE_CURRENT_SOURCE_DIR}/vendors/fftw3/dft/codelet-dft.h"
        "-include${CMAKE_CURRENT_SOURCE_DIR}/vendors/fftw3/rdft/codelet-rdft.h"
        "-includesys/time.h"
        -DHAVE_INTTYPES_H
        -DHAVE_STDINT_H
        -DHAVE_STDLIB_H
        -DHAVE_STRING_H
        -DHAVE_TIME_H
        -DHAVE_SYS_TIME_H=1
        -DHAVE_UNISTD_H
        -DHAVE_DECL_DRAND48
        -DHAVE_DECL_SRAND48
        -DHAVE_DECL_COSL
        -DHAVE_DECL_SINL
        -DHAVE_DECL_POSIX_MEMALIGN
        -DHAVE_DRAND48
        -DHAVE_SRAND48
        -DHAVE_POSIX_MEMALIGN
        -DHAVE_ISNAN
        -DHAVE_SNPRINTF
        -DHAVE_STRCHR
        -DHAVE_SYSCTL
        -DHAVE_GETTIMEOFDAY>
    )
endif()

# Add LTO if not debugging and not explicitly disabled
if(NOT DEBUG AND NOT DEFINED ENV{DISABLE_LTO})
    include(CheckIPOSupported)
    check_ipo_supported(RESULT HAVE_IPO)
    if(HAVE_IPO)
        set_property(TARGET pedalboard_native PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
    endif()
endif()

# Install targets
install(TARGETS pedalboard_native DESTINATION .) 
