cmake_minimum_required(VERSION 3.21)
cmake_policy(SET CMP0116 NEW)
set(CMAKE_POLICY_WARNING_CMP0116 OFF)

project(
    # gersemi: ignore
    CommunityShaders
    VERSION 1.4.7
    LANGUAGES CXX
)

# default install path
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
    set_property(
        CACHE CMAKE_INSTALL_PREFIX
        PROPERTY VALUE "${CMAKE_CURRENT_BINARY_DIR}/aio"
    )
endif()

list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")

# ########################################################################################################################
# ## Build options
# ########################################################################################################################
message("Options:")
option(
    AUTO_PLUGIN_DEPLOYMENT
    "Copy the build output and addons to env:CommunityShadersOutputDir."
    OFF
)
option(
    ZIP_TO_DIST
    "Zip the base mod and addons to their own 7z file in dist."
    ON
)
option(
    AIO_ZIP_TO_DIST
    "Zip the base mod and addons to a AIO 7z file in dist."
    ON
)
option(TRACY_SUPPORT "Enable support for tracy profiler" OFF)
option(
    BUILD_SHADER_TESTS
    "Build shader unit tests (runs automatically before packaging)"
    ON
)
message("\tAuto plugin deployment: ${AUTO_PLUGIN_DEPLOYMENT}")
message("\tZip to dist: ${ZIP_TO_DIST}")
message("\tAIO Zip to dist: ${AIO_ZIP_TO_DIST}")
message("\tTracy profiler: ${TRACY_SUPPORT}")
message("\tShader tests: ${BUILD_SHADER_TESTS}")

# #######################################################################################################################
# # Add CMake features
# #######################################################################################################################
include(XSEPlugin)

# #######################################################################################################################
# # Find dependencies
# #######################################################################################################################
find_path(BSHOSHANY_THREAD_POOL_INCLUDE_DIRS "BS_thread_pool.hpp")
find_package(magic_enum CONFIG REQUIRED)
find_package(xbyak CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(imgui CONFIG REQUIRED)
find_package(EASTL CONFIG REQUIRED)
find_package(directxtk CONFIG REQUIRED)
find_package(directxtex CONFIG REQUIRED)
find_path(CLIB_UTIL_INCLUDE_DIRS "ClibUtil/utils.hpp")
find_package(pystring CONFIG REQUIRED)
find_package(cppwinrt CONFIG REQUIRED)
find_package(unordered_dense CONFIG REQUIRED)
find_package(efsw CONFIG REQUIRED)
find_package(Tracy CONFIG REQUIRED)
find_package(directx-headers CONFIG REQUIRED)
add_subdirectory(${CMAKE_SOURCE_DIR}/cmake/Streamline)

find_path(DETOURS_INCLUDE_DIRS "detours/detours.h")
find_library(DETOURS_LIBRARY detours REQUIRED)
include(FidelityFX-SDK)

target_compile_definitions(
    ${PROJECT_NAME}
    PRIVATE "$<$<BOOL:${TRACY_SUPPORT}>:TRACY_SUPPORT>"
)

file(
    GLOB FEATURE_SHADER_DIRS
    RELATIVE "${CMAKE_SOURCE_DIR}"
    "${CMAKE_SOURCE_DIR}/features/*/Shaders"
)

foreach(_dir IN LISTS FEATURE_SHADER_DIRS)
    target_include_directories(
        ${PROJECT_NAME}
        PRIVATE "${CMAKE_SOURCE_DIR}/${_dir}"
    )
endforeach()

target_include_directories(
    ${PROJECT_NAME}
    PRIVATE
        ${BSHOSHANY_THREAD_POOL_INCLUDE_DIRS}
        ${CLIB_UTIL_INCLUDE_DIRS}
        "${CMAKE_SOURCE_DIR}/package/Shaders"
        ${DETOURS_INCLUDE_DIRS}
)

target_link_libraries(
    ${PROJECT_NAME}
    PRIVATE
        Microsoft::CppWinRT
        magic_enum::magic_enum
        xbyak::xbyak
        nlohmann_json::nlohmann_json
        imgui::imgui
        EASTL
        Microsoft::DirectXTK
        Microsoft::DirectXTex
        pystring::pystring
        unordered_dense::unordered_dense
        efsw::efsw
        Tracy::TracyClient
        Streamline
        d3d12.lib
        Microsoft::DirectX-Headers
        ${DETOURS_LIBRARY}
)

# https://gitlab.kitware.com/cmake/cmake/-/issues/24922#note_1371990
if(MSVC_VERSION GREATER_EQUAL 1936 AND MSVC_IDE) # 17.6+
    # When using /std:c++latest, "Build ISO C++23 Standard Library Modules" defaults to "Yes".
    # Default to "No" instead.
    #
    # As of CMake 3.26.4, there isn't a way to control this property
    # (https://gitlab.kitware.com/cmake/cmake/-/issues/24922),
    # We'll use the MSBuild project system instead
    # (https://learn.microsoft.com/en-us/cpp/build/reference/vcxproj-file-structure)
    file(
        CONFIGURE
        OUTPUT "${CMAKE_BINARY_DIR}/Directory.Build.props"
        CONTENT
            [==[
<Project>
  <ItemDefinitionGroup>
    <ClCompile>
      <BuildStlModules>false</BuildStlModules>
    </ClCompile>
  </ItemDefinitionGroup>
</Project>
]==]
        @ONLY
    )
endif()

# #######################################################################################################################
# # Feature version detection
# #######################################################################################################################
file(
    GLOB_RECURSE FEATURE_CONFIG_FILES
    LIST_DIRECTORIES false
    CONFIGURE_DEPENDS
    "features/*/Shaders/Features/*.ini"
)

foreach(FEATURE_PATH ${FEATURE_CONFIG_FILES})
    get_filename_component(FEATURE ${FEATURE_PATH} NAME_WE)
    file(READ "${FEATURE_PATH}" CONFIG_VALUE)
    string(STRIP "${CONFIG_VALUE}" CONFIG_VALUE)
    if(CONFIG_VALUE)
        string(
            REGEX MATCH
            "Version = ([0-9]+)-([0-9]+)-([0-9]+)"
            _
            "${CONFIG_VALUE}"
        )
        if(
            DEFINED CMAKE_MATCH_1
            AND DEFINED CMAKE_MATCH_2
            AND DEFINED CMAKE_MATCH_3
        )
            set(ver_major ${CMAKE_MATCH_1})
            set(ver_minor ${CMAKE_MATCH_2})
            set(ver_patch ${CMAKE_MATCH_3})
            list(
                APPEND
                FEATURE_VERSIONS
                "\t\t{\"${FEATURE}\"sv, {${ver_major},${ver_minor},${ver_patch}}}"
            )
        else()
            message(
                WARNING
                "Feature config file '${FEATURE_PATH}' does not contain a valid version string. Skipping."
            )
        endif()
    else()
        message(
            WARNING
            "Feature config file '${FEATURE_PATH}' is empty or contains only whitespace. Skipping version detection for this feature."
        )
    endif()
endforeach()

set_property(
    DIRECTORY
    APPEND
    PROPERTY CMAKE_CONFIGURE_DEPENDS "${FEATURE_CONFIG_FILES}"
)

string(REPLACE ";" ",\n" FEATURE_VERSIONS "${FEATURE_VERSIONS}")

configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/FeatureVersions.h.in
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h
    @ONLY
)

