cmake_minimum_required(VERSION 3.20)
project(UsdLayerManager VERSION 1.0.0 LANGUAGES C CXX)

# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# Add cmake modules path
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")

# Find dependencies
find_package(OpenGL REQUIRED)
find_package(OpenUSD REQUIRED)
find_package(Imgui REQUIRED)
find_package(ImguiNodeEditor REQUIRED)
find_package(Glad REQUIRED)
find_package(FFmpeg REQUIRED)

# OpenColorIO — provided by the OpenUSD distribution (USD built with OCIO support)
set(OpenColorIO_DIR
    "${OpenUSD_ROOT_DIR}/lib/cmake/OpenColorIO"
    CACHE PATH "OpenColorIO CMake config dir" FORCE)
find_package(OpenColorIO CONFIG REQUIRED)

# USD build that includes hdEmbree (built with PXR_ENABLE_EMBREE_PLUGIN=ON).
# Can be a separate install from OpenUSD_ROOT_DIR; leave empty to search there instead.
set(HDEMBREE_USD_ROOT "" CACHE PATH "USD build containing hdEmbree plugin (separate from OpenUSD_ROOT_DIR)")
# Only needed if embree*.dll are not inside HDEMBREE_USD_ROOT/bin.
set(EMBREE_LOCATION "" CACHE PATH "Embree SDK bin/ directory (leave empty if bundled in HDEMBREE_USD_ROOT/bin)")

# ── hdEmbree detection ───────────────────────────────────────────────────────
if(HDEMBREE_USD_ROOT AND EXISTS "${HDEMBREE_USD_ROOT}")
    set(_hdembree_root "${HDEMBREE_USD_ROOT}")
else()
    set(_hdembree_root "${OpenUSD_ROOT_DIR}")
endif()

# hdEmbree.dll lives at plugin/usd/hdEmbree.dll; the subdir holds only plugin metadata.
set(HDEMBREE_PLUGIN_DIR "${_hdembree_root}/plugin/usd/hdEmbree")
set(HDEMBREE_DLL        "${_hdembree_root}/plugin/usd/hdEmbree.dll")

if(EXISTS "${HDEMBREE_PLUGIN_DIR}")
    set(OPENUSD_HAS_EMBREE TRUE)
    if(EMBREE_LOCATION AND EXISTS "${EMBREE_LOCATION}")
        set(_embree_dll_dir "${EMBREE_LOCATION}")
    else()
        set(_embree_dll_dir "${_hdembree_root}/bin")
    endif()
    # Skip tbb*.dll — already supplied by the main USD install to avoid conflicts.
    file(GLOB EMBREE_DLLS "${_embree_dll_dir}/embree*.dll")
else()
    set(OPENUSD_HAS_EMBREE FALSE)
    message(STATUS "hdEmbree: NOT found — set HDEMBREE_USD_ROOT to a USD build with PXR_ENABLE_EMBREE_PLUGIN=ON")
endif()

# ── hdArnold detection ───────────────────────────────────────────────────────
# HDARNOLD_ROOT: CMAKE_INSTALL_PREFIX used when building arnold-usd
set(HDARNOLD_ROOT "" CACHE PATH "arnold-usd install directory (the one containing plugin/hdArnold.dll)")
# ARNOLD_LOCATION: Arnold SDK root (MtoA dir) — needed to deploy ai.dll
set(ARNOLD_LOCATION "" CACHE PATH "Arnold SDK root (e.g. C:/Autodesk/mtoa/5.5.0/2026) for ai.dll deployment")

