cmake_minimum_required(VERSION 3.20)
project(mujofil VERSION 0.1.0 LANGUAGES CXX C)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")

# ============================================================================
# Filament (static libs -> baked into the module, nothing to ship separately)
# ============================================================================
# mujofil renders HEADLESS via surfaceless EGL (no X server), which needs a
# Filament built with FILAMENT_SUPPORTS_EGL_ON_LINUX. Google's prebuilt Linux
# Filament is GLX-only and calls exit() when it can't open an X display, so we
# use a CUSTOM EGL-enabled build instead. Resolution order:
#   1. FILAMENT_DIR (env or -D) -> use as-is (fastest; CI sets this).
#   2. Linux: download our prebuilt EGL Filament artifact (hosted as a release
#      asset), else build it from source via packaging/build_filament_egl.sh.
#   3. mac/windows: Google's prebuilt (their backends don't need EGL/X).
# A prebuilt wheel never hits this path (FILAMENT_DIR is always set in CI).
set(FILAMENT_VERSION "1.56.3")
# Prebuilt EGL Filament artifact (overridable). Empty disables the download step.
set(MUJOFIL_FILAMENT_URL
    "https://github.com/tau-intelligence/MuJoCo-Filament/releases/download/filament-egl-v${FILAMENT_VERSION}/filament-egl-v${FILAMENT_VERSION}-linux-x86_64.tgz"
    CACHE STRING "URL of the prebuilt EGL-enabled Filament artifact (Linux)")
if(NOT FILAMENT_DIR)
    set(FILAMENT_DIR "$ENV{FILAMENT_DIR}")
endif()
if(NOT FILAMENT_DIR AND CMAKE_SYSTEM_NAME STREQUAL "Linux")
    set(FILAMENT_DIR "${CMAKE_BINARY_DIR}/_filament_egl")
    if(NOT EXISTS "${FILAMENT_DIR}/include")
        set(_got_filament FALSE)
        if(MUJOFIL_FILAMENT_URL)
            message(STATUS "Downloading prebuilt EGL Filament: ${MUJOFIL_FILAMENT_URL}")
            file(DOWNLOAD "${MUJOFIL_FILAMENT_URL}" "${CMAKE_BINARY_DIR}/filament-egl.tgz"
                 SHOW_PROGRESS STATUS _dl)
            list(GET _dl 0 _dl_status)
            if(_dl_status EQUAL 0)
                file(MAKE_DIRECTORY "${FILAMENT_DIR}")
                execute_process(
                    COMMAND ${CMAKE_COMMAND} -E tar xzf "${CMAKE_BINARY_DIR}/filament-egl.tgz"
                    WORKING_DIRECTORY "${FILAMENT_DIR}")
                # Tarball extracts to a 'filament/' subdir — flatten it.
                if(EXISTS "${FILAMENT_DIR}/filament/include")
                    file(GLOB _fc "${FILAMENT_DIR}/filament/*")
                    foreach(_c ${_fc})
                        get_filename_component(_n "${_c}" NAME)
                        file(RENAME "${_c}" "${FILAMENT_DIR}/${_n}")
                    endforeach()
                endif()
                if(EXISTS "${FILAMENT_DIR}/include")
                    set(_got_filament TRUE)
                endif()
            else()
                message(STATUS "Prebuilt EGL Filament not available (${_dl}); will build from source.")
            endif()
        endif()
        if(NOT _got_filament)
            message(STATUS "Building custom EGL Filament v${FILAMENT_VERSION} from source (slow ~20-30 min)...")
            execute_process(
                COMMAND bash "${CMAKE_SOURCE_DIR}/packaging/build_filament_egl.sh" "${FILAMENT_DIR}"
                RESULT_VARIABLE _fil_rc)
            if(NOT _fil_rc EQUAL 0)
                message(FATAL_ERROR "EGL Filament build failed (rc=${_fil_rc}). "
                    "See packaging/build_filament_egl.sh; it needs git, clang, libc++ dev, cmake, ninja.")
            endif()
        endif()
    endif()