target_sources(
    "${PROJECT_NAME}"
    PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/cmake/FeatureVersions.h
)

# #######################################################################################################################
# # clang-format
# #######################################################################################################################

find_program(CLANG_FORMAT_PATH clang-format)
if(CLANG_FORMAT_PATH)
    add_custom_target(
        FORMAT_CODE
        COMMAND ${CLANG_FORMAT_PATH} -i -style=file ${CPP_SOURCES};${HLSL_FILES}
        COMMENT "Running clang format for cpp and hlsl files"
    )
endif()

# #######################################################################################################################
# # HLSL additional include directories for VS intellisense
# #######################################################################################################################

set(HLSL_INCLUDE_DIRS ${FEATURE_SHADER_DIRS} "package/Shaders")

set(HLSL_INCLUDE_JSON "")
foreach(dir IN LISTS HLSL_INCLUDE_DIRS)
    if(HLSL_INCLUDE_JSON STREQUAL "")
        set(HLSL_INCLUDE_JSON "    \"${dir}\"")
    else()
        set(HLSL_INCLUDE_JSON "${HLSL_INCLUDE_JSON},\n    \"${dir}\"")
    endif()
endforeach()

configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/shadertoolsconfig.json.in"
    "${CMAKE_CURRENT_SOURCE_DIR}/shadertoolsconfig.json"
    @ONLY
)

# #######################################################################################################################
# # Shader validation config generation
# #######################################################################################################################

# Add target to generate shader validation configuration files
# This requires hlslkit and valid Skyrim installations with recent log files
find_program(POWERSHELL_PATH pwsh powershell)
if(POWERSHELL_PATH)
    add_custom_target(
        generate_shader_configs
        COMMAND
            ${POWERSHELL_PATH} -ExecutionPolicy Bypass -File
            "${CMAKE_SOURCE_DIR}/.github/configs/generate-shader-configs.ps1"
            -OutputDir "${CMAKE_SOURCE_DIR}/.github/configs"
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        COMMENT
            "Generating shader validation configuration files from Skyrim log files"
    )
endif()