if(HDARNOLD_ROOT AND EXISTS "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
    set(OPENUSD_HAS_ARNOLD TRUE)
    set(HDARNOLD_DLL          "${HDARNOLD_ROOT}/plugin/hdArnold.dll")
    set(NDRARNOLD_DLL         "${HDARNOLD_ROOT}/plugin/ndrArnold.dll")
    set(HDARNOLD_PLUGIN_DIR   "${HDARNOLD_ROOT}/plugin/hdArnold")
    set(NDRARNOLD_PLUGIN_DIR  "${HDARNOLD_ROOT}/plugin/ndrArnold")
    if(ARNOLD_LOCATION AND EXISTS "${ARNOLD_LOCATION}/bin/ai.dll")
        set(ARNOLD_AI_DLL "${ARNOLD_LOCATION}/bin/ai.dll")
        # ai.dll also requires companion runtime DLLs (OIDN denoiser, Adsk licensing, cer)
        file(GLOB ARNOLD_RUNTIME_DLLS
            "${ARNOLD_LOCATION}/bin/ai*.dll"
            "${ARNOLD_LOCATION}/bin/AdClm*.dll"
            "${ARNOLD_LOCATION}/bin/AdskLicensing*.dll"
            "${ARNOLD_LOCATION}/bin/cer.dll"
        )
    else()
        set(ARNOLD_AI_DLL "")
        set(ARNOLD_RUNTIME_DLLS "")
    endif()
else()
    set(OPENUSD_HAS_ARNOLD FALSE)
    message(STATUS "hdArnold: NOT found — set HDARNOLD_ROOT to the arnold-usd install dir and ARNOLD_LOCATION to the MtoA root")
endif()

# ── Cycles hdCycles Hydra render delegate ────────────────────────────────────
# Build Blender Cycles from third_party/cycles using ExternalProject.
# Set WITH_CYCLES=ON to enable; requires MSVC, and uses the precompiled libs
# bundled in third_party/cycles/lib/windows_x64/.
# hdCycles.dll is deployed to usd/ alongside hdEmbree and hdArnold.
option(WITH_CYCLES "Build Cycles Hydra render delegate from third_party/cycles" OFF)

set(CYCLES_BINARY_DIR  "${CMAKE_BINARY_DIR}/cycles_build")
set(CYCLES_INSTALL_DIR "${CMAKE_BINARY_DIR}/cycles_install")
set(HDCYCLES_DLL        "${CYCLES_INSTALL_DIR}/hydra/hdCycles.dll")
set(HDCYCLES_PLUGIN_DIR "${CYCLES_INSTALL_DIR}/hydra/hdCycles")
# tbb12.dll is required by openvdb/embree4 but lives in the precompiled lib tree,
# not in the install output — deploy it explicitly.
set(CYCLES_TBB12_DLL "${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/bin/tbb12.dll")

if(WITH_CYCLES)
    include(ExternalProject)

    # Resolve absolute USD root so we can forward it to the sub-build.
    # FindOpenUSD.cmake stores the path in OpenUSD_ROOT_DIR.
    if(NOT OpenUSD_ROOT_DIR)
        message(FATAL_ERROR "WITH_CYCLES requires OpenUSD_ROOT_DIR to be set (run find_package(OpenUSD) first).")
    endif()

    ExternalProject_Add(CyclesBuild
        SOURCE_DIR "${CMAKE_SOURCE_DIR}/third_party/cycles"
        BINARY_DIR "${CYCLES_BINARY_DIR}"
        INSTALL_DIR "${CYCLES_INSTALL_DIR}"
        CMAKE_ARGS
            # Always Release — Cycles' Debug build is slow and rarely needed here.
            -DCMAKE_BUILD_TYPE=Release
            -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
            # Point Cycles at our app's USD so hdCycles is ABI-compatible.
            -DPXR_ROOT=${OpenUSD_ROOT_DIR}
            # Hydra delegate is the only deliverable we need.
            -DWITH_CYCLES_HYDRA_RENDER_DELEGATE=ON
            -DWITH_CYCLES_USD=ON
            # Use bundled precompiled libs from lib/windows_x64/.
            -DWITH_LIBS_PRECOMPILED=ON
            # Turn off heavy optional features that aren't needed for the delegate.
            -DWITH_CYCLES_STANDALONE_GUI=OFF
            -DWITH_CYCLES_DEVICE_CUDA=OFF
            -DWITH_CYCLES_DEVICE_OPTIX=OFF
            -DWITH_CYCLES_DEVICE_HIP=OFF
            -DWITH_CYCLES_DEVICE_ONEAPI=OFF
            -DWITH_CYCLES_LOGGING=OFF
            # USD ships OpenVDB v9; Cycles prebuilt is v13 — ABI mismatch, both named
            # openvdb.dll.  Disable openvdb in Cycles to avoid the version conflict.
            -DWITH_CYCLES_OPENVDB=OFF
            # Embree4 links tbb12.dll; USD already loads tbb.dll at process startup.
            # oneTBB detects the older tbb.dll and aborts initialization (DLL_INIT_FAILED).
            # Disable Embree so Cycles uses its own BVH traversal instead.
            -DWITH_CYCLES_EMBREE=OFF
            # ---- pxrConfig.cmake dependency overrides ----
            # pxrConfig.cmake runs find_dependency() for these before Cycles'
            # precompiled lib paths are on CMAKE_PREFIX_PATH, so we must supply
            # them explicitly.  The Python3 vars also override the hardcoded
            # CI build-machine path embedded in pxrConfig.cmake.
            -DPython3_EXECUTABLE=$ENV{LOCALAPPDATA}/Programs/Python/Python312/python.exe
            -DPython3_LIBRARY=$ENV{LOCALAPPDATA}/Programs/Python/Python312/libs/python312.lib
            -DPython3_INCLUDE_DIR=$ENV{LOCALAPPDATA}/Programs/Python/Python312/include
            -DTBB_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/tbb/lib/cmake/TBB
            -DMaterialX_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/MaterialX
            # OpenSubdiv and Imath cmake configs are bundled in the USD install.
            # We must supply their dirs so pxrTargets.cmake can resolve the imported targets.
            -DOpenSubdiv_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/OpenSubdiv
            -DImath_DIR=${OpenUSD_ROOT_DIR}/lib/cmake/Imath
            # Force Cycles to use its own prebuilt OCIO 2.5, not the OCIO 2.1
            # bundled in the new USD include dir. Without this, the new USD's
            # include/OpenColorIO/ (OCIO 2.1) shadows Cycles' prebuilt OCIO 2.5
            # headers and the colorspace.cpp API calls fail to compile.
            -DOpenColorIO_DIR=${CMAKE_SOURCE_DIR}/third_party/cycles/lib/windows_x64/opencolorio/lib/cmake/OpenColorIO
        BUILD_COMMAND
            ${CMAKE_COMMAND} --build <BINARY_DIR> --config Release
        INSTALL_COMMAND
            ${CMAKE_COMMAND} --install <BINARY_DIR> --config Release
        BUILD_BYPRODUCTS
            "${HDCYCLES_DLL}"
    )

    set(OPENUSD_HAS_CYCLES TRUE)
else()
    set(OPENUSD_HAS_CYCLES FALSE)
    message(STATUS "hdCycles: disabled — set WITH_CYCLES=ON to build from third_party/cycles")
endif()

# Collect source files
file(GLOB_RECURSE CORE_SOURCES "src/core/*.cpp")
file(GLOB_RECURSE UI_SOURCES "src/ui/*.cpp")
# src/ui/NodeEditor is the in-tree vendored imgui-node-editor subset — already
# compiled into the imgui_node_editor_impl target by FindImguiNodeEditor.cmake
# (with its own required include dirs/defines); excluded here so it isn't
# compiled a second time, incorrectly, as part of this target's own sources.
list(FILTER UI_SOURCES EXCLUDE REGEX "/src/ui/NodeEditor/")
file(GLOB_RECURSE UTILS_SOURCES "src/utils/*.cpp")
set(MAIN_SOURCE "src/main.cpp")

# Create executable
add_executable(UsdLayerManager
    ${MAIN_SOURCE}
    ${CORE_SOURCES}
    ${UI_SOURCES}
    ${UTILS_SOURCES}
)

# Include directories
target_include_directories(UsdLayerManager PRIVATE
    ${CMAKE_SOURCE_DIR}/src
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_SOURCE_DIR}/third_party/fontawesome
    ${CMAKE_SOURCE_DIR}/third_party/nanosvg
)

