"""gen-dsp adapter: bridge dsp-graph output to gen-dsp platform backends.

Generates adapter C++ code that implements gen-dsp's ``_ext.h`` wrapper
interface by delegating to the dsp-graph compiled API.  Also generates
a ``manifest.json`` compatible with ``gen_dsp.core.manifest.Manifest``.
"""

from __future__ import annotations

import json
import shutil
from pathlib import Path
from typing import TYPE_CHECKING

from gen_dsp.graph.compile import _to_pascal, compile_graph
from gen_dsp.graph.models import Buffer, Graph

if TYPE_CHECKING:
    from gen_dsp.core.manifest import Manifest

# Platform key -> (common-header macro prefix, namespace suffix)
_PLATFORM_INFO: dict[str, tuple[str, str]] = {
    "chuck": ("CHUCK_EXT_NAME", "_chugin"),
    "clap": ("CLAP_EXT_NAME", "_clap"),
    "au": ("AU_EXT_NAME", "_au"),
    "vst3": ("VST3_EXT_NAME", "_vst3"),
    "lv2": ("LV2_EXT_NAME", "_lv2"),
    "sc": ("SC_EXT_NAME", "_sc"),
    "vcvrack": ("VCVRACK_EXT_NAME", "_vcvrack"),
    "daisy": ("DAISY_EXT_NAME", "_daisy"),
    "circle": ("CIRCLE_EXT_NAME", "_circle"),
    "pd": ("PD_EXT_NAME", "_pd"),
    "max": ("MAX_EXT_NAME", "_max"),
}

SUPPORTED_PLATFORMS = set(_PLATFORM_INFO.keys())


def generate_adapter_cpp(graph: Graph, platform: str) -> str:
    """Generate the ``_ext_{platform}.cpp`` adapter source.

    This replaces the genlib-side wrapper with dsp-graph calls while
    keeping gen-dsp's platform-side code unchanged.

    Args:
        graph: Compiled DSP graph.
        platform: gen-dsp platform key (e.g. ``"chuck"``, ``"clap"``).

    Returns:
        Complete C++ source as a string.

    Raises:
        ValueError: If *platform* is not a recognised gen-dsp platform.
    """
    if platform not in _PLATFORM_INFO:
        raise ValueError(
            f"Unknown platform {platform!r}; supported: {sorted(_PLATFORM_INFO)}"
        )

    name = graph.name
    struct = _to_pascal(name) + "State"
    # Max uses t_sample = double; all other platforms use float.
    use_double = platform == "max"
    st = "double" if use_double else "float"

    lines: list[str] = []
    w = lines.append

    w(f"// _ext_{platform}.cpp -- dsp-graph adapter for gen-dsp {platform} backend")
    w("// Generated by dsp-graph")
    w("")
    w(f'#include "gen_ext_common_{platform}.h"')
    w("")
    w("// Include dsp-graph compiled code")
    w(f'#include "{name}.cpp"')
    w("")
    w("typedef void GenState;")
    w("")
    w("namespace WRAPPER_NAMESPACE {")
    w("")

    # -- lifecycle
    w(f"GenState* wrapper_create({st} sr, long bs) {{")
    w("    (void)bs;")
    w(f"    return (GenState*){name}_create((float)sr);")
    w("}")
    w("")
    w("void wrapper_destroy(GenState* state) {")
    w(f"    {name}_destroy(({struct}*)state);")
    w("}")
    w("")
    w("void wrapper_reset(GenState* state) {")
    w(f"    {name}_reset(({struct}*)state);")
    w("}")
    w("")

    # -- perform
    if use_double:
        # Max passes double** I/O buffers; graph processes float internally.
        # Convert at the boundary.
        num_inputs = len(graph.inputs)
        num_outputs = len(graph.outputs)
        w(f"void wrapper_perform(GenState* state, {st}** ins, long numins,")
        w(f"                     {st}** outs, long numouts, long n) {{")
        w("    (void)numins; (void)numouts;")
        # Allocate float scratch buffers on the stack for small block sizes.
        for i in range(num_inputs):
            w(f"    float in{i}[n];")
        for i in range(num_outputs):
            w(f"    float out{i}[n];")
        w(f"    float* fins[{max(num_inputs, 1)}];")
        w(f"    float* fouts[{max(num_outputs, 1)}];")
        for i in range(num_inputs):
            w(f"    fins[{i}] = in{i};")
            w(f"    for (long j = 0; j < n; j++) in{i}[j] = (float)ins[{i}][j];")
        for i in range(num_outputs):
            w(f"    fouts[{i}] = out{i};")
        w(f"    {name}_perform(({struct}*)state, fins, fouts, (int)n);")
        for i in range(num_outputs):
            w(f"    for (long j = 0; j < n; j++) outs[{i}][j] = (double)out{i}[j];")
        w("}")
    else:
        w(f"void wrapper_perform(GenState* state, {st}** ins, long numins,")
        w(f"                     {st}** outs, long numouts, long n) {{")
        w("    (void)numins; (void)numouts;")
        w(f"    {name}_perform(({struct}*)state, ins, outs, (int)n);")
        w("}")
    w("")

    # -- I/O counts
    w(f"int wrapper_num_inputs() {{ return {name}_num_inputs(); }}")
    w(f"int wrapper_num_outputs() {{ return {name}_num_outputs(); }}")
    w(f"int wrapper_num_params() {{ return {name}_num_params(); }}")
    w("")

    # -- param introspection
    w("const char* wrapper_param_name(GenState* state, int index) {")
    w("    (void)state;")
    w(f"    return {name}_param_name(index);")
    w("}")
    w("")
    w("const char* wrapper_param_units(GenState* state, int index) {")
    w("    (void)state; (void)index;")
    w('    return "";')
    w("}")
    w("")
    w(f"{st} wrapper_param_min(GenState* state, int index) {{")
    w("    (void)state;")
    w(f"    return ({st}){name}_param_min(index);")
    w("}")
    w("")
    w(f"{st} wrapper_param_max(GenState* state, int index) {{")
    w("    (void)state;")
    w(f"    return ({st}){name}_param_max(index);")
    w("}")
    w("")
    w("char wrapper_param_hasminmax(GenState* state, int index) {")
    w("    (void)state; (void)index;")
    w("    return 1;")
    w("}")
    w("")
    w(f"void wrapper_set_param(GenState* state, int index, {st} value) {{")
    w(f"    {name}_set_param(({struct}*)state, index, (float)value);")
    w("}")
    w("")
    w(f"{st} wrapper_get_param(GenState* state, int index) {{")
    w(f"    return ({st}){name}_get_param(({struct}*)state, index);")
    w("}")
    w("")

    # -- buffers
    w(f"int wrapper_num_buffers() {{ return {name}_num_buffers(); }}")
    w("")
    w("const char* wrapper_buffer_name(int index) {")
    w(f"    return {name}_buffer_name(index);")
    w("}")
    w("")
    w("int wrapper_load_buffer(int index, const char* path) {")
    w("    (void)index; (void)path;")
    w("    return -1;  // graph-compiled code does not support runtime buffer loading")
    w("}")
    w("")
    w("} // namespace WRAPPER_NAMESPACE")
    w("")

    return "\n".join(lines)


