#!/usr/bin/env python3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os
import platform
import sys
import subprocess
import traceback
from textwrap import dedent, fill

MIN_PYTHON_VERSION = (3, 8)
MAX_PYTHON_VERSION_TO_CONSIDER = (3, 11)


def load_mach(dir_path, mach_path, args):
    # Defer import of "importlib.util" until after Python version check has happened
    # so that Python 2 usages fail gracefully.
    import importlib.util

    spec = importlib.util.spec_from_file_location("mach_initialize", mach_path)
    mach_initialize = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mach_initialize)
    return mach_initialize.initialize(dir_path, args)


def check_and_get_mach(dir_path, args):
    initialize_paths = (
        # Run Thunderbird's mach_initialize.py if it exists
        "comm/build/mach_initialize.py",
        "build/mach_initialize.py",
        # test package initialize
        "tools/mach_initialize.py",
    )
    for initialize_path in initialize_paths:
        mach_path = os.path.join(dir_path, initialize_path)
        if os.path.isfile(mach_path):
            return load_mach(dir_path, mach_path, args)
    return None


def find_alternate_python3_executables():
    for i in range(MIN_PYTHON_VERSION[1], MAX_PYTHON_VERSION_TO_CONSIDER[1] + 1):
        potential_python_binary = f"python3.{i}"
        if os.name == "nt":
            potential_python_binary += ".exe"

        try:
            out = subprocess.run(
                [potential_python_binary, "--version"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="UTF-8",
            )

            binary_minor_version = int(out.stdout[9:11].strip("."))

            if binary_minor_version >= MIN_PYTHON_VERSION[1]:
                yield potential_python_binary

        except Exception:
            pass


def try_alternate_python3_executables(args):
    for potential_python_binary in find_alternate_python3_executables():
        try:
            print(
                f"We found '{potential_python_binary}' and will attempt to re-run Mach with it."
            )
            os.execvp(
                potential_python_binary, [potential_python_binary] + ["mach"] + args
            )
        except Exception:
            # We don't really care what goes wrong, just don't let it bubble up
            # If we can't successfully launch with a different python3 binary
            # we will just print the normal help messages.
            pass


def main(args):
    # Ensure we are running Python 3.8+. We run this check as soon as
    # possible to avoid a cryptic import/usage error.
    if sys.version_info < MIN_PYTHON_VERSION:
        print(
            f"Python {MIN_PYTHON_VERSION[0]}.{MIN_PYTHON_VERSION[1]}+ is required to run mach."
        )
        print("You are running Mach with Python {0}".format(platform.python_version()))
        try_alternate_python3_executables(args)
        if sys.platform.startswith("linux"):
            print(
                dedent(
                    """
            See https://firefox-source-docs.mozilla.org/setup/linux_build.html#installingpython
            for guidance on how to install Python on your system.
            """
                ).strip()
            )
        elif sys.platform.startswith("darwin"):
            print(
                dedent(
                    """
            See https://firefox-source-docs.mozilla.org/setup/macos_build.html
            for guidance on how to prepare your system to build Firefox. Perhaps
            you need to update Xcode, or install Python using brew?
            """
                ).strip()
            )
        elif "MOZILLABUILD" in os.environ and os.environ.get("TERM"):
            print(
                dedent(
                    """
            Python is provided by MozillaBuild; ensure your MozillaBuild installation is
            up to date. See https://firefox-source-docs.mozilla.org/setup/windows_build.html#install-mozillabuild
            for details.
            """
                ).strip()
            )
        elif sys.platform.startswith("win"):
            print(
                dedent(
                    """
            You probably want to be interacting with Mach from within MozillaBuild, see
            https://firefox-source-docs.mozilla.org/setup/windows_build.html for details.
            
            If you are deliberately using Mach from outside MozillaBuild, then see
            https://firefox-source-docs.mozilla.org/mach/windows-usage-outside-mozillabuild.html#install-python
            for guidance on installing native Python on your system.
            """
                ).strip()
            )
        else:
            print(
                dedent(
                    """
            We do not have specific instructions for your platform on how to
            install Python. You may find Pyenv (https://github.com/pyenv/pyenv)
            helpful, if your system package manager does not provide a way to
            install a recent enough Python 3.
            """
                ).strip()
            )
        sys.exit(1)

    # XCode python sets __PYVENV_LAUNCHER__, which overrides the executable
    # used when a python subprocess is created. This is an issue when we want
    # to run using our virtualenv python executables.
    # In future Python relases, __PYVENV_LAUNCHER__ will be cleared before
    # application code (mach) is started.
    # https://github.com/python/cpython/pull/9516
    os.environ.pop("__PYVENV_LAUNCHER__", None)

    try:
        mach = check_and_get_mach(os.path.dirname(os.path.realpath(__file__)), args)
        if not mach:
            print("Could not run mach: No mach source directory found.")
            sys.exit(1)
        sys.exit(mach.run(args))
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception as e:
        if sys.version_info >= (
            MAX_PYTHON_VERSION_TO_CONSIDER[0],
            MAX_PYTHON_VERSION_TO_CONSIDER[1] + 1,
        ):
            traceback.print_exc()
            print()
            print("---")
            print()
            print(
                fill(
                    dedent(
                        f"""\
                Note that you are running Mach with Python
                {platform.python_version()}, which is higher than the highest
                known working version of Python for Mach. Consider running Mach
                with Python {MAX_PYTHON_VERSION_TO_CONSIDER[0]}.{MAX_PYTHON_VERSION_TO_CONSIDER[1]}
                or lower."""
                    )
                )
            )

            try:
                alternative = next(find_alternate_python3_executables())
                print()
                print("Running the following command may solve your issue:")
                print()
                print(f"    {alternative} {sys.argv[0]} {' '.join(args)}")
                print()
            except StopIteration:
                pass
            sys.exit(1)
        else:
            raise


if __name__ == "__main__":
    main(sys.argv[1:])