# Link libraries
target_link_libraries(UsdLayerManager PRIVATE
    OpenUSD::OpenUSD
    Imgui::Imgui
    ImguiNodeEditor::ImguiNodeEditor
    Glad::Glad
    FFmpeg::FFmpeg
    OpenColorIO::OpenColorIO
    ole32
    shell32
)

target_include_directories(UsdLayerManager PRIVATE
    "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
)

# Cycles is an ExternalProject; UsdLayerManager must wait for it to finish
# before POST_BUILD copies hdCycles.dll to the output directory.
if(WITH_CYCLES)
    add_dependencies(UsdLayerManager CyclesBuild)
endif()

# Platform-specific settings
if(WIN32)
    # Windows-specific settings
    target_compile_definitions(UsdLayerManager PRIVATE
        WIN32_LEAN_AND_MEAN
        NOMINMAX
        _CRT_SECURE_NO_WARNINGS
    )
    
    # OpenColorIO DLL — copy from USD bin/ (which already ships OpenColorIO_2_1.dll)
    add_custom_command(TARGET UsdLayerManager POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
            "$<TARGET_FILE_DIR:UsdLayerManager>"
        COMMENT "Copying OpenColorIO runtime DLL..."
    )

    # FFmpeg DLLs — copy all .dll files from third_party/ffmpeg/bin
    file(GLOB FFMPEG_RUNTIME_DLLS "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/bin/*.dll")
    if(FFMPEG_RUNTIME_DLLS)
        add_custom_command(TARGET UsdLayerManager POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                ${FFMPEG_RUNTIME_DLLS}
                "$<TARGET_FILE_DIR:UsdLayerManager>"
            COMMENT "Copying FFmpeg runtime DLLs..."
        )
    endif()

    # cmake -P script to copy USD bin/ DLLs excluding CRT DLLs (msvcp, vcruntime, etc.).
    # The USD distribution bundles older CRT DLLs (14.34) that conflict with Cycles
    # binaries built against the newer system CRT (14.42+). The system CRT is
    # always present on Windows 10+ — no need to bundle our own copy.
    file(WRITE "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
        "file(GLOB _all_dlls \"${OpenUSD_ROOT_DIR}/bin/*.dll\")\n"
        "set(_dlls)\n"
        "foreach(_dll \${_all_dlls})\n"
        "    get_filename_component(_name \"\${_dll}\" NAME)\n"
        "    string(TOLOWER \"\${_name}\" _lname)\n"
        "    if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
        "        continue()\n"
        "    endif()\n"
        "    list(APPEND _dlls \"\${_dll}\")\n"
        "endforeach()\n"
        "if(_dlls)\n"
        "    file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
        "endif()\n"
    )

    # Copy runtime DLLs to output directory for each configuration
    add_custom_command(TARGET UsdLayerManager POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>"
        # OpenUSD lib/ — usd_*.dll, boost_*.dll, Alembic.dll, etc.
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_BIN_DIR}"
            "$<TARGET_FILE_DIR:UsdLayerManager>"
        # OpenUSD bin/ — tbb.dll, MaterialX, OpenEXR, OpenImageIO, etc. (CRT DLLs excluded)
        COMMAND ${CMAKE_COMMAND}
            "-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
            -P "${CMAKE_BINARY_DIR}/copy_usd_bin_dlls.cmake"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/lib/usd"
            "$<TARGET_FILE_DIR:UsdLayerManager>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/plugin/usd"
            "$<TARGET_FILE_DIR:UsdLayerManager>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
            "$<TARGET_FILE_DIR:UsdLayerManager>"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
            "$<TARGET_FILE_DIR:UsdLayerManager>"
        COMMENT "Copying runtime DLLs and USD plugins..."
    )

    if(OPENUSD_HAS_EMBREE)
        add_custom_command(TARGET UsdLayerManager POST_BUILD
            # plugInfo.json has LibraryPath "../hdEmbree.dll" relative to usd/hdEmbree/,
            # which resolves to usd/hdEmbree.dll — so the DLL must live in usd/, not exe root.
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${HDEMBREE_DLL}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree.dll"
            COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${HDEMBREE_PLUGIN_DIR}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdEmbree"
            COMMENT "Copying hdEmbree plugin..."
        )
        if(EMBREE_DLLS)
            add_custom_command(TARGET UsdLayerManager POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    ${EMBREE_DLLS}
                    "$<TARGET_FILE_DIR:UsdLayerManager>"
                COMMENT "Copying Embree runtime DLLs..."
            )
        endif()
    endif()

    if(OPENUSD_HAS_ARNOLD)
        add_custom_command(TARGET UsdLayerManager POST_BUILD
            # DLLs go in usd/ alongside their metadata dirs (LibraryPath "../hdArnold.dll")
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${HDARNOLD_DLL}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold.dll"
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${NDRARNOLD_DLL}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold.dll"
            COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${HDARNOLD_PLUGIN_DIR}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdArnold"
            COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${NDRARNOLD_PLUGIN_DIR}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/ndrArnold"
            COMMENT "Copying hdArnold plugin..."
        )
        if(ARNOLD_RUNTIME_DLLS)
            add_custom_command(TARGET UsdLayerManager POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy_if_different
                    ${ARNOLD_RUNTIME_DLLS}
                    "$<TARGET_FILE_DIR:UsdLayerManager>"
                COMMENT "Copying Arnold runtime DLLs (ai, OIDN, licensing)..."
            )
        endif()
    endif()

    if(OPENUSD_HAS_CYCLES)
        # hdCycles uses the same usd/ layout as hdEmbree:
        #   usd/hdCycles.dll  +  usd/hdCycles/resources/plugInfo.json
        # The bundled plugInfo.json has Root=".." and LibraryPath="../hdCycles.dll"
        # which resolves correctly once the plugin dir lives in usd/.
        #
        # Runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, vulkan, etc.)
        # live in CYCLES_INSTALL_DIR root and must be next to the exe.  We can't
        # file(GLOB) them at configure time (ExternalProject hasn't run yet), so we
        # write a small cmake -P script that globs them at build time instead.
        # The script runs at build time (after ExternalProject finishes) so it can glob
        # the actual install output.  CRT/Windows DLLs are excluded — they must come
        # from the system or the main exe's toolchain, not from Cycles' bundled copy.
        file(WRITE "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
            "file(GLOB _all_dlls \"${CYCLES_INSTALL_DIR}/*.dll\")\n"
            "set(_dlls)\n"
            "foreach(_dll \${_all_dlls})\n"
            "    get_filename_component(_name \"\${_dll}\" NAME)\n"
            "    string(TOLOWER \"\${_name}\" _lname)\n"
            # Exclude CRT DLLs (system must supply these; Cycles bundles an older version).
            "    if(_lname MATCHES \"^(msvcp|vcruntime|ucrtbase|concrt|api-ms-win-)\")\n"
            "        continue()\n"
            "    endif()\n"
            # Exclude debug-variant DLLs (not needed in release, just wasted space).
            # Pattern: OpenColorIO_d_2_5.dll  →  contains "_d_"
            #          openjph.0.25d.dll      →  ends with "d.dll"
            "    if(_lname MATCHES \"_d_\" OR _lname MATCHES \"d[.]dll$\")\n"
            "        continue()\n"
            "    endif()\n"
            "    list(APPEND _dlls \"\${_dll}\")\n"
            "endforeach()\n"
            "if(_dlls)\n"
            "    file(COPY \${_dlls} DESTINATION \"\${DEST_DIR}\")\n"
            "endif()\n"
        )
        add_custom_command(TARGET UsdLayerManager POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${HDCYCLES_DLL}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles.dll"
            COMMAND ${CMAKE_COMMAND} -E copy_directory
                "${HDCYCLES_PLUGIN_DIR}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/usd/hdCycles"
            COMMAND ${CMAKE_COMMAND}
                "-DDEST_DIR=$<TARGET_FILE_DIR:UsdLayerManager>"
                -P "${CMAKE_BINARY_DIR}/copy_cycles_runtime_dlls.cmake"
            # tbb12.dll is required by openvdb/embree4 but is not in the install output
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
                "${CYCLES_TBB12_DLL}"
                "$<TARGET_FILE_DIR:UsdLayerManager>/tbb12.dll"
            COMMENT "Copying hdCycles plugin and runtime DLLs..."
        )
    endif()