# #######################################################################################################################
# # Automatic deployment
# #######################################################################################################################
file(GLOB FEATURE_PATHS LIST_DIRECTORIES true ${CMAKE_SOURCE_DIR}/features/*)
string(TIMESTAMP UTC_NOW "%Y-%m-%dT%H-%MZ" UTC)

# Set AIO directory path used by multiple targets below
set(AIO_DIR "${CMAKE_CURRENT_BINARY_DIR}/aio")

# Robocopy wrapper for Windows incremental file copy (used by deployment targets)
if(WIN32)
    set(ROBOCOPY_WRAPPER "${CMAKE_BINARY_DIR}/robocopy_wrapper.cmd")
    file(
        WRITE
        ${ROBOCOPY_WRAPPER}
        "@echo off\r\nrem Robocopy wrapper: forwards all args to robocopy and normalizes exit codes\r\nrobocopy %*\r\nset rc=%ERRORLEVEL%\r\nif %rc% GEQ 8 exit /b %rc%\r\nexit /b 0\r\n"
    )
endif()

# #######################################################################################################################
# # CMake install() infrastructure for manual packaging
# #######################################################################################################################

# Append a '/' to the end of each feature path for installation all its contents but not itself
set(FEATURE_PATHS_SLASH ${FEATURE_PATHS})
list(TRANSFORM FEATURE_PATHS_SLASH APPEND /)

# Install logic for AIO package
# To copy AIO package at a folder do `${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR}`
install(CODE "file(REMOVE_RECURSE \${CMAKE_INSTALL_PREFIX})")
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION SKSE/Plugins COMPONENT SKSE)
install(
    FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
    DESTINATION SKSE/Plugins
    COMPONENT SKSE
)
install(
    DIRECTORY ${CMAKE_SOURCE_DIR}/package/ ${FEATURE_PATHS_SLASH}
    DESTINATION .
    COMPONENT Shaders
)
install(CODE "file(REMOVE \${CMAKE_INSTALL_PREFIX}/Core)" COMPONENT Shaders)

# #######################################################################################################################
# # Automatic AIO preparation (incremental copy system)
# #######################################################################################################################

if(AUTO_PLUGIN_DEPLOYMENT OR AIO_ZIP_TO_DIST)
    message("Preparing AIO package in ${AIO_DIR}")

    # Prepare AIO only when sources change. Gather package + feature files as
    # inputs so the prepare step runs only when something actually changed.
    file(
        GLOB_RECURSE _AIO_PACKAGE_FILES
        LIST_DIRECTORIES FALSE
        "${CMAKE_SOURCE_DIR}/package/*"
    )
    foreach(_fpath IN LISTS FEATURE_PATHS)
        file(GLOB_RECURSE _tmp LIST_DIRECTORIES FALSE "${_fpath}/*")
        list(APPEND _AIO_PACKAGE_FILES ${_tmp})
    endforeach()

    # Prepare AIO by copying files only when different. This avoids updating
    # timestamps for unchanged files and prevents downstream incremental
    # deploys from copying everything every build.
    set(_prepare_aio_cmds)

    # Ensure SKSE/Plugins dir exists
    # Note: DLL and PDB are copied via POST_BUILD command to avoid race conditions
    list(
        APPEND
        _prepare_aio_cmds
        COMMAND
        ${CMAKE_COMMAND}
        -E
        make_directory
        "${AIO_DIR}/SKSE/Plugins"
    )

    # Copy package files
    file(
        GLOB_RECURSE _AIO_PACKAGE_SOURCE_FILES
        LIST_DIRECTORIES FALSE
        "${CMAKE_SOURCE_DIR}/package/*"
    )
    foreach(_src IN LISTS _AIO_PACKAGE_SOURCE_FILES)
        file(RELATIVE_PATH _rel "${CMAKE_SOURCE_DIR}/package" "${_src}")
        set(_dst "${AIO_DIR}/${_rel}")
        get_filename_component(_dst_dir "${_dst}" DIRECTORY)
        list(
            APPEND
            _prepare_aio_cmds
            COMMAND
            ${CMAKE_COMMAND}
            -E
            make_directory
            "${_dst_dir}"
            COMMAND
            ${CMAKE_COMMAND}
            -E
            copy_if_different
            "${_src}"
            "${_dst}"
        )
    endforeach()

    # Copy feature folders (only files, preserve existing files in AIO)
    foreach(_fpath IN LISTS FEATURE_PATHS)
        if(EXISTS "${_fpath}")
            file(
                GLOB_RECURSE _feature_files
                LIST_DIRECTORIES FALSE
                "${_fpath}/*"
            )
            foreach(_src IN LISTS _feature_files)
                file(RELATIVE_PATH _rel "${_fpath}" "${_src}")
                set(_dst "${AIO_DIR}/${_rel}")
                get_filename_component(_dst_dir "${_dst}" DIRECTORY)
                list(
                    APPEND
                    _prepare_aio_cmds
                    COMMAND
                    ${CMAKE_COMMAND}
                    -E
                    make_directory
                    "${_dst_dir}"
                    COMMAND
                    ${CMAKE_COMMAND}
                    -E
                    copy_if_different
                    "${_src}"
                    "${_dst}"
                )
            endforeach()
        endif()
    endforeach()

    # Remove CORE from AIO if it exists (keep rest intact)
    list(
        APPEND
        _prepare_aio_cmds
        COMMAND
        ${CMAKE_COMMAND}
        -E
        remove
        "${AIO_DIR}/CORE"
    )
    list(
        APPEND
        _prepare_aio_cmds
        COMMAND
        ${CMAKE_COMMAND}
        -E
        touch
        ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
    )

    add_custom_command(
        OUTPUT
            ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
            ${_prepare_aio_cmds}
        DEPENDS ${_AIO_PACKAGE_FILES} ${PROJECT_NAME}
    )

    add_custom_target(
        PREPARE_AIO
        ALL
        DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
    )

    # Copy DLL and PDB using POST_BUILD to avoid race conditions with file locking
    # This ensures the linker has fully released the files before we attempt to copy them
    add_custom_command(
        TARGET ${PROJECT_NAME}
        POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E make_directory "${AIO_DIR}/SKSE/Plugins"
        COMMAND
            ${CMAKE_COMMAND} -E copy_if_different
            "$<TARGET_FILE:${PROJECT_NAME}>"
            "${AIO_DIR}/SKSE/Plugins/$<TARGET_FILE_NAME:${PROJECT_NAME}>"
        COMMAND
            ${CMAKE_COMMAND} -E copy_if_different
            "$<TARGET_PDB_FILE:${PROJECT_NAME}>"
            "${AIO_DIR}/SKSE/Plugins/$<TARGET_PDB_FILE_NAME:${PROJECT_NAME}>"
        COMMENT "Copying built DLL and PDB to AIO package"
        VERBATIM
    )

    # Only copy shaders when HLSL files change; copy individually so unchanged
    # files do not get their timestamps updated.
    file(
        GLOB_RECURSE _package_shaders
        LIST_DIRECTORIES FALSE
        "${CMAKE_SOURCE_DIR}/package/Shaders/*"
    )
    # Exclude test files from production packages
    list(FILTER _package_shaders EXCLUDE REGEX "/Tests/")
    set(_shader_copy_cmds)
    foreach(_src IN LISTS _package_shaders)
        file(RELATIVE_PATH _rel "${CMAKE_SOURCE_DIR}/package/Shaders" "${_src}")
        set(_dst "${AIO_DIR}/Shaders/${_rel}")
        get_filename_component(_dst_dir "${_dst}" DIRECTORY)
        list(
            APPEND
            _shader_copy_cmds
            COMMAND
            ${CMAKE_COMMAND}
            -E
            make_directory
            "${_dst_dir}"
            COMMAND
            ${CMAKE_COMMAND}
            -E
            copy_if_different
            "${_src}"
            "${_dst}"
        )
    endforeach()
    # feature shader folders
    foreach(_fpath IN LISTS FEATURE_PATHS)
        if(EXISTS "${_fpath}/Shaders")
            file(
                GLOB_RECURSE _feat_shaders
                LIST_DIRECTORIES FALSE
                "${_fpath}/Shaders/*"
            )
            get_filename_component(_feat_name "${_fpath}" NAME)
            foreach(_src IN LISTS _feat_shaders)
                file(RELATIVE_PATH _rel "${_fpath}/Shaders" "${_src}")
                # Place feature shader files directly under AIO_DIR/Shaders to preserve expected include paths
                # This matches the package shader layout and ensures includes like "TerrainShadows/..." resolve correctly
                set(_dst "${AIO_DIR}/Shaders/${_rel}")
                get_filename_component(_dst_dir "${_dst}" DIRECTORY)
                list(
                    APPEND
                    _shader_copy_cmds
                    COMMAND
                    ${CMAKE_COMMAND}
                    -E
                    make_directory
                    "${_dst_dir}"
                    COMMAND
                    ${CMAKE_COMMAND}
                    -E
                    copy_if_different
                    "${_src}"
                    "${_dst}"
                )
            endforeach()
        endif()
    endforeach()

    add_custom_command(
        OUTPUT copy_shaders.stamp
        COMMAND
            ${CMAKE_COMMAND} -E make_directory "${AIO_DIR}/Shaders"
            ${_shader_copy_cmds}
        COMMAND ${CMAKE_COMMAND} -E touch copy_shaders.stamp
        DEPENDS ${HLSL_FILES}
        COMMENT "Copying changed shaders into AIO/Shaders"
    )

    # Standalone target for preparing shaders for CI validation
    # This allows shader validation to run without waiting for the full build
    add_custom_target(
        prepare_shaders
        DEPENDS copy_shaders.stamp
        COMMENT "Preparing shaders for validation"
    )
endif()

# Automatic deployment to CommunityShaders output directory.
if(AUTO_PLUGIN_DEPLOYMENT)
    set(DEPLOY_TARGET_HASHES)
    if(WIN32)
        foreach(DEPLOY_TARGET $ENV{CommunityShadersOutputDir})
            message("Deploying AIO to ${DEPLOY_TARGET} (incremental)")

            # Ensure destination root exists
            add_custom_command(
                TARGET ${PROJECT_NAME}
                POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}"
            )

            string(MD5 DEPLOY_TARGET_HASH ${DEPLOY_TARGET})

            # Incremental copy (non-shaders) - produce a stamp so the COPY_SHADERS
            # target can depend on both shader and non-shader deploy steps.
            add_custom_command(
                OUTPUT ${DEPLOY_TARGET_HASH}_deploy.stamp
                COMMAND ${CMAKE_COMMAND} -E make_directory "${DEPLOY_TARGET}"
                COMMAND
                    ${ROBOCOPY_WRAPPER} "${AIO_DIR}" "${DEPLOY_TARGET}" "/E"
                    "/XD" "${AIO_DIR}/Shaders" "/COPY:DAT" "/XO" "/R:1" "/W:1"
                    "/NFL" "/NDL" "/NJH" "/NJS"
                COMMAND
                    ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_deploy.stamp
                DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
                COMMENT
                    "Incremental deploy (excluding Shaders) to ${DEPLOY_TARGET} (robocopy-wrapper)"
            )

            # Ensure plugin DLL/PDB are copied directly to the target SKSE/Plugins
            # folder in case robocopy rules do not copy them as expected.
            add_custom_command(
                OUTPUT ${DEPLOY_TARGET_HASH}_plugin.stamp
                COMMAND
                    ${CMAKE_COMMAND} -E make_directory
                    "${DEPLOY_TARGET}/SKSE/Plugins"
                COMMAND
                    ${CMAKE_COMMAND} -E copy_if_different
                    $<TARGET_FILE:${PROJECT_NAME}>
                    "${DEPLOY_TARGET}/SKSE/Plugins/$<TARGET_FILE_NAME:${PROJECT_NAME}>"
                COMMAND
                    ${CMAKE_COMMAND} -E copy_if_different
                    $<TARGET_PDB_FILE:${PROJECT_NAME}>
                    "${DEPLOY_TARGET}/SKSE/Plugins/$<TARGET_PDB_FILE_NAME:${PROJECT_NAME}>"
                COMMAND
                    ${CMAKE_COMMAND} -E touch ${DEPLOY_TARGET_HASH}_plugin.stamp
                DEPENDS
                    ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
                    ${PROJECT_NAME}
                COMMENT "Copy plugin DLL/PDB to ${DEPLOY_TARGET}/SKSE/Plugins"
            )

            list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}_plugin.stamp)

            # Incremental shader-only copy for fast dev iteration
            # Depends ONLY on copy_shaders.stamp (no DLL, no PREPARE_AIO)
            add_custom_command(
                OUTPUT ${DEPLOY_TARGET_HASH}_shaders_only.stamp
                COMMAND
                    ${CMAKE_COMMAND} -E make_directory
                    "${DEPLOY_TARGET}/Shaders"
                COMMAND
                    ${ROBOCOPY_WRAPPER} "${AIO_DIR}/Shaders"
                    "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/XO" "/R:1"
                    "/W:1" "/NFL" "/NDL" "/NJH" "/NJS"
                COMMAND
                    ${CMAKE_COMMAND} -E touch
                    ${DEPLOY_TARGET_HASH}_shaders_only.stamp
                DEPENDS copy_shaders.stamp
                COMMENT "Fast shader-only deploy to ${DEPLOY_TARGET}/Shaders"
            )

            list(
                APPEND
                SHADER_ONLY_HASHES
                ${DEPLOY_TARGET_HASH}_shaders_only.stamp
            )

            # Full shader copy for packaging (includes PREPARE_AIO dependency)
            # This ensures DLL is built and all AIO files are ready
            add_custom_command(
                OUTPUT ${DEPLOY_TARGET_HASH}_shaders_full.stamp
                COMMAND
                    ${CMAKE_COMMAND} -E make_directory
                    "${DEPLOY_TARGET}/Shaders"
                COMMAND
                    ${ROBOCOPY_WRAPPER} "${AIO_DIR}/Shaders"
                    "${DEPLOY_TARGET}/Shaders" "/E" "/COPY:DAT" "/XO" "/R:1"
                    "/W:1" "/NFL" "/NDL" "/NJH" "/NJS"
                COMMAND
                    ${CMAKE_COMMAND} -E touch
                    ${DEPLOY_TARGET_HASH}_shaders_full.stamp
                DEPENDS
                    copy_shaders.stamp
                    ${CMAKE_CURRENT_BINARY_DIR}/prepare_aio.stamp
                COMMENT
                    "Full shader deploy to ${DEPLOY_TARGET}/Shaders (with PREPARE_AIO)"
            )

            list(APPEND DEPLOY_TARGET_HASHES ${DEPLOY_TARGET_HASH}_deploy.stamp)
            list(
                APPEND
                DEPLOY_TARGET_HASHES
                ${DEPLOY_TARGET_HASH}_shaders_full.stamp
            )
        endforeach()
    else()
        # AUTO_PLUGIN_DEPLOYMENT is enabled but the host is not Windows. Do
        # not attempt to deploy to local Skyrim directories on non-Windows
        # systems; instead provide a minimal COPY_SHADERS target so CI jobs
        # that only prepare shaders still work.
        message(
            WARNING
            "AUTO_PLUGIN_DEPLOYMENT is enabled but not supported on this platform; skipping deployment to CommunityShadersOutputDir"
        )
    endif()

    # Lightweight target for fast shader dev iteration
    # Deploys ONLY shaders to game directory (no DLL build, no tests)
    add_custom_target(
        COPY_SHADERS
        DEPENDS ${SHADER_ONLY_HASHES}
        COMMENT "Fast shader-only deploy to game directory (no DLL, no tests)"
    )

    # Full deployment target for packaging/CI
    # Builds DLL, prepares AIO, deploys everything, runs tests
    add_custom_target(
        DEPLOY_ALL
        ALL
        DEPENDS copy_shaders.stamp ${DEPLOY_TARGET_HASHES}
        COMMENT "Full deployment: DLL + shaders + all files to game directory"
    )
endif()

if(NOT DEFINED ENV{CommunityShadersOutputDir})
    message(
        "When using AUTO_PLUGIN_DEPLOYMENT option, you need to set environment variable 'CommunityShadersOutputDir'"
    )
endif()

# Zip base CommunityShaders and all addons as their own 7z in dist folder
if(ZIP_TO_DIST)
    set(ZIP_DIR "${CMAKE_CURRENT_BINARY_DIR}/zip")
    message("Copying base CommunityShader into ${ZIP_DIR}.")
    add_custom_command(
        TARGET ${PROJECT_NAME}
        POST_BUILD
        COMMAND
            ${CMAKE_COMMAND} -E remove_directory "${ZIP_DIR}"
            ${CMAKE_SOURCE_DIR}/dist
        COMMAND
            ${CMAKE_COMMAND} -E make_directory "${ZIP_DIR}/SKSE/Plugins"
            ${CMAKE_SOURCE_DIR}/dist
        COMMAND
            ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/package
            "${ZIP_DIR}"
        COMMAND
            ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${PROJECT_NAME}>
            "${ZIP_DIR}/SKSE/Plugins/"
        COMMAND
            ${CMAKE_COMMAND} -E copy $<TARGET_PDB_FILE:${PROJECT_NAME}>
            "${ZIP_DIR}/SKSE/Plugins/"
    )
    foreach(FEATURE_PATH ${FEATURE_PATHS})
        if(EXISTS "${FEATURE_PATH}/CORE")
            add_custom_command(
                TARGET ${PROJECT_NAME}
                POST_BUILD
                COMMAND
                    ${CMAKE_COMMAND} -E copy_directory ${FEATURE_PATH}
                    "${ZIP_DIR}"
            )
        endif()
    endforeach()
    add_custom_command(
        TARGET ${PROJECT_NAME}
        POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E remove "${ZIP_DIR}/CORE"
    )

    set(TARGET_ZIP "${PROJECT_NAME}-${UTC_NOW}.7z")
    message("Zipping ${ZIP_DIR} to ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP}")
    add_custom_command(
        TARGET ${PROJECT_NAME}
        POST_BUILD
        COMMAND
            ${CMAKE_COMMAND} -E tar cf ${CMAKE_SOURCE_DIR}/dist/${TARGET_ZIP}
            --format=7zip -- .
        WORKING_DIRECTORY ${ZIP_DIR}
    )

    foreach(FEATURE_PATH ${FEATURE_PATHS})
        if(EXISTS "${FEATURE_PATH}/CORE")
            continue()
        endif()
        get_filename_component(FEATURE ${FEATURE_PATH} NAME)
        message(
            "Zipping ${FEATURE_PATH} to ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z"
        )
        add_custom_command(
            TARGET ${PROJECT_NAME}
            POST_BUILD
            COMMAND
                ${CMAKE_COMMAND} -E tar cf
                ${CMAKE_SOURCE_DIR}/dist/${FEATURE}-${UTC_NOW}.7z --format=7zip
                -- .
            WORKING_DIRECTORY ${FEATURE_PATH}
        )
    endforeach()
endif()

# Create a AIO zip for easier testing
if(AIO_ZIP_TO_DIST)
    if(NOT ZIP_TO_DIST)
        add_custom_command(
            TARGET ${PROJECT_NAME}
            POST_BUILD
            COMMAND
                ${CMAKE_COMMAND} -E remove_directory ${CMAKE_SOURCE_DIR}/dist
            COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_SOURCE_DIR}/dist
        )
    endif()

    # Create a stamp-producing custom command for the AIO archive so CMake
    # only rebuilds the archive when its inputs change. The archive filename
    # keeps the UTC timestamp as before, but the command writes a stable
    # stamp file that CMake can track as OUTPUT.
    set(TARGET_AIO_ZIP "${PROJECT_NAME}_AIO-${UTC_NOW}.7z")
    set(AIO_ARCHIVE "${CMAKE_SOURCE_DIR}/dist/${TARGET_AIO_ZIP}")
    set(AIO_ZIP_STAMP "${CMAKE_CURRENT_BINARY_DIR}/aio_package.stamp")

    message("Zipping ${AIO_DIR} to ${AIO_ARCHIVE}")

    add_custom_command(
        OUTPUT ${AIO_ZIP_STAMP}
        COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_SOURCE_DIR}/dist"
        COMMAND ${CMAKE_COMMAND} -E tar cf ${AIO_ARCHIVE} --format=7zip -- .
        COMMAND ${CMAKE_COMMAND} -E touch ${AIO_ZIP_STAMP}
        WORKING_DIRECTORY ${AIO_DIR}
        DEPENDS PREPARE_AIO
        COMMENT "Creating AIO archive ${AIO_ARCHIVE}"
    )

    add_custom_target(AIO_ZIP_PACKAGE ALL DEPENDS ${AIO_ZIP_STAMP})
endif()

if(NOT DEFINED ENV{CommunityShadersOutputDir})
    message(
        "When using AUTO_PLUGIN_DEPLOYMENT option, you need to set environment variable 'CommunityShadersOutputDir'"
    )
endif()

# #######################################################################################################################
# # Manual packaging targets (Package-XXX)
# #######################################################################################################################

set(DIST_PATH "${CMAKE_SOURCE_DIR}/dist")
file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/dist")

set(CORE_PACKAGE "${DIST_PATH}/${PROJECT_NAME}-${UTC_NOW}.7z")

# CORE_SOURCES = all content copied to the AIO directory + the SKSE plugin dll
file(
    GLOB_RECURSE CORE_SOURCES
    CONFIGURE_DEPENDS
    "${CMAKE_SOURCE_DIR}/package/*"
)
# Add SKSE plugin dll as dependency (use target-file generator expression so CMake
# knows the actual output path of the target at build time)
list(APPEND CORE_SOURCES "$<TARGET_FILE:${PROJECT_NAME}>")

set(CORE_FEATURE_PATHS "${CMAKE_SOURCE_DIR}/package/")

foreach(FEATURE_PATH ${FEATURE_PATHS})
    if(EXISTS "${FEATURE_PATH}/CORE")
        list(APPEND CORE_FEATURE_PATHS "${FEATURE_PATH}/")
        file(GLOB_RECURSE FEATURE_SOURCES CONFIGURE_DEPENDS "${FEATURE_PATH}/*")
        list(APPEND CORE_SOURCES ${FEATURE_SOURCES})
    endif()
endforeach()

# Core package
set(FEATURE_PATH "${CMAKE_BINARY_DIR}/Core")
file(MAKE_DIRECTORY ${FEATURE_PATH})
add_custom_command(
    OUTPUT ${CORE_PACKAGE}
    DEPENDS ${CORE_SOURCES}
    COMMAND
        ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${FEATURE_PATH}
        --component SKSE
    COMMAND
        ${CMAKE_COMMAND} -E copy_directory ${CORE_FEATURE_PATHS} ${FEATURE_PATH}
    COMMAND ${CMAKE_COMMAND} -E rm -f -- ${FEATURE_PATH}/Core
    COMMAND ${CMAKE_COMMAND} -E tar cfv ${CORE_PACKAGE} --format=7zip -- .
    WORKING_DIRECTORY ${FEATURE_PATH}
    COMMENT "Creating Core zip package"
)
add_custom_target("Package-Core" DEPENDS ${CORE_PACKAGE})

# Feature packages
foreach(FEATURE_PATH ${FEATURE_PATHS})
    if(EXISTS "${FEATURE_PATH}/CORE")
        continue()
    endif()

    list(APPEND CORE_FEATURE_PATHS "${FEATURE_PATH}/")
    file(GLOB_RECURSE FEATURE_SOURCES CONFIGURE_DEPENDS "${FEATURE_PATH}/*")
    list(APPEND CORE_SOURCES ${FEATURE_SOURCES})

    get_filename_component(FEATURE ${FEATURE_PATH} NAME)
    set(FEATURE_PACKAGE "${DIST_PATH}/${FEATURE}-${UTC_NOW}.7z")

    add_custom_command(
        OUTPUT ${FEATURE_PACKAGE}
        COMMAND
            ${CMAKE_COMMAND} -E tar cfv ${FEATURE_PACKAGE} --format=7zip -- .
        WORKING_DIRECTORY "${FEATURE_PATH}"
        DEPENDS ${FEATURE_SOURCES}
        COMMENT "Creating ${FEATURE} zip package"
    )

    string(REPLACE " " "" FEATURE ${FEATURE})
    string(REPLACE "-" "" FEATURE ${FEATURE})
    add_custom_target("Package-${FEATURE}" DEPENDS ${FEATURE_PACKAGE})
endforeach()

# AIO Folder target
add_custom_command(
    OUTPUT ${AIO_DIR}/SKSE/Plugins/${PROJECT_NAME}.dll
    DEPENDS ${CORE_SOURCES}
    COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR}
    COMMENT "Installing to AIO folder"
)
add_custom_target("AIO" DEPENDS ${AIO_DIR}/SKSE/Plugins/${PROJECT_NAME}.dll)

# Manual AIO package target
set(AIO_PACKAGE "${DIST_PATH}/${PROJECT_NAME}_AIO-${UTC_NOW}.7z")
add_custom_command(
    OUTPUT ${AIO_PACKAGE}
    DEPENDS ${CORE_SOURCES}
    COMMAND ${CMAKE_COMMAND} -E make_directory ${AIO_DIR}
    COMMAND ${CMAKE_COMMAND} --install ${CMAKE_BINARY_DIR} --prefix ${AIO_DIR}
    COMMAND
        ${CMAKE_COMMAND} -E chdir ${AIO_DIR} ${CMAKE_COMMAND} -E tar cfv
        ${AIO_PACKAGE} --format=7zip -- .
    COMMENT "Creating AIO zip package (manual)"
)
add_custom_target("Package-AIO-Manual" DEPENDS ${AIO_PACKAGE})

# #######################################################################################################################
# # Shader Unit Tests
# #######################################################################################################################
if(BUILD_SHADER_TESTS)
    message(STATUS "Adding shader tests subdirectory")
    enable_testing() # Enable CTest integration for shader tests
    add_subdirectory(tests/shaders)

    # Add a custom target that runs the shader tests
    # Users can run this manually with: cmake --build <build-dir> --target run_shader_tests
    # Runs the test executable directly (not via CTest) to show discovery count
    add_custom_target(
        run_shader_tests
        COMMAND $<TARGET_FILE:shader_tests> --reporter compact
        DEPENDS shader_tests
        WORKING_DIRECTORY $<TARGET_FILE_DIR:shader_tests>
        COMMENT "Running shader unit tests..."
        VERBATIM
    )

    # Make all package targets depend on shader tests passing
    add_dependencies("Package-Core" run_shader_tests)
    add_dependencies("Package-AIO-Manual" run_shader_tests)
    foreach(FEATURE_PATH ${FEATURE_PATHS})
        get_filename_component(FEATURE ${FEATURE_PATH} NAME)
        string(REPLACE " " "" FEATURE ${FEATURE})
        string(REPLACE "-" "" FEATURE ${FEATURE})
        if(TARGET "Package-${FEATURE}")
            add_dependencies("Package-${FEATURE}" run_shader_tests)
        endif()
    endforeach()

    # Make shader deployment targets depend on tests passing
    if(TARGET prepare_shaders)
        add_dependencies(prepare_shaders run_shader_tests)
    endif()
    if(TARGET DEPLOY_ALL)
        add_dependencies(DEPLOY_ALL run_shader_tests)
    endif()

    message(
        STATUS
        "Package and shader deployment targets will automatically run shader tests before deploying"
    )
endif()

message("*************************************************************")
message("Community Shaders configuration complete")
message("To prepare a ZIP package of AIO, Core, or Features")
message("  Build cmake targets:")
message("    - Package-Core: Core package")
message("    - Package-AIO-Manual: AIO package (manual)")
message("    - Package-<Feature>: Individual feature packages")
message("  Or use cmake --install for custom deployment:")
message("    cmake --install ./build/ALL --prefix <TARGET_DIR>")
if(BUILD_SHADER_TESTS)
    message("To run shader tests manually:")
    message(
        "  cmake --build build/<preset> --config <Debug|Release> --target run_shader_tests"
    )
endif()
message("Try switching to build preset 'Dev' for faster iteration time")
message("*************************************************************")
