# 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 errno
import json
import os
import stat
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional, Union

import requests
from tqdm import tqdm

# We need the NDK version in multiple different places, and it's inconvenient
# to pass down the NDK version to all relevant places, so we have this global
# variable.
from mozboot.bootstrap import MOZCONFIG_SUGGESTION_TEMPLATE

NDK_VERSION = "r23c"
CMDLINE_TOOLS_VERSION_STRING = "9.0"
CMDLINE_TOOLS_VERSION = "9477386"

BUNDLETOOL_VERSION = "1.14.1"

# We expect the emulator AVD definitions to be platform agnostic
LINUX_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
LINUX_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"

MACOS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
MACOS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"
MACOS_ARM64_ANDROID_AVD = "linux64-android-avd-arm64-repack"

WINDOWS_X86_64_ANDROID_AVD = "linux64-android-avd-x86_64-repack"
WINDOWS_ARM_ANDROID_AVD = "linux64-android-avd-arm-repack"

AVD_MANIFEST_X86_64 = Path(__file__).resolve().parent / "android-avds/x86_64.json"
AVD_MANIFEST_ARM = Path(__file__).resolve().parent / "android-avds/arm.json"
AVD_MANIFEST_ARM64 = Path(__file__).resolve().parent / "android-avds/arm64.json"

JAVA_VERSION_MAJOR = "17"
JAVA_VERSION_MINOR = "0.7"
JAVA_VERSION_PATCH = "7"

ANDROID_NDK_EXISTS = """
Looks like you have the correct version of the Android NDK installed at:
%s
"""

ANDROID_SDK_EXISTS = """
Looks like you have the Android SDK installed at:
%s
We will install all required Android packages.
"""

ANDROID_SDK_TOO_OLD = """
Looks like you have an outdated Android SDK installed at:
%s
I can't update outdated Android SDKs to have the required 'sdkmanager'
tool.  Move it out of the way (or remove it entirely) and then run
bootstrap again.
"""

INSTALLING_ANDROID_PACKAGES = """
We are now installing the following Android packages:
%s
You may be prompted to agree to the Android license. You may see some of
output as packages are downloaded and installed.
"""

MOBILE_ANDROID_MOZCONFIG_TEMPLATE = """
# Build GeckoView/Firefox for Android:
ac_add_options --enable-project=mobile/android

# Targeting the following architecture.
# For regular phones, no --target is needed.
# For x86 emulators (and x86 devices, which are uncommon):
# ac_add_options --target=i686
# For newer phones or Apple silicon
# ac_add_options --target=aarch64
# For x86_64 emulators (and x86_64 devices, which are even less common):
# ac_add_options --target=x86_64

{extra_lines}
"""

MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE = """
# Build GeckoView/Firefox for Android Artifact Mode:
ac_add_options --enable-project=mobile/android
ac_add_options --enable-artifact-builds

{extra_lines}
# Write build artifacts to:
mk_add_options MOZ_OBJDIR=./objdir-frontend
"""


class GetNdkVersionError(Exception):
    pass


def install_mobile_android_sdk_or_ndk(url, path: Path):
    """
    Fetch an Android SDK or NDK from |url| and unpack it into the given |path|.

    We use, and 'requests' respects, https. We could also include SHAs for a
    small improvement in the integrity guarantee we give. But this script is
    bootstrapped over https anyway, so it's a really minor improvement.

    We keep a cache of the downloaded artifacts, writing into |path|/mozboot.
    We don't yet clean the cache; it's better to waste some disk space and
    not require a long re-download than to wipe the cache prematurely.
    """

    download_path = path / "mozboot"
    try:
        download_path.mkdir(parents=True)
    except OSError as e:
        if e.errno == errno.EEXIST and download_path.is_dir():
            pass
        else:
            raise

    file_name = url.split("/")[-1]
    download_file_path = download_path / file_name
    download(url, download_file_path)

    if file_name.endswith(".tar.gz") or file_name.endswith(".tgz"):
        cmd = ["tar", "zxf", str(download_file_path)]
    elif file_name.endswith(".tar.bz2"):
        cmd = ["tar", "jxf", str(download_file_path)]
    elif file_name.endswith(".zip"):
        cmd = ["unzip", "-q", str(download_file_path)]
    elif file_name.endswith(".bin"):
        # Execute the .bin file, which unpacks the content.
        mode = os.stat(path).st_mode
        download_file_path.chmod(mode | stat.S_IXUSR)
        cmd = [str(download_file_path)]
    else:
        raise NotImplementedError(f"Don't know how to unpack file: {file_name}")

    print(f"Unpacking {download_file_path}...")

    with open(os.devnull, "w") as stdout:
        # These unpack commands produce a ton of output; ignore it.  The
        # .bin files are 7z archives; there's no command line flag to quiet
        # output, so we use this hammer.
        subprocess.check_call(cmd, stdout=stdout, cwd=str(path))

    print(f"Unpacking {download_file_path}... DONE")
    # Now delete the archive
    download_file_path.unlink()