endif()

# Compiler warnings
if(MSVC)
    target_compile_options(UsdLayerManager PRIVATE /W4)
else()
    target_compile_options(UsdLayerManager PRIVATE -Wall -Wextra -Wpedantic)
endif()

# Install rules
install(TARGETS UsdLayerManager
    RUNTIME DESTINATION bin
)

# Install all OpenUSD runtime DLLs (same set that POST_BUILD copies to build/Release).
# Without these the exe cannot start from install/bin.
install(DIRECTORY ${OpenUSD_BIN_DIR}/
    DESTINATION bin
    FILES_MATCHING PATTERN "*.dll"
)

# Install OpenUSD bin/ DLLs — tbb.dll, tbbmalloc.dll, MaterialX, OpenEXR,
# OpenImageIO, zlib, etc.  Exclude CRT DLLs: the system VC++ Redistributable supplies
# these at runtime; bundling an older copy (14.34) causes crashes on machines with
# Cycles or other DLLs compiled against the newer CRT (14.42+).
install(DIRECTORY ${OpenUSD_ROOT_DIR}/bin/
    DESTINATION bin
    FILES_MATCHING PATTERN "*.dll"
    PATTERN "msvcp*" EXCLUDE
    PATTERN "vcruntime*" EXCLUDE
    PATTERN "ucrtbase*" EXCLUDE
    PATTERN "concrt*" EXCLUDE
    PATTERN "api-ms-win-*" EXCLUDE
)