elseif(NOT FILAMENT_DIR)
    # mac / windows: Google's prebuilt (Metal/D3D backends, no EGL/X dependency).
    if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
        set(_fil_os "mac")
    elseif(WIN32)
        set(_fil_os "windows")
    else()
        message(FATAL_ERROR "Unsupported platform for Filament auto-download.")
    endif()
    set(FILAMENT_DIR "${CMAKE_BINARY_DIR}/_filament")
    if(NOT EXISTS "${FILAMENT_DIR}/include")
        set(_fil_url "https://github.com/google/filament/releases/download/v${FILAMENT_VERSION}/filament-v${FILAMENT_VERSION}-${_fil_os}.tgz")
        message(STATUS "FILAMENT_DIR not set — downloading Filament v${FILAMENT_VERSION} (${_fil_os})...")
        file(MAKE_DIRECTORY "${FILAMENT_DIR}")
        file(DOWNLOAD "${_fil_url}" "${CMAKE_BINARY_DIR}/filament.tgz" SHOW_PROGRESS STATUS _dl)
        list(GET _dl 0 _dl_rc)
        if(NOT _dl_rc EQUAL 0)
            message(FATAL_ERROR "Failed to download Filament from ${_fil_url} (${_dl}).")
        endif()
        execute_process(
            COMMAND ${CMAKE_COMMAND} -E tar xzf "${CMAKE_BINARY_DIR}/filament.tgz"
            WORKING_DIRECTORY "${FILAMENT_DIR}")
        # The tarball extracts into a 'filament/' subdir — flatten it.
        if(EXISTS "${FILAMENT_DIR}/filament/include")
            file(GLOB _fil_children "${FILAMENT_DIR}/filament/*")
            foreach(_c ${_fil_children})
                get_filename_component(_n "${_c}" NAME)
                file(RENAME "${_c}" "${FILAMENT_DIR}/${_n}")
            endforeach()
        endif()
    endif()
endif()
if(NOT EXISTS "${FILAMENT_DIR}/include")
    message(FATAL_ERROR "FILAMENT_DIR='${FILAMENT_DIR}' has no include/ — bad Filament path.")
endif()
set(FILAMENT_INCLUDE_DIR "${FILAMENT_DIR}/include")
set(FILAMENT_LIB_DIR "${FILAMENT_DIR}/lib/x86_64")

set(FILAMENT_LIBS
    filament backend bluegl bluevk filabridge filaflat utils geometry smol-v
    ibl image camutils filameshio gltfio gltfio_core uberarchive meshoptimizer
    ktxreader basis_transcoder dracodec png z stb uberzlib zstd)