def download(
    url,
    download_file_path: Path,
):
    with requests.Session() as session:
        request = session.head(url, allow_redirects=True)
        request.raise_for_status()
        remote_file_size = int(request.headers["content-length"])

        if download_file_path.is_file():
            local_file_size = download_file_path.stat().st_size

            if local_file_size == remote_file_size:
                print(
                    f"{download_file_path.name} already downloaded. Skipping download..."
                )
            else:
                print(f"Partial download detected. Resuming download of {url}...")
                download_internal(
                    download_file_path,
                    session,
                    url,
                    remote_file_size,
                    local_file_size,
                )
        else:
            print(f"Downloading {url}...")
            download_internal(download_file_path, session, url, remote_file_size)


def download_internal(
    download_file_path: Path,
    session,
    url,
    remote_file_size,
    resume_from_byte_pos: int = None,
):
    """
    Handles both a fresh SDK/NDK download, as well as resuming a partial one
    """
    # "ab" will behave same as "wb" if file does not exist
    with open(download_file_path, "ab") as file:
        # 64 KB/s should be fine on even the slowest internet connections
        chunk_size = 1024 * 64
        # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#directives
        resume_header = (
            {"Range": f"bytes={resume_from_byte_pos}-"}
            if resume_from_byte_pos
            else None
        )

        request = session.get(
            url, stream=True, allow_redirects=True, headers=resume_header
        )

        with tqdm(
            total=int(remote_file_size),
            unit="B",
            unit_scale=True,
            unit_divisor=1024,
            desc=download_file_path.name,
            initial=resume_from_byte_pos if resume_from_byte_pos else 0,
        ) as progress_bar:
            for chunk in request.iter_content(chunk_size):
                file.write(chunk)
                progress_bar.update(len(chunk))


def get_ndk_version(ndk_path: Union[str, Path]):
    """Given the path to the NDK, return the version as a 3-tuple of (major,
    minor, human).
    """
    ndk_path = Path(ndk_path)
    with open(ndk_path / "source.properties", "r") as f:
        revision = [line for line in f if line.startswith("Pkg.Revision")]
        if not revision:
            raise GetNdkVersionError(
                "Cannot determine NDK version from source.properties"
            )
        if len(revision) != 1:
            raise GetNdkVersionError("Too many Pkg.Revision lines in source.properties")

        (_, version) = revision[0].split("=")
        if not version:
            raise GetNdkVersionError(
                "Unexpected Pkg.Revision line in source.properties"
            )

        (major, minor, revision) = version.strip().split(".")
        if not major or not minor:
            raise GetNdkVersionError("Unexpected NDK version string: " + version)

        # source.properties contains a $MAJOR.$MINOR.$PATCH revision number,
        # but the more common nomenclature that Google uses is alphanumeric
        # version strings like "r20" or "r19c".  Convert the source.properties
        # notation into an alphanumeric string.
        int_minor = int(minor)
        alphas = "abcdefghijklmnop"
        ascii_minor = alphas[int_minor] if int_minor > 0 else ""
        human = "r%s%s" % (major, ascii_minor)
        return (major, minor, human)