install(DIRECTORY ${OpenUSD_ROOT_DIR}/lib/usd
    DESTINATION bin
)

install(DIRECTORY ${OpenUSD_ROOT_DIR}/plugin/usd/
    DESTINATION bin/usd
)

if(OPENUSD_HAS_EMBREE)
    # Install to bin/usd/ so plugInfo.json's LibraryPath "../hdEmbree.dll" resolves correctly.
    install(FILES "${HDEMBREE_DLL}"
        DESTINATION bin/usd
        OPTIONAL
    )
    install(DIRECTORY "${HDEMBREE_PLUGIN_DIR}"
        DESTINATION bin/usd
        OPTIONAL
    )
    if(EMBREE_DLLS)
        install(FILES ${EMBREE_DLLS}
            DESTINATION bin
            OPTIONAL
        )
    endif()
endif()

if(OPENUSD_HAS_ARNOLD)
    install(FILES "${HDARNOLD_DLL}" "${NDRARNOLD_DLL}"
        DESTINATION bin/usd
        OPTIONAL
    )
    install(DIRECTORY "${HDARNOLD_PLUGIN_DIR}"
        DESTINATION bin/usd
        OPTIONAL
    )
    install(DIRECTORY "${NDRARNOLD_PLUGIN_DIR}"
        DESTINATION bin/usd
        OPTIONAL
    )
    if(ARNOLD_RUNTIME_DLLS)
        install(FILES ${ARNOLD_RUNTIME_DLLS}
            DESTINATION bin
            OPTIONAL
        )
    endif()
endif()

if(OPENUSD_HAS_CYCLES)
    install(FILES "${HDCYCLES_DLL}"
        DESTINATION bin/usd
        OPTIONAL
    )
    install(DIRECTORY "${HDCYCLES_PLUGIN_DIR}"
        DESTINATION bin/usd
        OPTIONAL
    )
    # Cycles runtime deps (embree4, epoxy, imath, openvdb, OIDN, sycl, etc.).
    # No OPTIONAL: install(DIRECTORY) disallows it with FILES_MATCHING, and the
    # dir always exists here since it is produced by the CyclesBuild step.
    #
    # EXCLUDE the MSVC CRT and api-ms-win DLLs that Cycles bundles: they are
    # older than what the system ships (v14.34 vs system v14.42+), and placing
    # an older MSVCP140.dll/VCRUNTIME140.dll ahead of System32 on the DLL
    # search path causes sycl8.dll (built with newer CRT) to fail DllMain with
    # ERROR_DLL_INIT_FAILED (1114) when hdCycles.dll is loaded.
    # The system VC++ Redistributable always satisfies these deps at runtime.
    install(DIRECTORY "${CYCLES_INSTALL_DIR}/"
        DESTINATION bin
        FILES_MATCHING
            PATTERN "*.dll"
            PATTERN "MSVCP140*.dll"       EXCLUDE
            PATTERN "VCRUNTIME140*.dll"   EXCLUDE
            PATTERN "ucrtbase*.dll"       EXCLUDE
            PATTERN "api-ms-win-*.dll"    EXCLUDE
            PATTERN "OpenColorIO_d_*.dll" EXCLUDE   # debug OCIO build
            PATTERN "openjph.*d.dll"      EXCLUDE   # debug OpenJPH
    )
    # tbb12.dll (used by openvdb/embree4) lives in the precompiled lib tree, not install output
    install(FILES "${CYCLES_TBB12_DLL}"
        DESTINATION bin
        OPTIONAL
    )