def generate_manifest_obj(graph: Graph) -> "Manifest":
    """Generate a ``Manifest`` dataclass from a DSP graph.

    Args:
        graph: The DSP graph.

    Returns:
        A gen-dsp ``Manifest`` instance.
    """
    from gen_dsp.core.manifest import Manifest, ParamInfo
    from gen_dsp.graph import __version__

    buffer_ids = [n.id for n in graph.nodes if isinstance(n, Buffer)]
    params = [
        ParamInfo(
            index=i,
            name=p.name,
            has_minmax=True,
            min=p.min,
            max=p.max,
            default=p.default,
        )
        for i, p in enumerate(graph.params)
    ]
    return Manifest(
        gen_name=graph.name,
        num_inputs=len(graph.inputs),
        num_outputs=len(graph.outputs),
        params=params,
        buffers=buffer_ids,
        source="dsp-graph",
        version=__version__,
    )


def generate_manifest(graph: Graph) -> str:
    """Generate a ``manifest.json`` compatible with gen-dsp's ``Manifest``.

    Args:
        graph: The DSP graph.

    Returns:
        JSON string.
    """
    from gen_dsp.graph import __version__

    buffer_ids = [n.id for n in graph.nodes if isinstance(n, Buffer)]
    data = {
        "gen_name": graph.name,
        "num_inputs": len(graph.inputs),
        "num_outputs": len(graph.outputs),
        "params": [
            {
                "index": i,
                "name": p.name,
                "has_minmax": True,
                "min": p.min,
                "max": p.max,
                "default": p.default,
            }
            for i, p in enumerate(graph.params)
        ],
        "buffers": buffer_ids,
        "source": "dsp-graph",
        "version": __version__,
    }
    return json.dumps(data, indent=2) + "\n"


def compile_for_gen_dsp(
    graph: Graph,
    output_dir: str | Path,
    platform: str,
) -> Path:
    """Compile a graph and generate gen-dsp adapter files.

    Writes three files to *output_dir*:

    - ``{name}.cpp`` -- compiled dsp-graph C++
    - ``_ext_{platform}.cpp`` -- gen-dsp adapter
    - ``manifest.json`` -- gen-dsp-compatible manifest

    Args:
        graph: The DSP graph.
        output_dir: Where to write files.
        platform: gen-dsp platform key.

    Returns:
        Path to the output directory.
    """
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)

    code = compile_graph(graph)
    (out / f"{graph.name}.cpp").write_text(code)

    adapter = generate_adapter_cpp(graph, platform)
    (out / f"_ext_{platform}.cpp").write_text(adapter)

    manifest = generate_manifest(graph)
    (out / "manifest.json").write_text(manifest)

    return out


def generate_graph_build_file(
    output_dir: Path,
    platform: str,
    lib_name: str,
    gen_name: str,
    num_inputs: int,
    num_outputs: int,
    num_params: int = 0,
    genext_version: str = "0.8.0",
    shared_cache: bool = False,
    midi_defines: str = "",
) -> Path:
    """Generate a simplified build file for a dsp-graph project.

    Unlike gen~ projects, dsp-graph projects have no genlib dependency,
    no gen/ subdirectory, and no json.c/json_builder.c sources.
    The compiled graph code is included via ``#include`` in the adapter.

    Args:
        output_dir: Project directory.
        platform: gen-dsp platform key.
        lib_name: Plugin/external name.
        gen_name: Graph name (used in compile definitions).
        num_inputs: Number of audio inputs.
        num_outputs: Number of audio outputs.
        num_params: Number of parameters.
        genext_version: gen-dsp version string.
        shared_cache: Whether to use shared FetchContent cache.
        midi_defines: Additional MIDI compile definitions.

    Returns:
        Path to the generated build file.
    """
    cmake_platforms = {
        "clap": _cmake_clap,
        "vst3": _cmake_vst3,
        "au": _cmake_au,
        "lv2": _cmake_lv2,
        "sc": _cmake_sc,
    }
    make_platforms = {
        "pd": _makefile_pd,
        "chuck": _makefile_chuck,
        "max": _cmake_max,
        "vcvrack": _makefile_vcvrack,
        "daisy": _makefile_daisy,
        "circle": _makefile_circle,
    }

    generator = cmake_platforms.get(platform) or make_platforms.get(platform)
    if generator is None:
        raise ValueError(
            f"Build file generation not yet supported for platform {platform!r}"
        )

    return generator(
        output_dir=output_dir,
        lib_name=lib_name,
        gen_name=gen_name,
        num_inputs=num_inputs,
        num_outputs=num_outputs,
        num_params=num_params,
        genext_version=genext_version,
        shared_cache=shared_cache,
        midi_defines=midi_defines,
    )