def get_paths(os_name):
    mozbuild_path = Path(
        os.environ.get("MOZBUILD_STATE_PATH", Path("~/.mozbuild").expanduser())
    )
    sdk_path = Path(
        os.environ.get("ANDROID_SDK_HOME", mozbuild_path / f"android-sdk-{os_name}"),
    )
    ndk_path = Path(
        os.environ.get(
            "ANDROID_NDK_HOME", mozbuild_path / f"android-ndk-{NDK_VERSION}"
        ),
    )
    avd_home_path = Path(
        os.environ.get("ANDROID_AVD_HOME", mozbuild_path / "android-device" / "avd")
    )
    return mozbuild_path, sdk_path, ndk_path, avd_home_path


def sdkmanager_tool(sdk_path: Path):
    # sys.platform is win32 even if Python/Win64.
    sdkmanager = "sdkmanager.bat" if sys.platform.startswith("win") else "sdkmanager"
    return (
        sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager
    )


def avdmanager_tool(sdk_path: Path):
    # sys.platform is win32 even if Python/Win64.
    sdkmanager = "avdmanager.bat" if sys.platform.startswith("win") else "avdmanager"
    return (
        sdk_path / "cmdline-tools" / CMDLINE_TOOLS_VERSION_STRING / "bin" / sdkmanager
    )


def adb_tool(sdk_path: Path):
    adb = "adb.bat" if sys.platform.startswith("win") else "adb"
    return sdk_path / "platform-tools" / adb


def emulator_tool(sdk_path: Path):
    emulator = "emulator.bat" if sys.platform.startswith("win") else "emulator"
    return sdk_path / "emulator" / emulator


def ensure_android(
    os_name,
    os_arch,
    artifact_mode=False,
    ndk_only=False,
    system_images_only=False,
    emulator_only=False,
    avd_manifest_path: Optional[Path] = None,
    prewarm_avd=False,
    no_interactive=False,
    list_packages=False,
):
    """
    Ensure the Android SDK (and NDK, if `artifact_mode` is falsy) are
    installed.  If not, fetch and unpack the SDK and/or NDK from the
    given URLs.  Ensure the required Android SDK packages are
    installed.

    `os_name` can be 'linux', 'macosx' or 'windows'.
    """
    # The user may have an external Android SDK (in which case we
    # save them a lengthy download), or they may have already
    # completed the download. We unpack to
    # ~/.mozbuild/{android-sdk-$OS_NAME, android-ndk-$VER}.
    mozbuild_path, sdk_path, ndk_path, avd_home_path = get_paths(os_name)

    if os_name == "macosx":
        os_tag = "mac"
    elif os_name == "windows":
        os_tag = "win"
    else:
        os_tag = os_name

    sdk_url = "https://dl.google.com/android/repository/commandlinetools-{0}-{1}_latest.zip".format(  # NOQA: E501
        os_tag, CMDLINE_TOOLS_VERSION
    )
    ndk_url = android_ndk_url(os_name)
    bundletool_url = "https://github.com/google/bundletool/releases/download/{v}/bundletool-all-{v}.jar".format(  # NOQA: E501
        v=BUNDLETOOL_VERSION
    )

    ensure_android_sdk_and_ndk(
        mozbuild_path,
        os_name,
        sdk_path=sdk_path,
        sdk_url=sdk_url,
        ndk_path=ndk_path,
        ndk_url=ndk_url,
        bundletool_url=bundletool_url,
        artifact_mode=artifact_mode,
        ndk_only=ndk_only,
        emulator_only=emulator_only,
    )

    if ndk_only:
        return

    avd_manifest = None
    if avd_manifest_path is not None:
        with open(avd_manifest_path) as f:
            avd_manifest = json.load(f)
        # Some AVDs cannot be prewarmed in CI because they cannot run on linux64
        # (like the arm64 AVD).
        if "emulator_prewarm" in avd_manifest:
            prewarm_avd = prewarm_avd and avd_manifest["emulator_prewarm"]

    # We expect the |sdkmanager| tool to be at
    # ~/.mozbuild/android-sdk-$OS_NAME/tools/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING/bin/sdkmanager. # NOQA: E501
    ensure_android_packages(
        os_name,
        os_arch,
        sdkmanager_tool=sdkmanager_tool(sdk_path),
        emulator_only=emulator_only,
        system_images_only=system_images_only,
        avd_manifest=avd_manifest,
        no_interactive=no_interactive,
        list_packages=list_packages,
    )

    if emulator_only or system_images_only:
        return

    ensure_android_avd(
        avdmanager_tool=avdmanager_tool(sdk_path),
        adb_tool=adb_tool(sdk_path),
        emulator_tool=emulator_tool(sdk_path),
        avd_home_path=avd_home_path,
        sdk_path=sdk_path,
        no_interactive=no_interactive,
        avd_manifest=avd_manifest,
        prewarm_avd=prewarm_avd,
    )