endif()

# Install Python runtime DLLs required by OpenUSD and Cycles.
# python312.dll  : linked by Cycles (hdCycles.dll → python312.dll)
# python311.dll  : linked by usd_python.dll (the OpenUSD Python bindings were
#                  built against Python 3.11, regardless of what we compile with)
# Detect python311.dll from common install paths.
foreach(_pyroot
    "$ENV{LOCALAPPDATA}/Programs/Python/Python311"
    "C:/Python311"
    "C:/Program Files/Python311"
)
    if(EXISTS "${_pyroot}/python311.dll")
        set(PYTHON311_DLL "${_pyroot}/python311.dll")
        message(STATUS "Found python311.dll (required by usd_python.dll): ${PYTHON311_DLL}")
        break()
    endif()
endforeach()

set(_python_runtime_dlls
    "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
    "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
)
if(PYTHON311_DLL)
    list(APPEND _python_runtime_dlls "${PYTHON311_DLL}")
endif()

install(FILES ${_python_runtime_dlls}
    DESTINATION bin
    OPTIONAL   # OPTIONAL so the install doesn't fail on machines with a
               # system-wide Python; in that case the DLL is on PATH already.
)

# Also copy python311.dll to the build output directory so debug runs work
# without requiring C:\Python311 to be on PATH.
if(PYTHON311_DLL)
    add_custom_command(TARGET UsdLayerManager POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${PYTHON311_DLL}"
            "$<TARGET_FILE_DIR:UsdLayerManager>/python311.dll"
        COMMENT "Copying python311.dll (for usd_python.dll)..."
    )
endif()

# ---------------------------------------------------------------------------
# OCIO: download ACES 1.2 config at configure time if not present
# ---------------------------------------------------------------------------
set(_ocio_config_dir "${CMAKE_SOURCE_DIR}/resources/OpenColorIO-Configs/aces_1.2")
if(NOT EXISTS "${_ocio_config_dir}/config.ocio")
    message(STATUS "Downloading ACES 1.2 OCIO config (~124 MB) ...")
    set(_ocio_zip "${CMAKE_BINARY_DIR}/aces_1.2.zip")
    file(DOWNLOAD
        "https://github.com/colour-science/OpenColorIO-Configs/releases/download/v1.2/OpenColorIO-Config-ACES-1.2.zip"
        "${_ocio_zip}"
        SHOW_PROGRESS
        STATUS _ocio_dl_status
    )
    list(GET _ocio_dl_status 0 _ocio_dl_code)
    if(_ocio_dl_code EQUAL 0)
        execute_process(
            COMMAND ${CMAKE_COMMAND} -E tar xf "${_ocio_zip}"
            WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
        )
        file(COPY "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2/aces_1.2/"
            DESTINATION "${_ocio_config_dir}")
        file(REMOVE_RECURSE "${CMAKE_BINARY_DIR}/OpenColorIO-Config-ACES-1.2")
        file(REMOVE "${_ocio_zip}")
        message(STATUS "ACES 1.2 OCIO config installed to ${_ocio_config_dir}")
    else()
        message(WARNING "ACES 1.2 OCIO download failed (${_ocio_dl_status}) — OCIO will not be bundled.")
    endif()
endif()

# ---------------------------------------------------------------------------
# HDRI: shader-ball lighting presets (Poly Haven, CC0) at configure time
# ---------------------------------------------------------------------------
set(_hdri_dir "${CMAKE_SOURCE_DIR}/resources/hdri")
set(_hdri_files
    kloofendal_48d_partly_cloudy_puresky_1k.exr
    lebombo_1k.exr
    artist_workshop_1k.exr
    venice_sunset_1k.exr
)
file(MAKE_DIRECTORY "${_hdri_dir}")
foreach(_hdri ${_hdri_files})
    if(NOT EXISTS "${_hdri_dir}/${_hdri}")
        message(STATUS "Downloading HDRI ${_hdri} ...")
        file(DOWNLOAD
            "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/${_hdri}"
            "${_hdri_dir}/${_hdri}"
            STATUS _hdri_dl_status
        )
        list(GET _hdri_dl_status 0 _hdri_dl_code)
        if(NOT _hdri_dl_code EQUAL 0)
            file(REMOVE "${_hdri_dir}/${_hdri}")
            message(WARNING "HDRI download failed for ${_hdri} (${_hdri_dl_status}) — the shader-ball lighting preset will fall back to a distant light.")
        endif()
    endif()