# ============================================================================
# MuJoCo — HEADERS ONLY, and VENDORED into the repo (third_party/mujoco/include).
# The bindings read mjModel/mjData via raw pointers and call no MuJoCo functions,
# so we never link or ship libmujoco. Vendoring the headers (MuJoCo is Apache-2.0)
# means the build needs NO mujoco install at all — identical across every Python
# version and CI container. `mujoco` remains a pure RUNTIME pip dependency that
# provides the actual library when the user runs.
# ============================================================================
if(NOT MUJOCO_INCLUDE_DIR)
    set(MUJOCO_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/mujoco/include")
endif()
if(NOT EXISTS "${MUJOCO_INCLUDE_DIR}/mujoco/mujoco.h")
    message(FATAL_ERROR "Vendored MuJoCo headers not found at ${MUJOCO_INCLUDE_DIR}.")
endif()
message(STATUS "MuJoCo headers (vendored): ${MUJOCO_INCLUDE_DIR}")

# scikit-build-core sets Python_EXECUTABLE; fall back to a plain interpreter
# lookup for standalone CMake configures. pybind11's CMake helpers need the
# Python development module so `pybind11_add_module` can call python_add_library.
if(NOT Python_EXECUTABLE)
    find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
else()
    find_package(Python COMPONENTS Development.Module REQUIRED)
endif()

find_package(pybind11 CONFIG REQUIRED)
find_package(Threads REQUIRED)

# ============================================================================
# Core static library (the Filament renderer)
# ============================================================================
add_library(vf_mujoco_core STATIC
    src/core/renderer.cpp
    src/core/scene_bridge.cpp
    src/core/material_manager.cpp
    src/core/light_manager.cpp
    src/core/camera_controller.cpp)

target_include_directories(vf_mujoco_core PUBLIC
    ${CMAKE_SOURCE_DIR}/src ${FILAMENT_INCLUDE_DIR} ${MUJOCO_INCLUDE_DIR})
target_link_directories(vf_mujoco_core PUBLIC ${FILAMENT_LIB_DIR})
# Wrap the Filament static archives in a link group: they have circular
# inter-dependencies (e.g. geometry <-> filament), and a single pass over the
# archives (the default) leaves symbols like TangentSpaceMesh undefined. The
# group makes the linker resolve them iteratively. (The prebuilt Google Filament
# happened to link without this; the from-source EGL build needs it.)
target_link_libraries(vf_mujoco_core PUBLIC -Wl,--start-group ${FILAMENT_LIBS} -Wl,--end-group)
# EGL + GL for the headless OpenGL backend (surfaceless EGL context, no X).
# GL is pulled transitively by Filament; EGL is what makes it headless.
target_link_libraries(vf_mujoco_core PUBLIC Threads::Threads dl EGL GL)

# ============================================================================
# Python module
# ============================================================================
pybind11_add_module(_vf_mujoco_native
    src/bindings/bindings.cpp
    src/bindings/renderer_bindings.cpp
    src/bindings/scene_bindings.cpp)
target_link_libraries(_vf_mujoco_native PRIVATE vf_mujoco_core c++ c++abi)
target_include_directories(_vf_mujoco_native PRIVATE
    ${CMAKE_SOURCE_DIR}/src ${FILAMENT_INCLUDE_DIR} ${MUJOCO_INCLUDE_DIR})

# Look for bundled shared libs (libc++ etc., placed by auditwheel) next to the
# module inside the installed package.
set_target_properties(_vf_mujoco_native PROPERTIES
    INSTALL_RPATH "$ORIGIN"
    BUILD_WITH_INSTALL_RPATH ON)

# Install the native module INTO the python package directory.
install(TARGETS _vf_mujoco_native DESTINATION mujofil)

# ============================================================================
# Filament materials, installed as package data.
# Prefer PREBUILT .filamat files committed under mujofil/materials/prebuilt/
# (compiled on a host with a working matc). Filament's matc binary needs glibc
# >= 2.29 and cannot run inside the manylinux_2_28 (glibc 2.28) CI container, so
# we ship the precompiled packages there. If they're absent (e.g. you edited a
# .mat), fall back to compiling with matc.
# ============================================================================
set(PREBUILT_MAT_DIR "${CMAKE_SOURCE_DIR}/mujofil/materials/prebuilt")
file(GLOB PREBUILT_MATERIALS "${PREBUILT_MAT_DIR}/*.filamat")

if(PREBUILT_MATERIALS)
    message(STATUS "Using prebuilt Filament materials from ${PREBUILT_MAT_DIR}")
    install(FILES ${PREBUILT_MATERIALS} DESTINATION mujofil/materials)
    add_custom_target(materials ALL)
else()
    set(MATC "${FILAMENT_DIR}/bin/matc")
    file(GLOB MATERIAL_SOURCES "${CMAKE_SOURCE_DIR}/mujofil/materials/*.mat")
    foreach(mat_file ${MATERIAL_SOURCES})
        get_filename_component(mat_name ${mat_file} NAME_WE)
        set(mat_output "${CMAKE_BINARY_DIR}/materials/${mat_name}.filamat")
        add_custom_command(
            OUTPUT ${mat_output}
            COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/materials"
            COMMAND ${MATC} -o ${mat_output} -a vulkan -a opengl ${mat_file}
            DEPENDS ${mat_file}
            COMMENT "Compiling material: ${mat_name}")
        list(APPEND COMPILED_MATERIALS ${mat_output})
    endforeach()
    add_custom_target(materials ALL DEPENDS ${COMPILED_MATERIALS})
    install(FILES ${COMPILED_MATERIALS} DESTINATION mujofil/materials)
endif()
add_dependencies(_vf_mujoco_native materials)

# ============================================================================
# Bundled default IBL environment (a neutral studio HDR, prefiltered to KTX).
# Loaded automatically by VFRenderer so a bare MJCF gets photoreal image-based
# lighting + reflections with no setup. Installed as package data so it ships in
# the wheel; __init__.py points VF_MUJOCO_IBL_DIR at this directory.
# ============================================================================
file(GLOB BUNDLED_IBL "${CMAKE_SOURCE_DIR}/mujofil/assets/ibl/*.ktx")
if(BUNDLED_IBL)
    install(FILES ${BUNDLED_IBL} DESTINATION mujofil/assets/ibl)
endif()