def _cmake_common_header(
    lib_name: str,
    genext_version: str,
    platform: str,
    shared_cache: bool,
) -> str:
    """Generate common CMake header (project, C++ standard, cache)."""
    from gen_dsp.core.cache import get_cache_dir

    cache_dir = str(get_cache_dir())
    use_cache = "TRUE" if shared_cache else "FALSE"

    return f"""\
cmake_minimum_required(VERSION 3.19)

# {platform.upper()} plugin for {lib_name}
# Generated by gen-dsp (dsp-graph source)

set(PROJECT_NAME {lib_name})
project(${{PROJECT_NAME}} VERSION {genext_version})

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if(DEFINED ENV{{GEN_DSP_CACHE_DIR}})
    file(TO_CMAKE_PATH "$ENV{{GEN_DSP_CACHE_DIR}}" _gen_dsp_cache)
    set(FETCHCONTENT_BASE_DIR "${{_gen_dsp_cache}}" CACHE PATH "" FORCE)
elseif({use_cache})
    set(FETCHCONTENT_BASE_DIR "{cache_dir}" CACHE PATH "" FORCE)
endif()
"""


def _cmake_clap(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    shared_cache = bool(kwargs["shared_cache"])
    midi_defines = str(kwargs["midi_defines"])

    content = _cmake_common_header(lib_name, genext_version, "clap", shared_cache)
    content += """
include(FetchContent)
FetchContent_Declare(
    clap
    GIT_REPOSITORY https://github.com/free-audio/clap.git
    GIT_TAG 1.2.2
)
FetchContent_MakeAvailable(clap)

include_directories("${CMAKE_CURRENT_SOURCE_DIR}")

add_library(${PROJECT_NAME} MODULE
    gen_ext_clap.cpp
    _ext_clap.cpp
)

"""
    content += f"""\
target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    CLAP_EXT_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    CLAP_NUM_INPUTS={num_inputs}
    CLAP_NUM_OUTPUTS={num_outputs}
    {midi_defines}
)

target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)
target_link_libraries(${{PROJECT_NAME}} PRIVATE clap)

set_target_properties(${{PROJECT_NAME}} PROPERTIES PREFIX "")

if(APPLE)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES
        BUNDLE TRUE
        BUNDLE_EXTENSION "clap"
        MACOSX_BUNDLE_GUI_IDENTIFIER "com.gen-dsp.${{PROJECT_NAME}}"
        MACOSX_BUNDLE_BUNDLE_VERSION "{genext_version}"
    )
    add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
        COMMAND codesign --force --sign - "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>"
        COMMENT "Ad-hoc signing ${{PROJECT_NAME}}.clap"
    )
    install(DIRECTORY "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>"
            DESTINATION "$ENV{{HOME}}/Library/Audio/Plug-Ins/CLAP")
elseif(WIN32)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".clap")
    install(FILES "$<TARGET_FILE:${{PROJECT_NAME}}>"
            DESTINATION "$ENV{{COMMONPROGRAMFILES}}/CLAP")
elseif(UNIX)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".clap")
    install(FILES "$<TARGET_FILE:${{PROJECT_NAME}}>"
            DESTINATION "$ENV{{HOME}}/.clap")
endif()
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _cmake_au(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    midi_defines = str(kwargs["midi_defines"])

    content = f"""\
cmake_minimum_required(VERSION 3.19)

# AudioUnit plugin for {lib_name}
# Generated by gen-dsp (dsp-graph source)

set(PROJECT_NAME {lib_name})
project(${{PROJECT_NAME}})

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include_directories("${{CMAKE_CURRENT_SOURCE_DIR}}")

add_library(${{PROJECT_NAME}} MODULE
    gen_ext_au.cpp
    _ext_au.cpp
)

target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    AU_EXT_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    AU_NUM_INPUTS={num_inputs}
    AU_NUM_OUTPUTS={num_outputs}
    {midi_defines}
)

target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)
target_link_libraries(${{PROJECT_NAME}} PRIVATE
    "-framework AudioToolbox"
    "-framework CoreFoundation"
    "-framework CoreAudio"
)

set_target_properties(${{PROJECT_NAME}} PROPERTIES
    PREFIX ""
    SUFFIX ""
    BUNDLE TRUE
    BUNDLE_EXTENSION "component"
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.gen-dsp.au.${{PROJECT_NAME}}"
    MACOSX_BUNDLE_BUNDLE_VERSION "{genext_version}"
    MACOSX_BUNDLE_INFO_PLIST "${{CMAKE_CURRENT_SOURCE_DIR}}/Info.plist"
)