endforeach()

# ---------------------------------------------------------------------------
# VDB: cloud volume for the shader-ball preview (OpenVDB sample model) at
# configure time if not present
# ---------------------------------------------------------------------------
set(_vdb_dir "${CMAKE_SOURCE_DIR}/resources/vdb")
file(MAKE_DIRECTORY "${_vdb_dir}")
if(NOT EXISTS "${_vdb_dir}/bunny_cloud.vdb")
    message(STATUS "Downloading OpenVDB sample bunny_cloud.vdb ...")
    set(_vdb_zip "${CMAKE_BINARY_DIR}/bunny_cloud.zip")
    file(DOWNLOAD
        "https://artifacts.aswf.io/io/aswf/openvdb/models/bunny_cloud.vdb/1.0.0/bunny_cloud.vdb-1.0.0.zip"
        "${_vdb_zip}"
        STATUS _vdb_dl_status
    )
    list(GET _vdb_dl_status 0 _vdb_dl_code)
    if(_vdb_dl_code EQUAL 0)
        execute_process(
            COMMAND ${CMAKE_COMMAND} -E tar xf "${_vdb_zip}"
            WORKING_DIRECTORY "${_vdb_dir}"
        )
        file(REMOVE "${_vdb_zip}")
        message(STATUS "bunny_cloud.vdb installed to ${_vdb_dir}")
    else()
        message(WARNING "OpenVDB sample download failed (${_vdb_dl_status}) — the Cloud Volume preview shape will be unavailable.")
    endif()
endif()

# Copy resources to build directory
file(COPY ${CMAKE_SOURCE_DIR}/resources
    DESTINATION ${CMAKE_BINARY_DIR}
)

# Copy font and SVG icons to build directory
add_custom_command(TARGET UsdLayerManager POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>/../resources/font"
    COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
    COMMAND ${CMAKE_COMMAND} -E copy
        "${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc"
        "$<TARGET_FILE_DIR:UsdLayerManager>/resources/font/Inter.ttc"
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/resources/icons"
        "$<TARGET_FILE_DIR:UsdLayerManager>/resources/icons"
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/resources/hdri"
        "$<TARGET_FILE_DIR:UsdLayerManager>/resources/hdri"
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/resources/preview"
        "$<TARGET_FILE_DIR:UsdLayerManager>/resources/preview"
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_SOURCE_DIR}/resources/vdb"
        "$<TARGET_FILE_DIR:UsdLayerManager>/resources/vdb"
    COMMENT "Copying fonts, SVG icons, HDRI presets and preview shapes to build output"
)

# Copy ACES OCIO config to build output — only on first build (skipped if already present).
if(EXISTS "${_ocio_config_dir}/config.ocio")
    add_custom_command(TARGET UsdLayerManager POST_BUILD
        COMMAND ${CMAKE_COMMAND}
            "-DSRC=${_ocio_config_dir}"
            "-DDST=$<TARGET_FILE_DIR:UsdLayerManager>/resources/OpenColorIO-Configs/aces_1.2"
            -P "${CMAKE_SOURCE_DIR}/cmake/CopyDirIfMissing.cmake"
        COMMENT "Copying ACES OCIO config to build output (first time only)"
    )
endif()

# Install OpenColorIO DLL (from the OpenUSD distribution's bin/)
install(FILES
    "${OpenUSD_ROOT_DIR}/bin/OpenColorIO_2_1.dll"
    DESTINATION bin
    OPTIONAL
)

# Install resources (icons, fonts placeholder) next to the exe so the
# exe-relative path "resources/..." resolves correctly from install/bin/.
install(DIRECTORY ${CMAKE_SOURCE_DIR}/resources
    DESTINATION bin
)

# Install Inter font next to the exe under resources/font/
# ImGuiContext tries "resources/font/Inter.ttf" then "resources/font/Inter.ttc".
install(FILES ${CMAKE_SOURCE_DIR}/third_party/Inter/Inter.ttc
    DESTINATION bin/resources/font
)

# Print configuration summary
message(STATUS "")
message(STATUS "=== USD Layer Manager Configuration ===")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")
message(STATUS "Install prefix: ${CMAKE_INSTALL_PREFIX}")
message(STATUS "OpenUSD found: ${OpenUSD_FOUND}")
message(STATUS "ImGui found: ${Imgui_FOUND}")
message(STATUS "OpenGL found: ${OPENGL_FOUND}")
message(STATUS "Glad found: ${Glad_FOUND}")
message(STATUS "Build tests: ${BUILD_TESTS}")
message(STATUS "hdEmbree support: ${OPENUSD_HAS_EMBREE}")
if(OPENUSD_HAS_EMBREE AND EMBREE_DLLS)
    message(STATUS "Embree DLLs: ${EMBREE_DLLS}")