def ensure_android_sdk_and_ndk(
    mozbuild_path: Path,
    os_name,
    sdk_path: Path,
    sdk_url,
    ndk_path: Path,
    ndk_url,
    bundletool_url,
    artifact_mode,
    ndk_only,
    emulator_only,
):
    """
    Ensure the Android SDK and NDK are found at the given paths.  If not, fetch
    and unpack the SDK and/or NDK from the given URLs into
    |mozbuild_path/{android-sdk-$OS_NAME,android-ndk-$VER}|.
    """

    # It's not particularly bad to overwrite the NDK toolchain, but it does take
    # a while to unpack, so let's avoid the disk activity if possible.  The SDK
    # may prompt about licensing, so we do this first.
    # Check for Android NDK only if we are not in artifact mode.
    if not artifact_mode and not emulator_only:
        install_ndk = True
        if ndk_path.is_dir():
            try:
                _, _, human = get_ndk_version(ndk_path)
                if human == NDK_VERSION:
                    print(ANDROID_NDK_EXISTS % ndk_path)
                    install_ndk = False
            except GetNdkVersionError:
                pass  # Just do the install.
        if install_ndk:
            # The NDK archive unpacks into a top-level android-ndk-$VER directory.
            install_mobile_android_sdk_or_ndk(ndk_url, mozbuild_path)

    if ndk_only:
        return

    # We don't want to blindly overwrite, since we use the
    # |sdkmanager| tool to install additional parts of the Android
    # toolchain.  If we overwrite, we lose whatever Android packages
    # the user may have already installed.
    if sdkmanager_tool(sdk_path).is_file():
        print(ANDROID_SDK_EXISTS % sdk_path)
    elif sdk_path.is_dir():
        raise NotImplementedError(ANDROID_SDK_TOO_OLD % sdk_path)
    else:
        # The SDK archive used to include a top-level
        # android-sdk-$OS_NAME directory; it no longer does so.  We
        # preserve the old convention to smooth detecting existing SDK
        # installations.
        cmdline_tools_path = mozbuild_path / f"android-sdk-{os_name}" / "cmdline-tools"
        install_mobile_android_sdk_or_ndk(sdk_url, cmdline_tools_path)
        # The tools package *really* wants to be in
        # <sdk>/cmdline-tools/$CMDLINE_TOOLS_VERSION_STRING
        (cmdline_tools_path / "cmdline-tools").rename(
            cmdline_tools_path / CMDLINE_TOOLS_VERSION_STRING
        )
        download(bundletool_url, mozbuild_path / "bundletool.jar")


def get_packages_to_install(packages_file_content, avd_manifest):
    packages = []
    packages += map(lambda package: package.strip(), packages_file_content)
    if avd_manifest is not None:
        packages += [avd_manifest["emulator_package"]]
    return packages