add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
    COMMAND codesign --force --sign - "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>"
    COMMENT "Ad-hoc signing ${{PROJECT_NAME}}.component"
)

install(DIRECTORY "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>"
        DESTINATION "$ENV{{HOME}}/Library/Audio/Plug-Ins/Components")
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _cmake_vst3(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    shared_cache = bool(kwargs["shared_cache"])
    midi_defines = str(kwargs["midi_defines"])

    # Generate FUID from plugin ID (same as vst3.py)
    import hashlib

    fuid_input = f"com.gen-dsp.vst3.{lib_name}"
    md5 = hashlib.md5(fuid_input.encode()).digest()
    fuid = [f"0x{int.from_bytes(md5[i : i + 4], 'big'):08X}" for i in range(0, 16, 4)]

    content = _cmake_common_header(lib_name, genext_version, "vst3", shared_cache)
    content += f"""
# VST3 SDK's fdebug.h requires NDEBUG or _DEBUG
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
endif()

set(SMTG_ENABLE_VST3_PLUGIN_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VSTGUI_SUPPORT OFF CACHE BOOL "" FORCE)
set(SMTG_ENABLE_VST3_HOSTING_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMTG_CREATE_PLUGIN_LINK OFF CACHE BOOL "" FORCE)
set(SMTG_RUN_VST_VALIDATOR OFF CACHE BOOL "" FORCE)

include(FetchContent)
FetchContent_Declare(
    vst3sdk
    GIT_REPOSITORY https://github.com/steinbergmedia/vst3sdk.git
    GIT_TAG v3.7.9_build_61
    GIT_SHALLOW ON
)
FetchContent_MakeAvailable(vst3sdk)

set(VST3SDK_SOURCE_DIR "${{vst3sdk_SOURCE_DIR}}")

# Platform-specific entry point
if(APPLE)
    set(PLATFORM_ENTRY "${{VST3SDK_SOURCE_DIR}}/public.sdk/source/main/macmain.cpp")
elseif(UNIX)
    set(PLATFORM_ENTRY "${{VST3SDK_SOURCE_DIR}}/public.sdk/source/main/linuxmain.cpp")
elseif(WIN32)
    set(PLATFORM_ENTRY "${{VST3SDK_SOURCE_DIR}}/public.sdk/source/main/dllmain.cpp")
endif()

smtg_add_vst3plugin(${{PROJECT_NAME}}
    gen_ext_vst3.cpp
    _ext_vst3.cpp
    ${{VST3SDK_SOURCE_DIR}}/public.sdk/source/vst/vstsinglecomponenteffect.cpp
    ${{PLATFORM_ENTRY}}
)

target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    VST3_EXT_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    VST3_NUM_INPUTS={num_inputs}
    VST3_NUM_OUTPUTS={num_outputs}
    VST3_FUID_0={fuid[0]}
    VST3_FUID_1={fuid[1]}
    VST3_FUID_2={fuid[2]}
    VST3_FUID_3={fuid[3]}
    {midi_defines}
)

target_include_directories(${{PROJECT_NAME}} PRIVATE
    ${{CMAKE_CURRENT_SOURCE_DIR}}
)

target_link_libraries(${{PROJECT_NAME}} PRIVATE sdk)

if(MSVC)
    target_compile_options(${{PROJECT_NAME}} PRIVATE /wd4101 /wd4244)
else()
    target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)
endif()

# Post-build fixes for DAW compatibility
if(APPLE)
    add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
        COMMAND python3 -c "import re,sys$<SEMICOLON>p=sys.argv[1]$<SEMICOLON>t=open(p).read()$<SEMICOLON>open(p,'w').write(re.sub(r',(\\\\s*[}}\\\\]])',r'\\\\1',t))"
            "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>/Contents/Resources/moduleinfo.json"
        COMMAND /usr/bin/plutil -replace CFBundlePackageType -string BNDL
            "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>/Contents/Info.plist"
        COMMAND /usr/bin/plutil -replace CFBundleIdentifier -string "com.gen-dsp.vst3.{lib_name}"
            "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>/Contents/Info.plist"
        COMMAND /usr/bin/plutil -replace CFBundleName -string "{lib_name}"
            "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>/Contents/Info.plist"
        COMMAND /usr/bin/codesign --force --sign - "$<TARGET_BUNDLE_DIR:${{PROJECT_NAME}}>"
        COMMENT "Fixing moduleinfo.json and Info.plist for DAW compatibility"
        VERBATIM
    )
endif()
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _cmake_lv2(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    num_params = int(kwargs["num_params"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    shared_cache = bool(kwargs["shared_cache"])
    midi_defines = str(kwargs["midi_defines"])

    content = _cmake_common_header(lib_name, genext_version, "lv2", shared_cache)
    content += f"""
include(FetchContent)
FetchContent_Declare(
    lv2
    GIT_REPOSITORY https://github.com/lv2/lv2.git
    GIT_TAG v1.18.10
    GIT_SHALLOW ON
)
FetchContent_Populate(lv2)

include_directories(
    "${{CMAKE_CURRENT_SOURCE_DIR}}"
    "${{lv2_SOURCE_DIR}}/include"
)

add_library(${{PROJECT_NAME}} MODULE
    gen_ext_lv2.cpp
    _ext_lv2.cpp
)

target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    LV2_EXT_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    LV2_NUM_INPUTS={num_inputs}
    LV2_NUM_OUTPUTS={num_outputs}
    LV2_NUM_PARAMS={num_params}
    {midi_defines}
)