elseif(OPENUSD_HAS_EMBREE)
    message(WARNING "hdEmbree found but no Embree DLLs detected — set EMBREE_LOCATION to the Embree SDK bin/ dir or check HDEMBREE_USD_ROOT/bin")
endif()
message(STATUS "hdArnold support: ${OPENUSD_HAS_ARNOLD}")
if(OPENUSD_HAS_ARNOLD)
    message(STATUS "hdArnold DLL: ${HDARNOLD_DLL}")
    message(STATUS "Arnold runtime DLLs: ${ARNOLD_RUNTIME_DLLS}")
    if(NOT ARNOLD_AI_DLL)
        message(WARNING "hdArnold found but ai.dll not located — set ARNOLD_LOCATION to the MtoA root (e.g. C:/Autodesk/mtoa/5.5.0/2026)")
    endif()
endif()
message(STATUS "hdCycles support: ${OPENUSD_HAS_CYCLES}")
if(OPENUSD_HAS_CYCLES)
    message(STATUS "Cycles source: ${CMAKE_SOURCE_DIR}/third_party/cycles")
    message(STATUS "Cycles build: ${CYCLES_BINARY_DIR}")
    message(STATUS "Cycles install: ${CYCLES_INSTALL_DIR}")
endif()
message(STATUS "=======================================")
message(STATUS "")

# === Test build option ===
option(BUILD_TESTS "Build test executables" OFF)

if(BUILD_TESTS)

# === Viewport Display Test ===
add_executable(ViewportDisplayTest
    tests/viewport_display_test.cpp
    ${CORE_SOURCES}
    ${UTILS_SOURCES}
)

target_include_directories(ViewportDisplayTest PRIVATE
    ${CMAKE_SOURCE_DIR}/src
    ${CMAKE_SOURCE_DIR}/include
    "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
)

target_link_libraries(ViewportDisplayTest PRIVATE
    OpenUSD::OpenUSD
    Glad::Glad
    FFmpeg::FFmpeg
)

if(WIN32)
    target_compile_definitions(ViewportDisplayTest PRIVATE
        WIN32_LEAN_AND_MEAN
        NOMINMAX
        _CRT_SECURE_NO_WARNINGS
    )

    add_custom_command(TARGET ViewportDisplayTest POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_BIN_DIR}"
            "$<TARGET_FILE_DIR:ViewportDisplayTest>"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/lib/usd"
            "$<TARGET_FILE_DIR:ViewportDisplayTest>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/plugin/usd"
            "$<TARGET_FILE_DIR:ViewportDisplayTest>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
            "$<TARGET_FILE_DIR:ViewportDisplayTest>"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
            "$<TARGET_FILE_DIR:ViewportDisplayTest>"
        COMMENT "Copying runtime DLLs and USD plugins for test..."
    )
endif()

enable_testing()
add_test(NAME ViewportDisplayTest COMMAND ViewportDisplayTest)

add_executable(RendererDiagnosticTest
    tests/renderer_diagnostic_test.cpp
    ${CORE_SOURCES}
    ${UTILS_SOURCES}
)

target_include_directories(RendererDiagnosticTest PRIVATE
    ${CMAKE_SOURCE_DIR}/src
    ${CMAKE_SOURCE_DIR}/include
    "${CMAKE_SOURCE_DIR}/third_party/ffmpeg/include"
)

target_link_libraries(RendererDiagnosticTest PRIVATE
    OpenUSD::OpenUSD
    Glad::Glad
    FFmpeg::FFmpeg
)

if(WIN32)
    target_compile_definitions(RendererDiagnosticTest PRIVATE
        WIN32_LEAN_AND_MEAN
        NOMINMAX
        _CRT_SECURE_NO_WARNINGS
    )

    add_custom_command(TARGET RendererDiagnosticTest POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_BIN_DIR}"
            "$<TARGET_FILE_DIR:RendererDiagnosticTest>"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/lib/usd"
            "$<TARGET_FILE_DIR:RendererDiagnosticTest>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${OpenUSD_ROOT_DIR}/plugin/usd"
            "$<TARGET_FILE_DIR:RendererDiagnosticTest>/usd"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python312.dll"
            "$<TARGET_FILE_DIR:RendererDiagnosticTest>"
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "$ENV{LOCALAPPDATA}/Programs/Python/Python312/python3.dll"
            "$<TARGET_FILE_DIR:RendererDiagnosticTest>"
        COMMENT "Copying runtime DLLs and USD plugins for diagnostic test..."
    )
endif()

endif() # BUILD_TESTS