def ensure_android_avd(
    avdmanager_tool: Path,
    adb_tool: Path,
    emulator_tool: Path,
    avd_home_path: Path,
    sdk_path: Path,
    no_interactive=False,
    avd_manifest=None,
    prewarm_avd=False,
):
    """
    Use the given sdkmanager tool (like 'sdkmanager') to install required
    Android packages.
    """
    if avd_manifest is None:
        return

    avd_home_path.mkdir(parents=True, exist_ok=True)
    # The AVD needs this folder to boot, so make sure it exists here.
    (sdk_path / "platforms").mkdir(parents=True, exist_ok=True)

    avd_name = avd_manifest["emulator_avd_name"]
    args = [
        str(avdmanager_tool),
        "--verbose",
        "create",
        "avd",
        "--force",
        "--name",
        avd_name,
        "--package",
        avd_manifest["emulator_package"],
    ]

    if not no_interactive:
        subprocess.check_call(args)
        return

    # Flush outputs before running sdkmanager.
    sys.stdout.flush()
    env = os.environ.copy()
    env["ANDROID_AVD_HOME"] = str(avd_home_path)
    proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
    proc.communicate("no\n".encode("UTF-8"))

    retcode = proc.poll()
    if retcode:
        cmd = args[0]
        e = subprocess.CalledProcessError(retcode, cmd)
        raise e

    avd_path = avd_home_path / (str(avd_name) + ".avd")
    config_file_name = avd_path / "config.ini"

    print(f"Writing config at {config_file_name}")

    if config_file_name.is_file():
        with open(config_file_name, "a") as config:
            for key, value in avd_manifest["emulator_extra_config"].items():
                config.write("%s=%s\n" % (key, value))
    else:
        raise NotImplementedError(
            f"Could not find config file at {config_file_name}, something went wrong"
        )
    if prewarm_avd:
        run_prewarm_avd(adb_tool, emulator_tool, env, avd_name, avd_manifest)
    # When running in headless mode, the emulator does not run the cleanup
    # step, and thus doesn't delete lock files. On some platforms, left-over
    # lock files can cause the emulator to not start, so we remove them here.
    for lock_file in ["hardware-qemu.ini.lock", "multiinstance.lock"]:
        lock_file_path = avd_path / lock_file
        try:
            lock_file_path.unlink()
            print(f"Removed lock file {lock_file_path}")
        except OSError:
            # The lock file is not there, nothing to do.
            pass


def run_prewarm_avd(
    adb_tool: Path,
    emulator_tool: Path,
    env,
    avd_name,
    avd_manifest,
):
    """
    Ensures the emulator is fully booted to save time on future iterations.
    """
    args = [str(emulator_tool), "-avd", avd_name] + avd_manifest["emulator_extra_args"]

    # Flush outputs before running emulator.
    sys.stdout.flush()
    proc = subprocess.Popen(args, env=env)

    booted = False
    for i in range(100):
        boot_completed_cmd = [str(adb_tool), "shell", "getprop", "sys.boot_completed"]
        completed_proc = subprocess.Popen(
            boot_completed_cmd, env=env, stdout=subprocess.PIPE
        )
        try:
            out, err = completed_proc.communicate(timeout=30)
            boot_completed = out.decode("UTF-8").strip()
            print("sys.boot_completed = %s" % boot_completed)
            time.sleep(30)
            if boot_completed == "1":
                booted = True
                break
        except subprocess.TimeoutExpired:
            # Sometimes the adb command hangs, that's ok
            print("sys.boot_completed = Timeout")

    if not booted:
        raise NotImplementedError("Could not prewarm emulator")

    # Wait until the emulator completely shuts down
    subprocess.Popen([str(adb_tool), "emu", "kill"], env=env).wait()
    proc.wait()


def ensure_android_packages(
    os_name,
    os_arch,
    sdkmanager_tool: Path,
    emulator_only=False,
    system_images_only=False,
    avd_manifest=None,
    no_interactive=False,
    list_packages=False,
):
    """
    Use the given sdkmanager tool (like 'sdkmanager') to install required
    Android packages.
    """

    # This tries to install all the required Android packages.  The user
    # may be prompted to agree to the Android license.
    if system_images_only:
        packages_file_name = "android-system-images-packages.txt"
    elif emulator_only:
        packages_file_name = "android-emulator-packages.txt"
    else:
        packages_file_name = "android-packages.txt"

    packages_file_path = (Path(__file__).parent / packages_file_name).resolve()

    with open(packages_file_path) as packages_file:
        packages_file_content = packages_file.readlines()

    packages = get_packages_to_install(packages_file_content, avd_manifest)
    print(INSTALLING_ANDROID_PACKAGES % "\n".join(packages))

    args = [str(sdkmanager_tool)]
    if os_name == "macosx" and os_arch == "arm64":
        # Support for Apple Silicon is still in nightly
        args.append("--channel=3")
    args.extend(packages)

    # sdkmanager needs JAVA_HOME
    java_bin_path = ensure_java(os_name, os_arch)
    env = os.environ.copy()
    env["JAVA_HOME"] = str(java_bin_path.parent)

    if not no_interactive:
        subprocess.check_call(args, env=env)
        return

    # Flush outputs before running sdkmanager.
    sys.stdout.flush()
    sys.stderr.flush()
    # Emulate yes.  For a discussion of passing input to check_output,
    # see https://stackoverflow.com/q/10103551.
    yes = "\n".join(["y"] * 100).encode("UTF-8")
    proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
    proc.communicate(yes)

    retcode = proc.poll()
    if retcode:
        cmd = args[0]
        e = subprocess.CalledProcessError(retcode, cmd)
        raise e
    if list_packages:
        subprocess.check_call([str(sdkmanager_tool), "--list"])


