name: "Build and Test"

on:
  push:
  schedule: # Build and test daily.
    - cron: 0 11 * * *

jobs:
  build-linux:
    name: "Build (linux, ${{ matrix.arch }}, ${{ matrix.target }}) #${{ github.run_number }}"
    runs-on: ubuntu-22.04
    strategy:
      fail-fast: false
      matrix:
        arch: [x86_64, x86_32]
        target: [release, debug]
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Cache submodules
        uses: ./.github/actions/cache-submodules
        id: cache-submodules
        with:
          platform: linux
          arch: ${{ matrix.arch }}
          target: ${{ matrix.target }}
      - name: Pull docker images
        run: docker compose pull
        working-directory: addons/godot_xterm/native
      - name: Cache docker image layers
        uses: jpribyl/action-docker-layer-caching@v0.1.1
        continue-on-error: true
      - name: Ensure scons cache exists
        run: mkdir -p ${{github.workspace}}/.scons-cache
      - name: Build libuv
        if: steps.cache-submodules.outputs.cache-hit != 'true'
        working-directory: addons/godot_xterm/native
        run: UID_GID="$(id -u):$(id -g)" docker compose run -e TARGET=${{ matrix.target }} -e ARCH=${{ matrix.arch }} -v ${{github.workspace}}/.scons-cache:/scons-cache libuv-linux
      - name: Build libgodot-xterm
        working-directory: addons/godot_xterm/native
        run: UID_GID="$(id -u):$(id -g)" docker compose run -e TARGET=${{ matrix.target }} -e ARCH=${{ matrix.arch }} -v ${{github.workspace}}/.scons-cache:/scons-cache libgodot-xterm-linux
      - name: Upload binaries
        uses: actions/upload-artifact@v4
        with:
          name: libgodot-xterm-linux-${{ matrix.arch }}-${{ matrix.target }}
          path: addons/godot_xterm/native/bin/*.so

  build-other-platforms:
    name: "Build (${{ matrix.platform }}, ${{ matrix.arch }}, ${{ matrix.target}}) #${{ github.run_number }}"
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        platform: [web, macos, windows]
        target: [release, debug]
        arch: [wasm32, universal, x86_64, x86_32]
        include:
          - platform: web
            os: ubuntu-22.04
          - platform: macos
            os: macos-15
          - platform: windows
            os: windows-2022
        exclude:
          - platform: web
            arch: x86_64
          - platform: web
            arch: x86_32
          - platform: web
            arch: universal
          - platform: macos
            arch: wasm32
          - platform: macos
            arch: x86_64
          - platform: macos
            arch: x86_32
          - platform: windows
            arch: wasm32
          - platform: windows
            arch: universal
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Cache submodules
        uses: ./.github/actions/cache-submodules
        id: cache-submodules
        with:
          platform: ${{ matrix.platform }}
          target: ${{ matrix.target }}
          arch: ${{ matrix.arch }}
      - name: Cache emscripten
        if: ${{ matrix.platform == 'web' }}
        uses: actions/cache@v4
        env:
          cache-name: cache-emscripten
        with:
          path: addons/godot_xterm/native/.emcache
          key: emsdk-cache-${{ matrix.target }}-${{ hashFiles('**/*.c*', '**/*.h*') }}
          restore-keys: |
            emsdk-cache-${{ matrix.target }}-
      - name: Install web build dependencies
        if: ${{ matrix.platform == 'web' }}
        uses: mymindstorm/setup-emsdk@v14
        with:
          version: 3.1.14
          actions-cache-folder: emsdk-cache-${{ matrix.target }}
      - name: Install additional web build dependencies
        if: ${{ matrix.platform == 'web' }}
        run: sudo apt-get update && sudo apt-get install -y scons gcc-multilib g++-multilib
      - name: Install additional macos build dependencies
        if: ${{ matrix.platform == 'macos' }}
        run: brew install scons
      - name: Install additional windows build dependencies
        if: ${{ matrix.platform == 'windows' }}
        run: python -m pip install scons
      - name: Setup MSVC command prompt
        uses: ilammy/msvc-dev-cmd@v1
        if: ${{ matrix.platform == 'windows' }}
        with:
          arch: win${{ matrix.arch == 'x86_64' && '64' || '32' }}
      - name: Setup cmake
        if: steps.cache-submodules.outputs.cache-hit != 'true'
        uses: jwlawson/actions-setup-cmake@v2
        with:
          cmake-version: "3.23.2"
          use-32bit: ${{ matrix.arch == 'x86_32' && matrix.platform == 'windows' }}
      - name: Build libuv
        if: steps.cache-submodules.outputs.cache-hit != 'true'
        shell: bash
        env:
          PLATFORM: ${{ matrix.platform }}
          TARGET: ${{ matrix.target }}
          BITS: ${{ matrix.arch == 'x86_64' && 64 || 32 }}
        run: |
          cd addons/godot_xterm/native/thirdparty/libuv
          args="-DCMAKE_BUILD_TYPE=$TARGET -DBUILD_SHARED_LIBS=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE \
            -DCMAKE_OSX_ARCHITECTURES=x86_64;arm64"
          if [ "$TARGET" == "release" ]; then
            args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL"
          else
            args="$args -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL"
          fi
          if [ "$BITS" -eq 32 -a "$PLATFORM" == "windows" ]; then
            cmake -G "Visual Studio 17 2022" -A Win32 -S $(pwd) -B "build" $args
          else
            mkdir build || true
            cd build
            if [ "$BITS" -eq 32 ]; then args="$args -DCMAKE_SYSTEM_PROCESSOR=i686 -DCMAKE_C_FLAGS=-m32"; fi
            cmake .. $args
            cd ..
          fi
          cmake --build build --config $TARGET
      - name: Ensure scons cache exists
        run: mkdir -p ${{github.workspace}}/.scons-cache
        shell: bash
      - name: Build libgodot-xterm
        env:
          SCONS_CACHE: ${{github.workspace}}/.scons-cache
        run: |
          cd addons/godot_xterm/native
          scons platform=${{ matrix.platform }} target=template_${{ matrix.target }} arch=${{ matrix.arch }} debug_symbols=${{ matrix.platform != 'windows' && matrix.target == 'debug' && 'yes' || 'no' }} -j2
      - name: Upload binaries
        uses: actions/upload-artifact@v4
        with:
          name: libgodot-xterm-${{ matrix.platform }}-${{ matrix.arch }}-${{ matrix.target }}
          path: |
            addons/godot_xterm/native/bin/*.so
            addons/godot_xterm/native/bin/*.wasm
            addons/godot_xterm/native/bin/*.framework
            addons/godot_xterm/native/bin/spawn-helper
            addons/godot_xterm/native/bin/*.xcframework
            addons/godot_xterm/native/bin/*.dll

  web-export:
    name: "Web Export"
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - id: install-chrome
        uses: browser-actions/setup-chrome@v1
        with:
          chrome-version: 121
      - name: Setup Godot
        uses: lihop/setup-godot@v2
        with:
          version: "4.2.1-stable"
          export-templates: true
      - name: Import assets
        run: godot --editor --headless --quit-after 100 || true
      - name: Wait for wasm build
        uses: fountainhead/action-wait-for-check@v1.2.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          checkName: "Build (web, wasm32, release) #${{ github.run_number }}"
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Wait for linux build # Required for linux-based editor to recognize the GDExtension node types.
        uses: fountainhead/action-wait-for-check@v1.2.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          checkName: "Build (linux, x86_64, debug) #${{ github.run_number }}"
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Install binary build artifacts
        uses: actions/download-artifact@v4
        with:
          path: addons/godot_xterm/native/bin
          merge-multiple: true
      - name: Export for web
        shell: bash
        run: godot --no-window --export-release Web
      - name: NPM cache
        uses: c-hive/gha-npm-cache@v1
        with:
          directory: test/web
      - name: Smoke test Web export
        shell: bash
        working-directory: test/web
        run: |
          export GODOT_XTERM_CHROME_PATH=${{ steps.install-chrome.outputs.chrome-path }}
          npm ci
          npm test
      - name: Upload export
        uses: actions/upload-artifact@v4
        with:
          name: web-demo
          path: docs/demo

  test:
    name: Test (${{ matrix.platform }}, ${{ matrix.arch }}, ${{ matrix.godot-version }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        platform: [linux, macos, windows]
        arch: [x86_64, x86_32, universal]
        godot-version: ["v4.2-stable", "v4.2.1-stable"]
        exclude:
          - platform: linux
            arch: universal
          - platform: macos
            arch: x86_64
          - platform: macos
            arch: x86_32
          - platform: windows
            arch: universal
        include:
          - platform: linux
            os: ubuntu-22.04
          - platform: macos
            os: macos-15
          - platform: windows
            os: windows-2022
    steps:
      - uses: actions/checkout@v4
      - name: Setup Godot
        uses: lihop/setup-godot@v2
        with:
          bits: ${{ matrix.arch == 'x86_32' && 32 || 64 }}
          version: ${{ matrix.godot-version }}
      - name: Macos setup
        if: ${{ matrix.platform == 'macos' }}
        run: |
          # Some macos runners have metal which means we can use vulkan, others don't.
          if system_profiler SPDisplaysDataType | grep -q "Metal"; then
            echo RENDERING_DRIVER=vulkan >> .env
            brew install molten-vk
          else
            echo RENDERING_DRIVER=opengl3 >> .env
          fi
      - name: Install just
        uses: taiki-e/install-action@just
      - name: Install dependencies
        run: just install
      - name: Import assets
        shell: bash
        run: godot --editor --headless --quit-after 100 || true
      - name: Wait for build
        uses: fountainhead/action-wait-for-check@v1.2.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          checkName: "Build (${{ matrix.platform }}, ${{ matrix.arch }}, debug) #${{ github.run_number }}"
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Install binary build artifacts
        uses: actions/download-artifact@v4
        with:
          path: addons/godot_xterm/native/bin
          merge-multiple: true
      - name: Set spawn-helper file permissions
        if: ${{ matrix.platform == 'macos' }}
        # File permissions are not preserved by upload-artifact.
        run: chmod +x addons/godot_xterm/native/bin/spawn-helper
      - name: Test
        shell: bash
        run: |
          if [ "${{ matrix.platform }}" = "windows" ]; then
            just test | tee output.log
          else
            just test-all | tee output.log
          fi

          # This step often passes when it shouldn't, due to GUT not failing on script errors
          # (see: https://github.com/bitwes/Gut/issues/210). Therefore, we check the output
          # for expected (and unexpected) strings.
          if grep -q 'SCRIPT_ERROR:' output.log || grep -q 'Tests             none' output.log; then
            exit 1
          fi
      - name: Upload screenshots
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: failed-screenshots
          path: test/visual_regression/screenshots

  benchmark:
    name: Benchmark (${{matrix.benchmark}})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        benchmark:
          [
            editor_launch,
            cursor_motion,
            dense_cells,
            light_cells,
            scrolling,
            scrolling_bottom_region,
            scrolling_bottom_small_region,
            scrolling_fullscreen,
            scrolling_top_region,
            scrolling_top_small_region,
            unicode,
          ]
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Setup Godot
        uses: lihop/setup-godot@v2
        with:
          version: "4.2.2-stable"
      - name: Install just
        uses: taiki-e/install-action@just
      - name: Update gdextension file
        run: | # Use release builds as the build job finishes sooner.
          sed -i 's/template_debug/template_release/g' addons/godot_xterm/native/godot-xterm.gdextension
      - name: Import assets
        shell: bash
        run: godot --editor --headless --quit-after 100 || true
      - name: Wait for build
        uses: fountainhead/action-wait-for-check@v1.2.0
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          checkName: "Build (linux, x86_64, release) #${{ github.run_number }}"
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      - name: Install binary build artifacts
        uses: actions/download-artifact@v4
        with:
          path: addons/godot_xterm/native/bin
          merge-multiple: true
      - name: Benchmark
        shell: bash
        run: just bench ${{matrix.benchmark}}
      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: benchmark-results-${{ matrix.benchmark }}
          path: benchmark/results/*.json

  process-benchmarks:
    name: Process Benchmarks
    runs-on: ubuntu-latest
    needs: [benchmark]
    permissions:
      deployments: write
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/upload-artifact/merge@v4
        with:
          name: benchmark-results
          pattern: "benchmark-results-*"
          delete-merged: true
      - uses: actions/download-artifact@v4
        with:
          name: benchmark-results
          path: benchmark/results/
      - name: Merge results
        run: jq -s '[.[][]]' benchmark/results/*.json > benchmark/results/all.json
      - name: Download previous benchmark data
        uses: actions/cache@v4
        with:
          path: ./cache
          key: ${{runner.os}}-benchmark
      - name: Store benchmark result
        uses: benchmark-action/github-action-benchmark@v1
        if: github.ref != 'refs/heads/main'
        with:
          tool: "customSmallerIsBetter"
          output-file-path: benchmark/results/all.json
          external-data-json-path: ./cache/benchmark-data.json
          alert-threshold: "20%"
          fail-threshold: "200%"
          github-token: ${{ secrets.GITHUB_TOKEN }}
          comment-on-alert: true
          summary-always: true
      - name: Publish benchmark results
        if: github.ref == 'refs/heads/main'
        uses: benchmark-action/github-action-benchmark@v1
        with:
          name: "GodotXterm Benchmarks"
          tool: "customSmallerIsBetter"
          output-file-path: benchmark/results/all.json
          github-token: ${{ secrets.GITHUB_TOKEN }}
          gh-pages-branch: stable
          benchmark-data-dir-path: docs/dev/bench
          auto-push: true

  merge-artifacts:
    name: Merge Artifacts
    runs-on: ubuntu-latest
    needs: [build-linux, build-other-platforms, test, web-export]
    strategy:
      matrix:
        target: [release, debug]
    steps:
      - uses: actions/upload-artifact/merge@v4
        with:
          name: libgodot-xterm-${{ matrix.target }}
          pattern: "*-${{ matrix.target }}"
          delete-merged: true

  # Git archive should only include addons/godot_xterm directory.
  check-archive:
    name: "Check Archive"
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Create git archive
        run: git archive -o archive.zip HEAD
      - name: Extract archive
        run: mkdir -p /tmp/unzipped && unzip archive.zip -d /tmp/unzipped
      - name: Check that archive only contains addons/godot_xterm directory
        run: |
          shopt -s nullglob dotglob
          ls -laR /tmp/unzipped
          files=(/tmp/unzipped/*)
          if [ ${#files[@]} -ne 1 ]; then
            echo "Wrong number of files in archive (${#files[@]}) expected 1."
            exit 1
          fi
          if [ ! -d "/tmp/unzipped/addons" ]; then
            echo "Expected directory (addons) not found."
            exit 1
          fi
          files=(/tmp/unzipped/addons)
          if [ ${#files[@]} -ne 1 ]; then
            echo "Wrong number of files in addons directory (${#files[@]}) expected 1."
            exit 1
          fi
          if [ ! -d "/tmp/unzipped/addons/godot_xterm" ]; then
            echo "Expected directory (addons/godot_xterm) not found."
            exit 1
          fi

  check-pre-commit:
    name: "Check Pre-Commit"
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: pre-commit/action@v3.0.1