# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#   Unai Martinez-Corral                                                                                               #
#                                                                                                                      #
# ==================================================================================================================== #
# Copyright 2020-2025 The pyTooling Authors                                                                            #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
name: Parameters

on:
  workflow_call:
    inputs:
      ubuntu_image_version:
        description: 'Ubuntu image version.'
        required: false
        default: '24.04'
        type: string
      name:
        description: 'Name of the tool.'
        required: false
        default: ''
        type: string
      package_namespace:
        description: 'Name of the tool''s namespace.'
        required: false
        default: ''
        type: string
      package_name:
        description: 'Name of the tool''s package.'
        required: false
        default: ''
        type: string
      python_version:
        description: 'Python version.'
        required: false
        default: '3.13'
        type: string
      python_version_list:
        description: 'Space separated list of Python versions to run tests with.'
        required: false
        default: '3.9 3.10 3.11 3.12 3.13'
        type: string
      system_list:
        description: 'Space separated list of systems to run tests on.'
        required: false
        default: 'ubuntu ubuntu-arm windows windows-arm macos macos-arm mingw64 ucrt64'
        type: string
      include_list:
        description: 'Space separated list of system:python items to be included into the list of test.'
        required: false
        default: ''
        type: string
      exclude_list:
        description: 'Space separated list of system:python items to be excluded from the list of test.'
        required: false
        default: 'windows-arm:3.9 windows-arm:3.10'
        type: string
      disable_list:
        description: 'Space separated list of system:python items to be disabled from the list of test.'
        required: false
        default: 'windows-arm:pypy-3.10 windows-arm:pypy-3.11'
        type: string
      ubuntu_image:
        description: 'The used GitHub Action image for Ubuntu (x86-64) based jobs.'
        required: false
        default: 'ubuntu-24.04'
        type: string
      ubuntu_arm_image:
        description: 'The used GitHub Action image for Ubuntu (aarch64) based jobs.'
        required: false
        default: 'ubuntu-24.04-arm'
        type: string
      windows_image:
        description: 'The used GitHub Action image for Windows Server (x86-64) based jobs.'
        required: false
        default: 'windows-2025'
        type: string
      windows_arm_image:
        description: 'The used GitHub Action image for Windows (aarch64) based jobs.'
        required: false
        default: 'windows-11-arm'
        type: string
      macos_intel_image:
        description: 'The used GitHub Action image for macOS (Intel x86-64) based jobs.'
        required: false
        default: 'macos-13'
        type: string
      macos_arm_image:
        description: 'The used GitHub Action image for macOS (ARM aarch64) based jobs.'
        required: false
        default: 'macos-15'
        type: string
      pipeline-delay:
        description: 'Slow down this job, to delay the startup of the GitHub Action pipline.'
        required: false
        default: 0
        type: number

    outputs:
      python_version:
        description: "Default Python version for other jobs."
        value: ${{ jobs.Parameters.outputs.python_version }}
      package_fullname:
        description: "The package's full name."
        value: ${{ jobs.Parameters.outputs.package_fullname }}
      package_directory:
        description: "The package's directory."
        value: ${{ jobs.Parameters.outputs.package_directory }}
      artifact_basename:
        description: "Artifact base name."
        value: ${{ jobs.Parameters.outputs.artifact_basename }}
      artifact_names:
        description: "Pre-defined artifact names for other jobs."
        value: ${{ jobs.Parameters.outputs.artifact_names }}
      python_jobs:
        description: "List of Python versions (and system combinations) to be used in the matrix of other jobs."
        value: ${{ jobs.Parameters.outputs.python_jobs }}