def generate_mozconfig(os_name, artifact_mode=False):
    moz_state_dir, sdk_path, ndk_path, avd_home_path = get_paths(os_name)

    extra_lines = []
    if extra_lines:
        extra_lines.append("")

    if artifact_mode:
        template = MOBILE_ANDROID_ARTIFACT_MODE_MOZCONFIG_TEMPLATE
    else:
        template = MOBILE_ANDROID_MOZCONFIG_TEMPLATE

    kwargs = dict(
        sdk_path=sdk_path,
        ndk_path=ndk_path,
        avd_home_path=avd_home_path,
        moz_state_dir=moz_state_dir,
        extra_lines="\n".join(extra_lines),
    )
    return template.format(**kwargs).strip()


def android_ndk_url(os_name, ver=NDK_VERSION):
    # Produce a URL like
    # 'https://dl.google.com/android/repository/android-ndk-$VER-linux.zip
    base_url = "https://dl.google.com/android/repository/android-ndk"

    if os_name == "macosx":
        # |mach bootstrap| uses 'macosx', but Google uses 'darwin'.
        os_name = "darwin"

    return "%s-%s-%s.zip" % (base_url, ver, os_name)


def main(argv):
    import optparse  # No argparse, which is new in Python 2.7.
    import platform

    parser = optparse.OptionParser()
    parser.add_option(
        "-a",
        "--artifact-mode",
        dest="artifact_mode",
        action="store_true",
        help="If true, install only the Android SDK (and not the Android NDK).",
    )
    parser.add_option(
        "--jdk-only",
        dest="jdk_only",
        action="store_true",
        help="If true, install only the Java JDK.",
    )
    parser.add_option(
        "--ndk-only",
        dest="ndk_only",
        action="store_true",
        help="If true, install only the Android NDK (and not the Android SDK).",
    )
    parser.add_option(
        "--system-images-only",
        dest="system_images_only",
        action="store_true",
        help="If true, install only the system images for the AVDs.",
    )
    parser.add_option(
        "--no-interactive",
        dest="no_interactive",
        action="store_true",
        help="Accept the Android SDK licenses without user interaction.",
    )
    parser.add_option(
        "--emulator-only",
        dest="emulator_only",
        action="store_true",
        help="If true, install only the Android emulator (and not the SDK or NDK).",
    )
    parser.add_option(
        "--avd-manifest",
        dest="avd_manifest_path",
        help="If present, generate AVD from the manifest pointed by this argument.",
    )
    parser.add_option(
        "--prewarm-avd",
        dest="prewarm_avd",
        action="store_true",
        help="If true, boot the AVD and wait until completed to speed up subsequent boots.",
    )
    parser.add_option(
        "--list-packages",
        dest="list_packages",
        action="store_true",
        help="If true, list installed packages.",
    )

    options, _ = parser.parse_args(argv)

    if options.artifact_mode and options.ndk_only:
        raise NotImplementedError("Use no options to install the NDK and the SDK.")

    if options.artifact_mode and options.emulator_only:
        raise NotImplementedError("Use no options to install the SDK and emulators.")

    os_name = None
    if platform.system() == "Darwin":
        os_name = "macosx"
    elif platform.system() == "Linux":
        os_name = "linux"
    elif platform.system() == "Windows":
        os_name = "windows"
    else:
        raise NotImplementedError(
            "We don't support bootstrapping the Android SDK (or Android "
            "NDK) on {0} yet!".format(platform.system())
        )

    os_arch = platform.machine()

    if options.jdk_only:
        ensure_java(os_name, os_arch)
        return 0

    avd_manifest_path = (
        Path(options.avd_manifest_path) if options.avd_manifest_path else None
    )

    ensure_android(
        os_name,
        os_arch,
        artifact_mode=options.artifact_mode,
        ndk_only=options.ndk_only,
        system_images_only=options.system_images_only,
        emulator_only=options.emulator_only,
        avd_manifest_path=avd_manifest_path,
        prewarm_avd=options.prewarm_avd,
        no_interactive=options.no_interactive,
        list_packages=options.list_packages,
    )
    mozconfig = generate_mozconfig(os_name, options.artifact_mode)

    # |./mach bootstrap| automatically creates a mozconfig file for you if it doesn't
    # exist. However, here, we don't know where the "topsrcdir" is, and it's not worth
    # pulling in CommandContext (and its dependencies) to find out.
    # So, instead, we'll politely ask users to create (or update) the file themselves.
    suggestion = MOZCONFIG_SUGGESTION_TEMPLATE % ("$topsrcdir/mozconfig", mozconfig)
    print("\n" + suggestion)

    return 0


