# Summary
# This reusable workflow handles building UE4SS with CMake, including setup of dependencies and uploading of artifacts.
# Any future workflows/tasks that require UE4SS to be built should call this reusable workflow to ensure consistency.
name: "Build UE4SS with CMake"
permissions:
  contents: read
on:
  workflow_call:
    inputs:
      build-mode:
        description: 'Which UE4SS Mode to build (e.g., Game__Shipping__Win64)'
        type: string
        required: true
      commit-sha:
        description: 'Commit to build'
        type: string
        required: true
      should-upload-artifact:
        description: 'Should build output be uploaded as an artifact?'
        type: boolean
        default: false
      artifact-retention-days:
        description: 'How many days to retain artifacts'
        type: string
        default: '7'
      enable-caching:
        description: 'Enable build and dependency caching (disable for production releases)'
        type: boolean
        default: true
      use-clang:
        description: 'Use Clang-CL compiler instead of MSVC'
        type: boolean
        default: false
      build-ue4ss:
        description: 'Build UE4SS and proxy DLLs'
        type: boolean
        default: true
      build-cppmods:
        description: 'Build C++ mods (requires build-ue4ss)'
        type: boolean
        default: true
      build-uvtd:
        description: 'Build UVTD (only for Game__Shipping__Win64)'
        type: boolean
        default: true
defaults:
  run:
    shell: pwsh