target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)

set_target_properties(${{PROJECT_NAME}} PROPERTIES PREFIX "")
if(APPLE)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".dylib")
elseif(WIN32)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".dll")
else()
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".so")
endif()

set(LV2_BUNDLE_DIR "${{CMAKE_BINARY_DIR}}/{lib_name}.lv2")
add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
    COMMAND ${{CMAKE_COMMAND}} -E make_directory "${{LV2_BUNDLE_DIR}}"
    COMMAND ${{CMAKE_COMMAND}} -E copy "$<TARGET_FILE:${{PROJECT_NAME}}>" "${{LV2_BUNDLE_DIR}}/"
    COMMAND ${{CMAKE_COMMAND}} -E copy "${{CMAKE_CURRENT_SOURCE_DIR}}/manifest.ttl" "${{LV2_BUNDLE_DIR}}/"
    COMMAND ${{CMAKE_COMMAND}} -E copy "${{CMAKE_CURRENT_SOURCE_DIR}}/{lib_name}.ttl" "${{LV2_BUNDLE_DIR}}/"
    COMMENT "Assembling {lib_name}.lv2 bundle"
)

if(APPLE)
    add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
        COMMAND codesign --force --sign - "${{LV2_BUNDLE_DIR}}/$<TARGET_FILE_NAME:${{PROJECT_NAME}}>"
        COMMENT "Ad-hoc signing {lib_name}.lv2"
    )
    install(DIRECTORY "${{LV2_BUNDLE_DIR}}" DESTINATION "$ENV{{HOME}}/Library/Audio/Plug-Ins/LV2")
elseif(UNIX)
    install(DIRECTORY "${{LV2_BUNDLE_DIR}}" DESTINATION "$ENV{{HOME}}/.lv2")