def ensure_java(os_name, os_arch):
    mozbuild_path, _, _, _ = get_paths(os_name)

    if os_name == "macosx":
        os_tag = "mac"
    else:
        os_tag = os_name

    if os_arch == "x86_64":
        arch = "x64"
    elif os_arch == "arm64":
        arch = "aarch64"
    else:
        arch = os_arch

    ext = "zip" if os_name == "windows" else "tar.gz"

    java_path = java_bin_path(os_name, mozbuild_path)
    if not java_path:
        raise NotImplementedError(f"Could not bootstrap java for {os_name}.")

    if not java_path.exists():
        # e.g. https://github.com/adoptium/temurin17-binaries/releases/
        #      download/jdk-17.0.7%2B7/OpenJDK17U-jre_x64_linux_hotspot_17.0.7_7.tar.gz
        java_url = (
            "https://github.com/adoptium/temurin{major}-binaries/releases/"
            "download/jdk-{major}.{minor}%2B{patch}/"
            "OpenJDK{major}U-jdk_{arch}_{os}_hotspot_{major}.{minor}_{patch}.{ext}"
        ).format(
            major=JAVA_VERSION_MAJOR,
            minor=JAVA_VERSION_MINOR,
            patch=JAVA_VERSION_PATCH,
            os=os_tag,
            arch=arch,
            ext=ext,
        )
        install_mobile_android_sdk_or_ndk(java_url, mozbuild_path / "jdk")
    return java_path


def java_bin_path(os_name, toolchain_path: Path):
    # Like jdk-17.0.7+7
    jdk_folder = "jdk-{major}.{minor}+{patch}".format(
        major=JAVA_VERSION_MAJOR, minor=JAVA_VERSION_MINOR, patch=JAVA_VERSION_PATCH
    )

    java_path = toolchain_path / "jdk" / jdk_folder

    if os_name == "macosx":
        return java_path / "Contents" / "Home" / "bin"
    elif os_name == "linux":
        return java_path / "bin"
    elif os_name == "windows":
        return java_path / "bin"
    else:
        return None


def locate_java_bin_path(host_kernel, toolchain_path: Union[str, Path]):
    if host_kernel == "WINNT":
        os_name = "windows"
    elif host_kernel == "Darwin":
        os_name = "macosx"
    elif host_kernel == "Linux":
        os_name = "linux"
    else:
        # Default to Linux
        os_name = "linux"
    path = java_bin_path(os_name, Path(toolchain_path))
    if not path.is_dir():
        raise JavaLocationFailedException(
            f"Could not locate Java at {path}, please run "
            "./mach bootstrap --no-system-changes"
        )
    return str(path)


class JavaLocationFailedException(Exception):
    pass


if __name__ == "__main__":
    sys.exit(main(sys.argv))
