#!/usr/bin/env python3.12 -B
"""
Script for collecting distribution archives needed for doing a release.

This collects "sdist" and "wheel" archives, the latter for a number of
Python versions (see below).
"""

import fnmatch
import os
import shutil
import subprocess
import sys
import time

from _common_definitions import (
    DIST_DIR,
    TOP_DIR,
    setup_variant,
    system_report,
    variants,
    virtualenv,
    detect_pyversions,
    RED,
    BOLD,
    RESET,
)

WHEEL_BUILD_FAILURE_IS_ERROR = True


def files_matching(pattern: str, dirname: str) -> list[str]:
    result = set(fnmatch.filter(os.listdir(dirname), pattern))
    result.update(fnmatch.filter(os.listdir(dirname), pattern.lower()))
    return sorted(result)


_latest_version: str | None = None


def latest_version() -> str:
    global _latest_version
    if _latest_version is not None:
        return _latest_version

    for ver in detect_pyversions()[::-1]:
        if ver.endswith("t"):
            continue
        _latest_version = ver
        return _latest_version

    raise RuntimeError("Cannot find latest version")


def has_extensions(source_root: str) -> bool:
    """
    Return True if the project at ``source_root`` has
    a C extension
    """
    with open(f"{source_root}/setup.py") as fp:
        if "Extension(" in fp.read():
            return True
    return False


def uses_limited_api(source_root: str) -> bool:
    """
    Return True if the project at ``source_root``
    uses the limited API/stable ABI
    """
    with open(f"{source_root}/setup.py") as fp:
        for ln in fp:
            if ln.lstrip().startswith("#"):
                continue
            if '"py_limited_api"' in ln:
                return True

    return False


def main() -> None:
    if os.path.exists(DIST_DIR):
        shutil.rmtree(DIST_DIR)
    os.mkdir(DIST_DIR)

    # Information about this build:
    system_report(os.path.join(TOP_DIR, "build-info.txt"), detect_pyversions())

    # Collect the list of subprojects to build
    names = ["pyobjc", "pyobjc-core"]
    for nm in sorted(os.listdir(TOP_DIR)):
        if nm.startswith("pyobjc-framework-"):
            names.append(nm)

    failed = []

    before = time.time()
    for ver in detect_pyversions():
        for variant in variants(ver):
            if any(variant.endswith(x) for x in ("-x86_64", "-arm64")):
                # Don't create single architecture wheels
                continue

            setup_variant(ver, variant)

            print(f"{BOLD}Setup {variant}{RESET}")
            with virtualenv(f"python{ver}") as interpreter:
                for nm in names:
                    source_root = f"{TOP_DIR}/{nm}"

                    if ver == latest_version():
                        # Collect sdist archive with most recent python version.
                        print(f"{BOLD}{nm}:{RESET} Build sdist for Python {variant}")
                        status = subprocess.call(
                            [
                                interpreter,
                                "setup.py",
                                "sdist",
                                f"--dist-dir={DIST_DIR}",
                            ],
                            stdout=subprocess.DEVNULL,
                            stderr=subprocess.DEVNULL,
                            cwd=source_root,
                        )
                        if status != 0:
                            if WHEEL_BUILD_FAILURE_IS_ERROR:
                                raise RuntimeError(f"Building wheel for {nm} failed")

                            else:
                                print(
                                    f"{RED}WARNING: Building wheel for {nm} failed{RESET}"
                                )
                                failed.append((nm, "sdist", ""))
                    else:
                        print(
                            f"{BOLD}{nm}:{RESET} Skip sdist for Python {variant} (not {latest_version()})"
                        )

                    if not has_extensions(source_root):
                        # For projects without a C extension build
                        # the wheel using the most recent Python
                        # version
                        if ver != latest_version() and "-" not in variant:
                            print(
                                f"{BOLD}{nm}:{RESET} Skip wheel for Python {variant} (no extension, not {latest_version()})"
                            )
                            continue

                    if uses_limited_api(source_root):
                        # Use the olderst version to build binaries
                        # for wheels using the stable ABI. That's
                        # because the CPython documentation mention
                        # that the headers are not safe for back deploying.
                        if (
                            "t" not in ver
                            and ver != detect_pyversions()[0]
                            and "-" not in variant
                        ):
                            print(
                                f"{BOLD}{nm}:{RESET} Skip wheel for Python {variant} (limited ABI, not {detect_pyversions()[0]})"
                            )
                            continue

                    print(f"{BOLD}{nm}:{RESET} Building wheel for Python {variant}")
                    status = subprocess.call(
                        [
                            interpreter,
                            "setup.py",
                            "build_ext",
                            "-j",
                            str(os.cpu_count()),
                            "bdist_wheel",
                            f"--dist-dir={DIST_DIR}",
                        ],
                        stdout=subprocess.DEVNULL,
                        stderr=subprocess.DEVNULL,
                        cwd=source_root,
                    )

                    if status != 0:
                        if WHEEL_BUILD_FAILURE_IS_ERROR:
                            raise RuntimeError(
                                f"WARNING: Building wheel for {nm} failed (python {variant})"
                            )

                        else:
                            print(
                                f"{RED}WARNING: Building wheel for {nm} failed (python {variant}){RESET}"
                            )
                            failed.append((nm, "wheel", variant))

    after = time.time()
    total_seconds = int(after - before)

    print()
    print(f"{BOLD}Collected files{RESET}")
    print(f"{BOLD}==============={RESET}")
    print()
    for nm in names:
        if nm == "pyobjc":
            continue
        have_wheel = False
        pattern = nm.replace("-", "?") + "-*"
        print(f"{BOLD}{nm}:{RESET}")
        for fn in sorted(files_matching(pattern, DIST_DIR)):
            if fn.endswith(".whl"):
                have_wheel = True
            print(f"  {fn}")
        if not have_wheel:
            print(f"ERROR: no wheels for {nm}")
        print()

    if failed:
        print()
        print(f"{BOLD}Build failures{RESET}")
        print(f"{BOLD}=============={RESET}")
        print()
        for nm, command, ver in failed:
            if ver:
                print(f"{nm} {command}@{ver}")
            else:
                print(f"{nm} {command}")

        print()
        print(
            f"{BOLD}Failed build took {total_seconds // 3600}:{(total_seconds // 60) % 60}:{total_seconds % 60}{RESET}"
        )
        sys.exit(1)

    print()
    print(
        f"{BOLD}Build took {total_seconds // 3600}:{(total_seconds // 60) % 60}:{total_seconds % 60}{RESET}"
    )
    sys.exit(0)


if __name__ == "__main__":
    main()