endif()
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _cmake_sc(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    num_params = int(kwargs["num_params"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    shared_cache = bool(kwargs["shared_cache"])
    midi_defines = str(kwargs["midi_defines"])

    content = _cmake_common_header(lib_name, genext_version, "sc", shared_cache)
    content += f"""
include(FetchContent)
FetchContent_Declare(
    supercollider
    URL https://github.com/supercollider/supercollider/archive/refs/tags/Version-3.13.0.tar.gz
    DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_Populate(supercollider)

include_directories(
    "${{CMAKE_CURRENT_SOURCE_DIR}}"
    "${{supercollider_SOURCE_DIR}}/include/plugin_interface"
    "${{supercollider_SOURCE_DIR}}/include/common"
)

add_library(${{PROJECT_NAME}} MODULE
    gen_ext_sc.cpp
    _ext_sc.cpp
)

target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    SC_EXT_NAME={lib_name}
    SC_UGEN_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    SC_NUM_INPUTS={num_inputs}
    SC_NUM_OUTPUTS={num_outputs}
    SC_NUM_PARAMS={num_params}
    {midi_defines}
)

target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)

set_target_properties(${{PROJECT_NAME}} PROPERTIES PREFIX "")
if(APPLE)
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".scx")
else()
    set_target_properties(${{PROJECT_NAME}} PROPERTIES SUFFIX ".so")
endif()

if(APPLE)
    add_custom_command(TARGET ${{PROJECT_NAME}} POST_BUILD
        COMMAND codesign --force --sign - "$<TARGET_FILE:${{PROJECT_NAME}}>"
        COMMENT "Ad-hoc signing ${{PROJECT_NAME}}"
    )
    install(FILES "$<TARGET_FILE:${{PROJECT_NAME}}>"
            DESTINATION "$ENV{{HOME}}/Library/Application Support/SuperCollider/Extensions")
elseif(UNIX)
    install(FILES "$<TARGET_FILE:${{PROJECT_NAME}}>"
            DESTINATION "$ENV{{HOME}}/.local/share/SuperCollider/Extensions")
endif()
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _cmake_max(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])
    midi_defines = str(kwargs["midi_defines"])

    content = f"""\
cmake_minimum_required(VERSION 3.19)

# Max/MSP external for {lib_name}
# Generated by gen-dsp (dsp-graph source)

# Set output directory before including max-sdk-base
set(C74_LIBRARY_OUTPUT_DIRECTORY "${{CMAKE_CURRENT_SOURCE_DIR}}/externals")

set(PROJECT_NAME {lib_name})
project(${{PROJECT_NAME}})

# Include max-sdk-base pre-target configuration
include(${{CMAKE_CURRENT_SOURCE_DIR}}/max-sdk-base/script/max-pretarget.cmake)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include_directories(
    "${{MAX_SDK_INCLUDES}}"
    "${{MAX_SDK_MSP_INCLUDES}}"
    "${{CMAKE_CURRENT_SOURCE_DIR}}"
)

add_library(${{PROJECT_NAME}} MODULE
    gen_ext_max.cpp
    _ext_max.cpp
)

target_compile_definitions(${{PROJECT_NAME}} PRIVATE
    GENLIB_USE_FLOAT32
    GEN_EXT_VERSION="{genext_version}"
    MAX_EXT_NAME={lib_name}
    GEN_EXPORTED_NAME={gen_name}
    GEN_EXPORTED_HEADER="{gen_name}.h"
    GEN_EXPORTED_CPP="{gen_name}.cpp"
    MAX_NUM_INPUTS={num_inputs}
    MAX_NUM_OUTPUTS={num_outputs}
    {midi_defines}
)

if(APPLE)
    target_compile_definitions(${{PROJECT_NAME}} PRIVATE MSP_ON_CLANG)
    target_compile_options(${{PROJECT_NAME}} PRIVATE -Wno-unused-function -Wno-unused-variable)
endif()

# Include max-sdk-base post-target configuration (handles linking, bundle creation, signing)
include(${{CMAKE_CURRENT_SOURCE_DIR}}/max-sdk-base/script/max-posttarget.cmake)
"""
    path = output_dir / "CMakeLists.txt"
    path.write_text(content)
    return path


def _makefile_pd(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    genext_version = str(kwargs["genext_version"])

    content = f"""\
# PureData external for {lib_name}
# Generated by gen-dsp (dsp-graph source)

lib.name = {lib_name}~

{lib_name}~.class.sources = gen_ext_pd.cpp _ext_pd.cpp

cflags = -I. \\
    -DGENLIB_USE_FLOAT32 \\
    -DPD_EXT_NAME={lib_name} \\
    -DGEN_EXT_VERSION="{genext_version}" \\
    -DGEN_EXPORTED_NAME={gen_name} \\
    -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\" \\
    -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\" \\
    -Wno-unused-function -Wno-unused-variable

PDLIBBUILDER_DIR = pd-lib-builder
include $(PDLIBBUILDER_DIR)/Makefile.pdlibbuilder
"""
    path = output_dir / "Makefile"
    path.write_text(content)
    return path


def _makefile_chuck(**kwargs: object) -> Path:
    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    genext_version = str(kwargs["genext_version"])

    content = f"""\
# ChucK chugin for {lib_name}
# Generated by gen-dsp (dsp-graph source)

CHUGIN_NAME={lib_name}

C_MODULES=
CXX_MODULES=gen_ext_chuck.cpp _ext_chuck.cpp

CK_SRC_PATH?=.

current:
	@echo "[chugin build]: please use one of the following configurations:"
	@echo "   make linux, make mac, or make win32"

ifneq ($(CK_TARGET),)
.DEFAULT_GOAL:=$(CK_TARGET)
ifeq ($(MAKECMDGOALS),)
MAKECMDGOALS:=$(.DEFAULT_GOAL)
endif
endif

.PHONY: mac osx linux linux-pulse linux-oss linux-jack linux-alsa win32
mac osx linux linux-pulse linux-oss linux-jack linux-alsa: all

CC=gcc
CXX=gcc
LD=g++

ifneq (,$(strip $(filter mac osx,$(MAKECMDGOALS))))
include makefile.mac
endif

ifneq (,$(strip $(filter linux,$(MAKECMDGOALS))))
include makefile.linux
endif

ifneq (,$(strip $(filter linux-oss,$(MAKECMDGOALS))))
include makefile.linux
endif

ifneq (,$(strip $(filter linux-pulse,$(MAKECMDGOALS))))
include makefile.linux
endif

ifneq (,$(strip $(filter linux-jack,$(MAKECMDGOALS))))
include makefile.linux
endif

ifneq (,$(strip $(filter linux-alsa,$(MAKECMDGOALS))))
include makefile.linux
endif

FLAGS+=-I.
FLAGS+=-DGENLIB_USE_FLOAT32
FLAGS+=-DCHUCK_EXT_NAME={lib_name}
FLAGS+=-DGEN_EXT_VERSION="{genext_version}"
FLAGS+=-DGEN_EXPORTED_NAME={gen_name}
FLAGS+=-DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
FLAGS+=-DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
FLAGS+=-Wno-unused-function -Wno-unused-variable

CXXFLAGS+=-std=c++11

ifneq ($(CHUCK_DEBUG),)
FLAGS+= -g
else
FLAGS+= -O3
endif

ifneq ($(CHUCK_STRICT),)
FLAGS+= -Werror
endif

CK_CHUGIN_STATIC?=0

ifeq ($(CK_CHUGIN_STATIC),0)
SUFFIX=.chug
else
SUFFIX=.schug
FLAGS+= -D__CK_DLL_STATIC__
endif

C_OBJECTS=$(addsuffix .o,$(basename $(C_MODULES)))
CXX_OBJECTS=$(addsuffix .o,$(basename $(CXX_MODULES)))

CHUG=$(addsuffix $(SUFFIX),$(CHUGIN_NAME))

all: $(CHUG)

$(CHUG): $(C_OBJECTS) $(CXX_OBJECTS)
ifeq ($(CK_CHUGIN_STATIC),0)
	$(LD) $(LDFLAGS) -o $@ $^
else
	ar rv $@ $^
	ranlib $@
endif

$(C_OBJECTS): %.o: %.c
	$(CC) $(FLAGS) -c -o $@ $<

$(CXX_OBJECTS): %.o: %.cpp
	$(CXX) $(FLAGS) $(CXXFLAGS) -c -o $@ $<

clean:
	rm -rf $(C_OBJECTS) $(CXX_OBJECTS) $(CHUG) Release Debug
"""
    path = output_dir / "makefile"
    path.write_text(content)
    return path


def _makefile_vcvrack(**kwargs: object) -> Path:
    from gen_dsp.platforms.vcvrack import _get_default_rack_sdk_dir

    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    num_params = int(kwargs["num_params"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])

    default_rack_dir = str(_get_default_rack_sdk_dir())
    total_components = num_inputs + num_outputs + num_params

    # Same HP computation as VcvRackPlatform._compute_panel_hp
    if total_components <= 6:
        panel_hp = 6
    elif total_components <= 12:
        panel_hp = 10
    elif total_components <= 20:
        panel_hp = 16
    else:
        panel_hp = 24

    content = f"""\
# VCV Rack plugin for {lib_name}
# Generated by gen-dsp (dsp-graph source)

RACK_DIR ?= $(shell \\
    if [ -n "$$RACK_DIR" ]; then echo "$$RACK_DIR"; \\
    elif [ -n "$$GEN_DSP_CACHE_DIR" ]; then echo "$$GEN_DSP_CACHE_DIR/rack-sdk-src/Rack-SDK"; \\
    else echo "{default_rack_dir}"; fi)

SOURCES += gen_ext_vcvrack.cpp _ext_vcvrack.cpp plugin.cpp

FLAGS += -I.
FLAGS += -DGENLIB_USE_FLOAT32
FLAGS += -DVCR_EXT_NAME={lib_name}
FLAGS += -DGEN_EXT_VERSION=\\"{genext_version}\\"
FLAGS += -DGEN_EXPORTED_NAME={gen_name}
FLAGS += -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
FLAGS += -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
FLAGS += -DVCR_NUM_INPUTS={num_inputs}
FLAGS += -DVCR_NUM_OUTPUTS={num_outputs}
FLAGS += -DVCR_NUM_PARAMS={num_params}
FLAGS += -DVCR_PANEL_HP={panel_hp}
FLAGS += -Wno-unused-function -Wno-unused-variable

DISTRIBUTABLES += res plugin.json

include $(RACK_DIR)/plugin.mk
"""
    path = output_dir / "Makefile"
    path.write_text(content)
    return path


def _makefile_daisy(**kwargs: object) -> Path:
    from gen_dsp.platforms.daisy import _get_default_libdaisy_dir

    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    num_params = int(kwargs["num_params"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])

    default_libdaisy_dir = str(_get_default_libdaisy_dir())

    content = f"""\
# Daisy firmware for {lib_name}
# Generated by gen-dsp (dsp-graph source)

TARGET = {lib_name}

# Resolve libDaisy directory (priority: env LIBDAISY_DIR > GEN_DSP_CACHE_DIR > default)
ifdef LIBDAISY_DIR
  # Use explicit env var
else ifdef GEN_DSP_CACHE_DIR
  LIBDAISY_DIR = $(GEN_DSP_CACHE_DIR)/libdaisy-src/libDaisy
else
  LIBDAISY_DIR = {default_libdaisy_dir}
endif

CPP_SOURCES = gen_ext_daisy.cpp _ext_daisy.cpp

# No genlib sources in graph path (graph-compiled code uses standard C++ new/delete)

OPT = -O3
SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core
include $(SYSTEM_FILES_DIR)/Makefile

# Append gen-dsp flags after libDaisy's Makefile (so they're not overwritten)
CFLAGS += -I.
CFLAGS += -DGENLIB_USE_FLOAT32
CFLAGS += -DGENLIB_NO_JSON
CFLAGS += -DDAISY_EXT_NAME={lib_name}
CFLAGS += -DGEN_EXT_VERSION=\\"{genext_version}\\"
CFLAGS += -DGEN_EXPORTED_NAME={gen_name}
CFLAGS += -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
CFLAGS += -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
CFLAGS += -DDAISY_NUM_INPUTS={num_inputs}
CFLAGS += -DDAISY_NUM_OUTPUTS={num_outputs}
CFLAGS += -DDAISY_NUM_PARAMS={num_params}
CFLAGS += -Wno-unused-function -Wno-unused-variable

CPPFLAGS += -I.
CPPFLAGS += -DGENLIB_USE_FLOAT32
CPPFLAGS += -DGENLIB_NO_JSON
CPPFLAGS += -DDAISY_EXT_NAME={lib_name}
CPPFLAGS += -DGEN_EXT_VERSION=\\"{genext_version}\\"
CPPFLAGS += -DGEN_EXPORTED_NAME={gen_name}
CPPFLAGS += -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
CPPFLAGS += -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
CPPFLAGS += -DDAISY_NUM_INPUTS={num_inputs}
CPPFLAGS += -DDAISY_NUM_OUTPUTS={num_outputs}
CPPFLAGS += -DDAISY_NUM_PARAMS={num_params}
CPPFLAGS += -Wno-unused-function -Wno-unused-variable
"""
    path = output_dir / "Makefile"
    path.write_text(content)
    return path


def _makefile_circle(**kwargs: object) -> Path:
    from gen_dsp.platforms.circle import _get_default_circle_dir

    output_dir = kwargs["output_dir"]
    assert isinstance(output_dir, Path)
    lib_name = str(kwargs["lib_name"])
    gen_name = str(kwargs["gen_name"])
    num_inputs = int(kwargs["num_inputs"])  # type: ignore[call-overload]
    num_outputs = int(kwargs["num_outputs"])  # type: ignore[call-overload]
    num_params = int(kwargs["num_params"])  # type: ignore[call-overload]
    genext_version = str(kwargs["genext_version"])

    default_circle_dir = str(_get_default_circle_dir())

    # Default board: pi3-i2s (rasppi=3, aarch=64, prefix=aarch64-none-elf-)
    content = f"""\
# Circle bare-metal firmware for {lib_name}
# Generated by gen-dsp (dsp-graph source)
# Default board: Raspberry Pi 3, I2S audio

# Resolve Circle directory
ifdef CIRCLE_DIR
  CIRCLEHOME = $(CIRCLE_DIR)
else ifdef GEN_DSP_CACHE_DIR
  CIRCLEHOME = $(GEN_DSP_CACHE_DIR)/circle-src/circle
else
  CIRCLEHOME ?= {default_circle_dir}
endif

override RASPPI = 3
override AARCH = 64
override PREFIX = aarch64-none-elf-

OBJS = gen_ext_circle.o _ext_circle.o

# No genlib sources in graph path (graph-compiled code uses standard C++ new/delete)

include $(CIRCLEHOME)/Rules.mk

CFLAGS += -I.
CFLAGS += -DGENLIB_USE_FLOAT32
CFLAGS += -DGENLIB_NO_JSON
CFLAGS += -DCIRCLE_EXT_NAME={lib_name}
CFLAGS += -DGEN_EXT_VERSION=\\"{genext_version}\\"
CFLAGS += -DGEN_EXPORTED_NAME={gen_name}
CFLAGS += -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
CFLAGS += -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
CFLAGS += -DCIRCLE_NUM_INPUTS={num_inputs}
CFLAGS += -DCIRCLE_NUM_OUTPUTS={num_outputs}
CFLAGS += -DCIRCLE_NUM_PARAMS={num_params}
CFLAGS += -Wno-unused-function -Wno-unused-variable

CPPFLAGS += -I.
CPPFLAGS += -DGENLIB_USE_FLOAT32
CPPFLAGS += -DGENLIB_NO_JSON
CPPFLAGS += -DCIRCLE_EXT_NAME={lib_name}
CPPFLAGS += -DGEN_EXT_VERSION=\\"{genext_version}\\"
CPPFLAGS += -DGEN_EXPORTED_NAME={gen_name}
CPPFLAGS += -DGEN_EXPORTED_HEADER=\\"{gen_name}.h\\"
CPPFLAGS += -DGEN_EXPORTED_CPP=\\"{gen_name}.cpp\\"
CPPFLAGS += -DCIRCLE_NUM_INPUTS={num_inputs}
CPPFLAGS += -DCIRCLE_NUM_OUTPUTS={num_outputs}
CPPFLAGS += -DCIRCLE_NUM_PARAMS={num_params}
CPPFLAGS += -Wno-unused-function -Wno-unused-variable

LIBS += $(CIRCLEHOME)/lib/sound/libsound.a $(CIRCLEHOME)/lib/libcircle.a
"""
    path = output_dir / "Makefile"
    path.write_text(content)
    return path


def _copy_platform_templates(output_dir: Path, platform: str) -> None:
    """Copy gen-dsp platform template files to the output directory."""
    import gen_dsp.templates as templates

    getter = getattr(templates, f"get_{platform}_templates_dir", None)
    if getter is None:
        raise ValueError(f"No gen-dsp template directory for platform {platform!r}")

    tmpl_dir = getter()
    if not tmpl_dir.is_dir():
        raise FileNotFoundError(f"Template directory not found: {tmpl_dir}")

    # Files to copy (platform-side code + interface header + common header)
    targets = [
        f"gen_ext_{platform}.cpp",
        f"gen_ext_common_{platform}.h",
        f"_ext_{platform}.h",
    ]

    for fname in targets:
        src = tmpl_dir / fname
        if src.is_file():
            shutil.copy2(src, output_dir / fname)

    # Copy platform-specific extras
    if platform == "pd":
        # pd-lib-builder directory
        pd_lib_builder_src = tmpl_dir / "pd-lib-builder"
        if pd_lib_builder_src.is_dir():
            shutil.copytree(
                pd_lib_builder_src,
                output_dir / "pd-lib-builder",
                dirs_exist_ok=True,
            )

        # m_pd.h into pd-include/ (graph template uses pd-include/ path)
        m_pd_src = tmpl_dir / "m_pd.h"
        if m_pd_src.is_file():
            dest = output_dir / "pd-include"
            dest.mkdir(parents=True, exist_ok=True)
            shutil.copy2(m_pd_src, dest / "m_pd.h")

    elif platform == "chuck":
        # chugin.h bundle
        chugin_src = tmpl_dir / "chuck" / "include" / "chugin.h"
        if chugin_src.is_file():
            dest = output_dir / "chuck" / "include"
            dest.mkdir(parents=True, exist_ok=True)
            shutil.copy2(chugin_src, dest / "chugin.h")

        # makefile templates
        for mf in ("makefile.mac", "makefile.linux"):
            src = tmpl_dir / mf
            if src.is_file():
                shutil.copy2(src, output_dir / mf)

    elif platform == "vcvrack":
        # plugin.cpp and plugin.hpp entry point files
        for fname in ("plugin.cpp", "plugin.hpp"):
            src = tmpl_dir / fname
            if src.is_file():
                shutil.copy2(src, output_dir / fname)

    elif platform == "circle":
        # C++ stdlib shims for bare-metal builds (Circle's -nostdinc++ strips them)
        for shim in ("cmath", "cstdlib", "cstdint", "cstring"):
            src = tmpl_dir / shim
            if src.is_file():
                shutil.copy2(src, output_dir / shim)


def _generate_buffer_header(output_dir: Path) -> None:
    """Generate a gen_buffer.h with zero buffers (dsp-graph manages its own)."""
    content = (
        "// gen_buffer.h -- auto-generated by dsp-graph\n"
        "#ifndef GEN_BUFFER_H\n"
        "#define GEN_BUFFER_H\n"
        "\n"
        "#define WRAPPER_BUFFER_COUNT 0\n"
        "\n"
        "#endif // GEN_BUFFER_H\n"
    )
    (output_dir / "gen_buffer.h").write_text(content)