jobs:
  Parameters:
    name: ✎ Generate pipeline parameters
    runs-on: "ubuntu-${{ inputs.ubuntu_image_version }}"
    outputs:
      python_version:    ${{ steps.variables.outputs.python_version }}
      package_fullname:  ${{ steps.variables.outputs.package_fullname }}
      package_directory: ${{ steps.variables.outputs.package_directory }}
      artifact_basename: ${{ steps.variables.outputs.artifact_basename }}
      artifact_names:    ${{ steps.artifacts.outputs.artifact_names }}
      python_jobs:       ${{ steps.jobs.outputs.python_jobs }}

    steps:
      - name: Generate a startup delay of ${{ inputs.pipeline-delay }} seconds
        id: delay
        if: inputs.pipeline-delay >= 0
        run: |
          sleep ${{ inputs.pipeline-delay }}

      - name: Generate 'python_version'
        id: variables
        shell: python
        run: |
          from os       import getenv
          from pathlib  import Path
          from sys      import exit
          from textwrap import dedent

          python_version =    "${{ inputs.python_version }}".strip()
          package_namespace = "${{ inputs.package_namespace }}".strip()
          package_name =      "${{ inputs.package_name }}".strip()
          name =              "${{ inputs.name }}".strip()

          if package_namespace == "":  # or package_namespace == ".":
            package_fullname = package_name
            package_directory = package_name
          elif package_namespace[-2:] == ".*":
            package_fullname = package_namespace[:-2]
            package_directory = package_namespace[:-2]
          else:
            package_fullname = f"{package_namespace}.{package_name}"
            package_directory = f"{package_namespace}/{package_name}"

          artifact_basename = package_fullname if name == "" else name
          if artifact_basename == "" or artifact_basename == ".":
            print("::error title=Parameter::artifact_basename is empty.")
            exit(1)

          print("Variables:")
          print(f"  python_version:    {python_version}")
          print(f"  package_fullname:  {package_fullname}")
          print(f"  package_directory: {package_directory}")
          print(f"  artifact_basename: {artifact_basename}")

          # Write jobs to special file
          github_output = Path(getenv("GITHUB_OUTPUT"))
          print(f"GITHUB_OUTPUT: {github_output}")
          with github_output.open("a+", encoding="utf-8") as f:
            f.write(dedent(f"""\
              python_version={python_version}
              package_fullname={package_fullname}
              package_directory={package_directory}
              artifact_basename={artifact_basename}
          """))

      - name: Generate 'artifact_names'
        id: artifacts
        shell: python
        run: |
          from json import dumps as json_dumps
          from os import getenv
          from pathlib import Path
          from textwrap import dedent

          package_namespace = "${{ inputs.package_namespace }}".strip()
          package_name =      "${{ inputs.package_name }}".strip()
          artifact_basename = "${{ steps.variables.outputs.artifact_basename }}"

          artifact_names = {
            "unittesting_xml":        f"{artifact_basename}-UnitTestReportSummary-XML",
            "unittesting_html":       f"{artifact_basename}-UnitTestReportSummary-HTML",
            "perftesting_xml":        f"{artifact_basename}-PerformanceTestReportSummary-XML",
            "benchtesting_xml":       f"{artifact_basename}-BenchmarkTestReportSummary-XML",
            "apptesting_xml":         f"{artifact_basename}-ApplicationTestReportSummary-XML",
            "codecoverage_sqlite":    f"{artifact_basename}-CodeCoverage-SQLite",
            "codecoverage_xml":       f"{artifact_basename}-CodeCoverage-XML",
            "codecoverage_json":      f"{artifact_basename}-CodeCoverage-JSON",
            "codecoverage_html":      f"{artifact_basename}-CodeCoverage-HTML",
            "statictyping_cobertura": f"{artifact_basename}-StaticTyping-Cobertura-XML",
            "statictyping_junit":     f"{artifact_basename}-StaticTyping-JUnit-XML",
            "statictyping_html":      f"{artifact_basename}-StaticTyping-HTML",
            "package_all":            f"{artifact_basename}-Packages",
            "documentation_html":     f"{artifact_basename}-Documentation-HTML",
            "documentation_latex":    f"{artifact_basename}-Documentation-LaTeX",
            "documentation_pdf":      f"{artifact_basename}-Documentation-PDF",
          }

          print("Artifacts Names ({len(artifact_names)}):")
          for id, artifactName in artifact_names.items():
            print(f"  {id:>24}: {artifactName}")

          # Write jobs to special file
          github_output = Path(getenv("GITHUB_OUTPUT"))
          print(f"GITHUB_OUTPUT: {github_output}")
          with github_output.open("a+", encoding="utf-8") as f:
            f.write(dedent(f"""\
              artifact_names={json_dumps(artifact_names)}
          """))

      - name: Generate 'python_jobs'
        id: jobs
        shell: python
        run: |
          from json import dumps as json_dumps
          from os import getenv
          from pathlib import Path
          from textwrap import dedent
          from typing import Iterable

          python_version = "${{ steps.variables.outputs.python_version }}"
          name =           "${{ steps.variables.outputs.artifact_basename }}"
          systems =        "${{ inputs.system_list }}".strip()
          versions =       "${{ inputs.python_version_list }}".strip()
          include_list =   "${{ inputs.include_list }}".strip()
          exclude_list =   "${{ inputs.exclude_list }}".strip()
          disable_list =   "${{ inputs.disable_list }}".strip()

          currentMSYS2Version = "3.12"
          currentAlphaVersion = "3.14"
          currentAlphaRelease = "3.14.0-rc.3"

          if systems == "":
            print("::error title=Parameter::system_list is empty.")
          else:
            systems = [sys.strip() for sys in systems.split(" ")]

          if versions == "":
            versions = [ python_version ]
          else:
            versions = [ver.strip() for ver in versions.split(" ")]

          if include_list == "":
            includes = []
          else:
            includes = [tuple(include.strip().split(":")) for include in include_list.split(" ")]

          if exclude_list == "":
            excludes = []
          else:
            excludes = [exclude.strip() for exclude in exclude_list.split(" ")]

          if disable_list == "":
            disabled = []
          else:
            disabled = [disable.strip() for disable in disable_list.split(" ")]

          if "3.8" in versions:
            print("::warning title=Deprecated::Support for Python 3.8 ended in 2024.10.")
          if "msys2" in systems:
            print("::warning title=Deprecated::System 'msys2' will be replaced by 'mingw64'.")
          if currentAlphaVersion in versions:
            print(f"::notice title=Experimental::Python {currentAlphaVersion} ({currentAlphaRelease}) is a pre-release.")
          for disable in disabled:
            print(f"::warning title=Disabled Python Job::{name}: Job combination '{disable}' temporarily disabled.")

          # see https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
          data = {
            # Python and PyPy versions supported by "setup-python" action
            "python": {
              "3.8":       { "icon": "⚫",  "until": "2024.10" },
              "3.9":       { "icon": "🔴",  "until": "2025.10" },
              "3.10":      { "icon": "🟠",  "until": "2026.10" },
              "3.11":      { "icon": "🟡",  "until": "2027.10" },
              "3.12":      { "icon": "🟢",  "until": "2028.10" },
              "3.13":      { "icon": "🟢",  "until": "2029.10" },
              "3.14":      { "icon": "🟣",  "until": "2030.10" },
              "pypy-3.7":  { "icon": "⟲⚫", "until": "????.??" },
              "pypy-3.8":  { "icon": "⟲⚫", "until": "????.??" },
              "pypy-3.9":  { "icon": "⟲🔴", "until": "????.??" },
              "pypy-3.10": { "icon": "⟲🟠", "until": "????.??" },
              "pypy-3.11": { "icon": "⟲🟡", "until": "????.??" },
            },
            # Runner systems (runner images) supported by GitHub Actions
            "sys": {
              "ubuntu":      { "icon": "🐧", "runs-on": "${{ inputs.ubuntu_image }}",      "shell": "bash", "name": "Linux (x86-64)"   },
              "ubuntu-arm":  { "icon": "⛄", "runs-on": "${{ inputs.ubuntu_arm_image }}",  "shell": "bash", "name": "Linux (aarch64)"   },
              "windows":     { "icon": "🪟", "runs-on": "${{ inputs.windows_image }}",     "shell": "pwsh", "name": "Windows (x86-64)" },
              "windows-arm": { "icon": "🏢", "runs-on": "${{ inputs.windows_arm_image }}", "shell": "pwsh", "name": "Windows (aarch64)" },
              "macos":       { "icon": "🍎", "runs-on": "${{ inputs.macos_intel_image }}", "shell": "bash", "name": "macOS (x86-64)"   },
              "macos-arm":   { "icon": "🍏", "runs-on": "${{ inputs.macos_arm_image }}",   "shell": "bash", "name": "macOS (aarch64)"  },
            },
            # Runtimes provided by MSYS2
            "runtime": {
              "msys":    { "icon": "🪟🟪", "name": "Windows+MSYS2 (x86-64) - MSYS"    },
              "mingw32": { "icon": "🪟⬛", "name": "Windows+MSYS2 (x86-64) - MinGW32" },
              "mingw64": { "icon": "🪟🟦", "name": "Windows+MSYS2 (x86-64) - MinGW64" },
              "clang32": { "icon": "🪟🟫", "name": "Windows+MSYS2 (x86-64) - Clang32" },
              "clang64": { "icon": "🪟🟧", "name": "Windows+MSYS2 (x86-64) - Clang64" },
              "ucrt64":  { "icon": "🪟🟨", "name": "Windows+MSYS2 (x86-64) - UCRT64"  },
            }
          }

          print(f"includes ({len(includes)}):")
          for system,version in includes:
            print(f"- {system}:{version}")
          print(f"excludes ({len(excludes)}):")
          for exclude in excludes:
            print(f"- {exclude}")
          print(f"disabled ({len(disabled)}):")
          for disable in disabled:
            print(f"- {disable}")

          def match(combination: str, pattern: str) -> bool:
              system, version = combination.split(":")
              sys, ver = pattern.split(":")

              if sys == "*":
                  return (ver == "*") or (version == ver)
              elif system == sys:
                  return (ver == "*") or (version == ver)
              else:
                  return False

          def notIn(combination: str, patterns: Iterable[str]) -> bool:
              for pattern in patterns:
                  if match(combination, pattern):
                      return False

              return True

          combinations = [
            (system, version)
            for system in systems
              if system in data["sys"]
            for version in versions
              if version in data["python"]
              and notIn(f"{system}:{version}", excludes)
              and notIn(f"{system}:{version}", disabled)
          ] + [
            (system, currentMSYS2Version)
            for system in systems
              if system in data["runtime"]
              and notIn(f"{system}:{currentMSYS2Version}", excludes)
              and notIn(f"{system}:{currentMSYS2Version}", disabled)
          ] + [
            (system, version)
            for system, version in includes
              if system in data["sys"]
              and version in data["python"]
              and notIn(f"{system}:{version}", disabled)
          ]
          print(f"Combinations ({len(combinations)}):")
          for system, version in combinations:
            print(f"- {system}:{version}")

          jobs = [
            {
              "sysicon": data["sys"][system]["icon"],
              "system":  system,
              "runs-on": data["sys"][system]["runs-on"],
              "runtime": "native",
              "shell":   data["sys"][system]["shell"],
              "pyicon":  data["python"][version]["icon"],
              "python":  currentAlphaRelease if version == currentAlphaVersion else version,
              "envname": data["sys"][system]["name"],
            }
            for system, version in combinations if system in data["sys"]
          ] + [
            {
              "sysicon": data["runtime"][runtime]["icon"],
              "system":  "msys2",
              "runs-on": "${{ inputs.windows_image }}",
              "runtime": runtime.upper(),
              "shell":   "msys2 {0}",
              "pyicon":  data["python"][currentMSYS2Version]["icon"],
              "python":  version,
              "envname": data["runtime"][runtime]["name"],
            }
            for runtime, version in combinations if runtime not in data["sys"]
          ]

          print("Parameters:")
          print(f"  python_version: {python_version}")
          print(f"  python_jobs ({len(jobs)}):\n" +
            "".join([f"    {{ " + ", ".join([f"\"{key}\": \"{value}\"" for key, value in job.items()]) + f" }},\n" for job in jobs])
          )

          # Write jobs to special file
          github_output = Path(getenv("GITHUB_OUTPUT"))
          print(f"GITHUB_OUTPUT: {github_output}")
          with github_output.open("a+", encoding="utf-8") as f:
            f.write(dedent(f"""\
              python_version={python_version}
              python_jobs={json_dumps(jobs)}
          """))

      - name: Verify out parameters
        id: verify
        run: |
          printf "python_version:     %s\n" '${{ steps.variables.outputs.python_version }}'
          printf "package_fullname:   %s\n" '${{ steps.variables.outputs.package_fullname }}'
          printf "package_directory:  %s\n" '${{ steps.variables.outputs.package_directory }}'
          printf "artifact_basename:  %s\n" '${{ steps.variables.outputs.artifact_basename }}'
          printf "====================\n"
          printf "artifact_names:     %s\n" '${{ steps.artifacts.outputs.artifact_names }}'
          printf "====================\n"
          printf "python_jobs:        %s\n" '${{ steps.jobs.outputs.python_jobs }}'
          printf "====================\n"