jobs:
  build:
    runs-on: windows-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4
        with:
          submodules: recursive
          token: ${{ secrets.UEPSEUDO_PAT }}
          ref: ${{inputs.commit-sha}}
          fetch-depth: 0  # Need full history for git diff to work with PR base SHA

      # Calculate cache key based on CMakeLists and submodules
      - name: Calculate cache key
        id: cache_key
        run: |
          # Get all CMakeLists.txt and .cmake files and create a combined hash
          $files = Get-ChildItem -Path . -Include "CMakeLists.txt","*.cmake" -Recurse
          $combinedContent = ""
          foreach ($file in $files) {
            # Include file content hash AND path for better cache invalidation
            $combinedContent += (Get-FileHash $file.FullName).Hash
            $combinedContent += $file.FullName
          }

          # Create a hash of the combined content
          $bytes = [System.Text.Encoding]::UTF8.GetBytes($combinedContent)
          $hash = [System.Security.Cryptography.SHA256]::Create()
          $hashBytes = $hash.ComputeHash($bytes)
          $cmakeHash = [System.BitConverter]::ToString($hashBytes).Replace("-", "")

          # Get submodule commits hash
          git submodule status > submodule_status.txt
          $submoduleHash = (Get-FileHash submodule_status.txt).Hash

          # Combine hashes for final cache key (shortened for readability)
          $combinedHash = "$($cmakeHash.Substring(0, 16))-$($submoduleHash.Substring(0, 16))"
          echo "hash=$combinedHash" >> $env:GITHUB_OUTPUT

          # Clean up temp file
          Remove-Item submodule_status.txt

      # Cache for third-party dependencies fetched by CMake
      # NOTE: This caching approach was carefully designed to avoid PatternSleuth (Rust) cache issues
      # The cache key includes CMakeLists.txt hashes which should catch most dependency changes
      # For Rust version changes, the weekly cache invalidation in CI provides a safety net
      # Caching is disabled for production releases to ensure clean builds
      # Dependencies are shared between MSVC and Clang-CL builds since most are compiler-agnostic
      - name: Cache CMake Dependencies
        if: ${{ inputs.enable-caching }}
        uses: actions/cache@v4
        with:
          path: |
            ~/.cmake/packages
            ~/AppData/Local/vcpkg/archives
            ${{ github.workspace }}/build_cmake_shared_deps/_deps
            ${{ github.workspace }}/build_cmake_shared_deps/ep_downloads
          key: cmake-deps-${{ runner.os }}-${{ steps.cache_key.outputs.hash }}
          restore-keys: |
            cmake-deps-${{ runner.os }}-

      # Use pre-installed VS instead of downloading
      - name: Setup MSVC Developer Command Prompt
        uses: ilammy/msvc-dev-cmd@v1
        with:
          arch: x64
          vsversion: "2022"

      # Setup Clang-CL if requested
      - name: Setup Clang-CL
        if: ${{ inputs.use-clang == true }}
        run: |
          # LLVM is pre-installed on windows-latest, just need to ensure clang-cl is in PATH
          $llvmPath = "C:\Program Files\LLVM\bin"
          if (Test-Path $llvmPath) {
            Write-Host "Found LLVM at $llvmPath"
            echo "$llvmPath" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
          } else {
            Write-Host "LLVM not found at expected location, checking if clang-cl is already available..."
            $clangcl = Get-Command clang-cl -ErrorAction SilentlyContinue
            if ($clangcl) {
              Write-Host "clang-cl found at: $($clangcl.Path)"
            } else {
              Write-Host "##[error]clang-cl not found. Please install LLVM or use MSVC."
              exit 1
            }
          }

      - name: Setup CMake
        uses: jwlawson/actions-setup-cmake@v2
        with:
          cmake-version: 'latest'

      - name: Setup Ninja
        uses: seanmiddleditch/gha-setup-ninja@v5

      - name: Create build directory name
        id: build_dir
        run: |
          $mode = "${{inputs.build-mode}}"
          $compiler = if ("${{ inputs.use-clang }}" -eq "true") { "clang" } else { "msvc" }
          $buildDir = "build_cmake_${compiler}_$mode"
          echo "name=$buildDir" >> $env:GITHUB_OUTPUT

      # Cache the build directory for incremental builds
      # Disabled for production releases to ensure clean builds
      # Build cache is compiler-specific since object files differ between MSVC and Clang
      # Cache key includes hash of CMakeLists/code - when code changes, cache is invalidated
      - name: Cache CMake build directory
        if: ${{ inputs.enable-caching }}
        uses: actions/cache@v4
        id: cache_build
        with:
          path: |
            ${{ steps.build_dir.outputs.name }}
            !${{ steps.build_dir.outputs.name }}/_deps
            !${{ steps.build_dir.outputs.name }}/ep_downloads
          key: cmake-build-${{ inputs.use-clang && 'clang' || 'msvc' }}-${{ inputs.build-mode }}-${{ steps.cache_key.outputs.hash }}
          restore-keys: |
            cmake-build-${{ inputs.use-clang && 'clang' || 'msvc' }}-${{ inputs.build-mode }}-

      - name: Configure CMake
        run: |
          $needs_reconfigure = $false

          # Check if caching is enabled
          if ("${{ inputs.enable-caching }}" -eq "true") {
            if ("${{ steps.cache_build.outputs.cache-hit }}" -ne "true") {
              Write-Host "Cache miss - full configure needed"
              $needs_reconfigure = $true
            } elseif (-not (Test-Path "${{ steps.build_dir.outputs.name }}/CMakeCache.txt")) {
              Write-Host "No CMakeCache.txt - full configure needed"
              $needs_reconfigure = $true
            }
          } else {
            Write-Host "Caching disabled - clean build required"
            $needs_reconfigure = $true
          }

          if ($needs_reconfigure) {
            # Set environment variables for FetchContent to use a shared location
            # This allows MSVC and Clang-CL builds to share dependencies
            $env:FETCHCONTENT_BASE_DIR = "${{ github.workspace }}/build_cmake_shared_deps/_deps"

            # Select compiler based on use-clang parameter
            if ("${{ inputs.use-clang }}" -eq "true") {
              Write-Host "Configuring with Clang-CL compiler"
              $c_compiler = "clang-cl"
              $cxx_compiler = "clang-cl"
            } else {
              Write-Host "Configuring with MSVC compiler"
              $c_compiler = "cl"
              $cxx_compiler = "cl"
            }

            # Configure with incremental linking disabled
            # Note: Must use double quotes for variables to expand properly
            cmake -B "${{ steps.build_dir.outputs.name }}" `
              -G Ninja `
              -DCMAKE_BUILD_TYPE="${{inputs.build-mode}}" `
              -DCMAKE_C_COMPILER="$c_compiler" `
              -DCMAKE_CXX_COMPILER="$cxx_compiler" `
              -DCMAKE_EXPORT_COMPILE_COMMANDS=ON `
              -DFETCHCONTENT_QUIET=OFF `
              -DFETCHCONTENT_FULLY_DISCONNECTED=OFF `
              -DFETCHCONTENT_UPDATES_DISCONNECTED=ON
          } else {
            Write-Host "Using cached configuration - incremental build"
            # Just run cmake to ensure cache is valid
            cmake ${{ steps.build_dir.outputs.name }}
          }

      # Build targets based on inputs from caller
      - name: Build Targets
        run: |
          Write-Host "Building targets with verbose output..."
          $buildMode = "${{inputs.build-mode}}"
          $buildUE4SS = "${{ inputs.build-ue4ss }}" -eq "true"
          $buildCppMods = "${{ inputs.build-cppmods }}" -eq "true"
          $buildUVTD = "${{ inputs.build-uvtd }}" -eq "true"
          $buildProxy = $false

          Write-Host "Build UE4SS: $buildUE4SS"
          Write-Host "Build C++ Mods: $buildCppMods"
          Write-Host "Build UVTD: $buildUVTD (only for Game__Shipping__Win64)"

          # Proxy (dwmapi.dll) should only be built for Game__Shipping__Win64 and Game__Debug__Win64
          if ($buildUE4SS -and ($buildMode -eq "Game__Shipping__Win64" -or $buildMode -eq "Game__Debug__Win64")) {
            $buildProxy = $true
            Write-Host "Build Proxy: True (dwmapi.dll for $buildMode)"
          } else {
            Write-Host "Build Proxy: False (not needed or UE4SS not being built)"
          }

          # UVTD should only be built for Game__Shipping__Win64
          if ($buildMode -ne "Game__Shipping__Win64") {
            $buildUVTD = $false
            Write-Host "Disabling UVTD build - only built for Game__Shipping__Win64"
          }

          # Determine targets to build
          $targets = @()
          if ($buildUE4SS) { $targets += "UE4SS" }
          if ($buildProxy) { $targets += "proxy" }
          if ($buildUVTD) { $targets += "UnrealVTableDumper" }

          # Dynamically discover and add all C++ mod targets
          # C++ mods depend on UE4SS, so only build them if UE4SS is being built
          if ($buildCppMods -and $buildUE4SS -and (Test-Path "cppmods")) {
            $modDirs = Get-ChildItem -Path "cppmods" -Directory | Where-Object {
              Test-Path (Join-Path $_.FullName "CMakeLists.txt")
            }
            foreach ($modDir in $modDirs) {
              $modName = $modDir.Name
              Write-Host "Discovered C++ mod: $modName"
              $targets += $modName
            }
          } elseif (Test-Path "cppmods") {
            Write-Host "Skipping C++ mods - UE4SS not being built or build-cppmods is false"
          }

          if ($targets.Count -eq 0) {
            Write-Host "No targets to build"
            exit 0
          }

          Write-Host "Building targets: $($targets -join ', ')"
          foreach ($target in $targets) {
            cmake --build ${{ steps.build_dir.outputs.name }} --target $target --verbose
          }

      # Collect artifacts based on what was built
      - name: Calculate Files For Artifact Inclusion
        id: calc-files-artifact
        if: ${{inputs.should-upload-artifact == true}}
        run: |
          $artifact_dir = New-Item -Path '${{runner.temp}}/ue4ss_artifacts/' -ItemType Directory -Force
          $buildDir = "${{ steps.build_dir.outputs.name }}"
          $buildMode = "${{inputs.build-mode}}"

          # Use inputs to determine what to collect
          $includeUE4SS = "${{ inputs.build-ue4ss }}" -eq "true"
          $includeCppMods = "${{ inputs.build-cppmods }}" -eq "true"
          $includeUVTD = "${{ inputs.build-uvtd }}" -eq "true"

          # Determine if we should have proxy (dwmapi.dll) for this build mode
          $shouldHaveProxy = ($buildMode -eq "Game__Shipping__Win64") -or ($buildMode -eq "Game__Debug__Win64")

          # Determine if we should have UVTD for this build mode
          $shouldHaveUVTD = ($buildMode -eq "Game__Shipping__Win64")

          Write-Host "Collecting artifacts from $buildDir..."
          Write-Host "Build Mode: $buildMode"
          Write-Host "Include UE4SS: $includeUE4SS"
          Write-Host "Include C++ Mods: $includeCppMods"
          Write-Host "Include UVTD: $includeUVTD"
          Write-Host "Should have proxy: $shouldHaveProxy"
          Write-Host "Should have UVTD: $shouldHaveUVTD"

          # Collect UE4SS.dll and dwmapi.dll (proxy) if needed
          if ($includeUE4SS) {
            if ($shouldHaveProxy) {
              $ue4ssFiles = Get-ChildItem -Path $buildDir -Recurse -Include "UE4SS.dll","UE4SS.pdb","dwmapi.dll" -ErrorAction SilentlyContinue
            } else {
              $ue4ssFiles = Get-ChildItem -Path $buildDir -Recurse -Include "UE4SS.dll","UE4SS.pdb" -ErrorAction SilentlyContinue
            }
            foreach ($file in $ue4ssFiles) {
              if ($file.FullName -notlike "*\_deps\*") {
                Write-Host "✓ Found UE4SS file: $($file.Name)"
                Copy-Item $file.FullName -Destination $artifact_dir -Force
              }
            }
          }

          # Dynamically collect all C++ mod DLLs
          # Discover mod names from cppmods directory structure
          if ($includeCppMods -and (Test-Path "cppmods")) {
            $modDirs = Get-ChildItem -Path "cppmods" -Directory | Where-Object {
              Test-Path (Join-Path $_.FullName "CMakeLists.txt")
            }
            foreach ($modDir in $modDirs) {
              $modName = $modDir.Name
              Write-Host "Looking for C++ mod: $modName"

              # Search for the mod DLL (and PDB if dev build)
              $modDll = Get-ChildItem -Path $buildDir -Recurse -Filter "$modName.dll" -ErrorAction SilentlyContinue | Where-Object {
                $_.FullName -notlike "*\_deps\*"
              } | Select-Object -First 1

              if ($modDll) {
                Write-Host "✓ Found C++ mod DLL: $($modDll.Name)"
                Copy-Item $modDll.FullName -Destination $artifact_dir -Force

                # Also collect PDB if it exists
                $modPdb = $modDll.FullName -replace '\.dll$', '.pdb'
                if (Test-Path $modPdb) {
                  Write-Host "✓ Found C++ mod PDB: $(Split-Path $modPdb -Leaf)"
                  Copy-Item $modPdb -Destination $artifact_dir -Force
                }
              } else {
                Write-Host "⚠ Warning: $modName.dll not found in build output"
              }
            }
          }

          # Collect UVTD files (only for Game__Shipping__Win64)
          # CMake target is UnrealVTableDumper, so look for both possible names
          if ($includeUVTD -and $shouldHaveUVTD) {
            $uvtdFiles = Get-ChildItem -Path $buildDir -Recurse -Include "UVTD.exe","UVTD.pdb","UnrealVTableDumper.exe","UnrealVTableDumper.pdb" -ErrorAction SilentlyContinue
            foreach ($file in $uvtdFiles) {
              if ($file.FullName -notlike "*\_deps\*") {
                Write-Host "✓ Found UVTD file: $($file.Name)"
                # Rename UnrealVTableDumper to UVTD for consistency
                if ($file.Name -like "UnrealVTableDumper.*") {
                  $newName = $file.Name -replace "UnrealVTableDumper", "UVTD"
                  Copy-Item $file.FullName -Destination "$artifact_dir\$newName" -Force
                } else {
                  Copy-Item $file.FullName -Destination $artifact_dir -Force
                }
              }
            }
          }

          # Validate that required files were collected (only check files we expect)
          $missingFiles = @()

          if ($includeUE4SS) {
            # Always require UE4SS.dll
            if (-not (Test-Path "$artifact_dir\UE4SS.dll")) {
              Write-Host "❌ ERROR: Required UE4SS.dll not found!"
              $missingFiles += "UE4SS.dll"
            }

            # Only require dwmapi.dll for Shipping and Debug configs
            if ($shouldHaveProxy) {
              if (-not (Test-Path "$artifact_dir\dwmapi.dll")) {
                Write-Host "❌ ERROR: Required dwmapi.dll not found for $buildMode!"
                $missingFiles += "dwmapi.dll"
              }
            }
          }

          # Only require UVTD.exe for Game__Shipping__Win64
          if ($includeUVTD -and $shouldHaveUVTD) {
            if (-not (Test-Path "$artifact_dir\UVTD.exe")) {
              Write-Host "❌ ERROR: Required UVTD.exe not found!"
              $missingFiles += "UVTD.exe"
            }
          }

          # Summary of collected artifacts
          Write-Host "`n========== Artifact Summary =========="
          $artifacts = Get-ChildItem -Path $artifact_dir -ErrorAction SilentlyContinue
          Write-Host "Total files collected: $($artifacts.Count)"

          if ($artifacts.Count -gt 0) {
            # Group by extension
            $artifacts | Group-Object Extension | ForEach-Object {
              Write-Host "$($_.Name) files: $($_.Count)"
              $_.Group | ForEach-Object { Write-Host "  - $($_.Name)" }
            }
          }

          # Fail if critical files are missing
          if ($missingFiles.Count -gt 0) {
            Write-Host "`n❌ Build failed: Missing critical artifacts:" -ForegroundColor Red
            $missingFiles | ForEach-Object { Write-Host "  - $_" -ForegroundColor Red }
            exit 1
          }

          # Fail if no artifacts were collected at all
          if ($artifacts.Count -eq 0) {
            Write-Host "`n❌ Build failed: No artifacts were collected!" -ForegroundColor Red
            Write-Host "This may indicate a build failure or incorrect artifact paths." -ForegroundColor Red
            exit 1
          }

          echo "artifact_dir=$artifact_dir" >> $env:GITHUB_OUTPUT

      - name: Upload a Build Artifact
        if: ${{inputs.should-upload-artifact == true}}
        uses: actions/upload-artifact@v4
        with:
          name: CMAKE-${{inputs.build-mode}}
          path: ${{ steps.calc-files-artifact.outputs.artifact_dir }}
          retention-days: ${{ inputs.artifact-retention-days }}
          overwrite: true
