#!/usr/bin/env -S python3 -u
#  Copyright (C) 2024 The Android Open Source Project
#
#  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.

import argparse
import dataclasses
import os
from pathlib import Path
import shutil
import subprocess
import sys

# Formatted using: pyformat -s 4 --force_quote_type double -i scripts/gather-android-metalava-artifacts.py


def parse_command_line_args(args):
    """Define the command line options and the parse the command line arguments with them.

    :param args: the command line arguments :return: Return the result of
    parsing the command line arguments.
    """
    args_parser = argparse.ArgumentParser(
        description=(
            "Gather Android artifacts created by Metalava. This will build and"
            " then copy a set of targets to the output directory. If no custom"
            " targets are provided then a set of default ones will be provided"
            " that covers stub generation, signature to JDiff conversion and"
            " api-versions.xml file generation. The intent is that this would"
            " be run this before and after making the change to build and copy"
            " the artifacts into two separate directories that can then be"
            " compared to see what, if any, changes have happened. This does"
            " not check signature file generation as that can be easily checked"
            " by running `m checkapi`."
        ),
    )
    args_parser.add_argument(
        "directory",
        help="Output directory into which artifacts will be copied.",
    )
    args_parser.add_argument(
        "--stub-src-jar",
        action="append",
        help="Additional stub jar to gather",
    )

    return args_parser.parse_args(args)


@dataclasses.dataclass()
class Target:
    """A build target.

    This consists of a file to build and an optional output file name to use to
    differentiate build files that have the same base name.
    """

    # File to build and copy into the output directory.
    buildFile: str

    # Output file.
    outputFile: str = ""


def create_targets(files):
    """Create Target instances from a list of build files.

    :param files: The list of build files. :return: A list of Target instances.
    """
    return [x if isinstance(x, Target) else Target(x) for x in files]


def default_stub_files():
    """:return: A representative sample list of stub source jars generated by the Android build using Metalava"""
    return create_targets(
        [
            f"out/soong/.intermediates/frameworks/base/api/{x}/android_common/everything/{x}-stubs.srcjar"
            for x in [
                "api-stubs-docs-non-updatable",
                "system-api-stubs-docs-non-updatable",
                "test-api-stubs-docs-non-updatable",
                "module-lib-api-stubs-docs-non-updatable",
            ]
        ]
        + [
            # Use Metalava for host side API.
            "out/soong/.intermediates/tools/tradefederation/core/tradefed-doc-stubs/linux_glibc_common/everything/tradefed-doc-stubs-stubs.srcjar",
            # Exportable test API surface created by java_sdk_library
            "out/soong/.intermediates/frameworks/base/test-mock/android.test.mock.stubs.exportable.test.from-source/android_common/javac/android.test.mock.stubs.exportable.test.from-source.jar",
        ]
    )


def default_doc_stub_files():
    """:return: A representative sample list of doc stub source jars generated by the Android build using Metalava"""
    return create_targets(
        [
            f"out/soong/.intermediates/frameworks/base/api/{x}/android_common/exportable/{x}-stubs.srcjar"
            for x in [
                "framework-doc-stubs",
                "framework-doc-system-stubs",
            ]
        ]
        + [
            # Car doc stubs
            "out/soong/.intermediates/packages/services/Car/car-lib/car-doc-stubs/android_common/exportable/car-doc-stubs-stubs.srcjar",
        ]
    )


def default_sdk_values_metadata_zip_files():
    """:return: A representative sample list of sdk values zip files generated by the Android build using Metalava"""
    return create_targets([
        f"out/soong/.intermediates/frameworks/base/api/{x}/android_common/exportable/{x}-metadata.zip"
        for x in [
            "android-non-updatable-doc-stubs",
            "android-non-updatable-doc-stubs-system",
        ]
    ])


def default_annotation_zip_files():
    """:return: A representative sample list of annotation zip files generated by the Android build using Metalava"""
    return create_targets([
        "out/soong/.intermediates/frameworks/base/api/framework-doc-stubs/android_common/exportable/framework-doc-stubs_annotations.zip",
        "out/soong/.intermediates/frameworks/base/api/sdk-annotations.zip/android_common/gen/annotations.zip",
    ])


def default_api_version_files():
    """:return: A representative sample list of `api-versions.xml` files generated by the Android build using

    Metalava.
    """
    return create_targets([
        "out/soong/lint/api_versions_public.xml",
        "out/soong/lint/api_versions_system.xml",
        "out/soong/lint/api_versions_module_lib.xml",
        "out/soong/lint/api_versions_system_server.xml",
        Target(
            buildFile="out/soong/.intermediates/frameworks/base/api/api_versions_module_lib_complete/android_common/everything/api-versions.xml",
            outputFile="api_versions_module_lib_complete.xml",
        ),
        Target(
            buildFile="out/soong/.intermediates/frameworks/base/api/api_versions_system_server_complete/android_common/everything/api-versions.xml",
            outputFile="api_versions_system_server_complete.xml",
        ),
    ])


def default_jdiff_files():
    """:return: A representative sample list of JDiff files created by the Android build using Metalava."""
    return create_targets([
        # JDiff files generated from jar files.
        "out/soong/.intermediates/frameworks/base/api/android_stubs_current/android_common/api.xml",
        "out/soong/.intermediates/frameworks/base/api/android_system_stubs_current/android_common/system-api.xml",
        "out/soong/.intermediates/frameworks/base/api/android_module_lib_stubs_current/android_common/module-lib-api.xml",
        "out/soong/.intermediates/frameworks/base/api/android_system_server_stubs_current/android_common/system-server-api.xml",
        "out/soong/.intermediates/frameworks/base/api/android_test_stubs_current/android_common/test-api.xml",
        # JDiff files generated from txt files.
        "out/soong/.intermediates/packages/services/Car/car-lib/android.car-test-stubs-jdiff/gen/car-test-api.xml",
        "out/soong/.intermediates/packages/services/Car/car-lib/android.car-stubs-jdiff/gen/car-api.xml",
        "out/soong/.intermediates/packages/services/Car/car-lib/android.car-system-stubs-jdiff/gen/car-system-api.xml",
        ""  # JDiff files generated from txt files and then compressed using gzip..
        "out/soong/.intermediates/cts/tests/signature/api/cts-android-test-base-current-api-gz/gen/android-test-base-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-android-test-mock-current-api-gz/gen/android-test-mock-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-android-test-runner-current-api-gz/gen/android-test-runner-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-apache-http-legacy-current-api-gz/gen/apache-http-legacy-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-car-system-current-api-gz/gen/car-system-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-current-api-gz/android_common/gen/current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-system-current-api-gz/android_common/gen/system-current.api.gz",
        "out/soong/.intermediates/cts/tests/signature/api/cts-system-removed-api-gz/android_common/gen/system-removed.api.gz",
    ])


def default_dex_writer_files():
    """:return: A representative sample list of dex writer related files created by the Android build using Metalava."""
    return create_targets([
        # This is not actually a dex writer file but it contains information derived from lots of dex writer files so
        # any differences in the dex writer files will affect this file.
        "out/soong/hiddenapi/hiddenapi-flags.csv",
    ])


def default_custom_files(top):
    """Returns a representative sample list of custom files created by the Android build using a custom tool based, at

    least in part, on Metalava.
    :return: A list of custom files.
    """
    return create_targets([
        f"out/soong/.intermediates/frameworks/base/api/flag-api-mapping-{surface}/android_common/gen/{textproto}"
        for surface, textproto in [
            ("PublicApi", "flag_api_map.textproto"),
            ("SystemApi", "system_flag_api_map.textproto"),
            ("ModuleLibApi", "module_lib_flag_api_map.textproto"),
            ("SystemServerApi", "system_server_flag_api_map.textproto"),
        ]
    ])


def construct_target_list(args, top):
    """Generate a list of targets from the supplied arguments

    :param args: the command line arguments.
    :return: a non-empty list of Target instances to build.
    """
    targets = []
    # If any custom options have been provided then build them.
    if args.stub_src_jar:
        targets += create_targets(args.stub_src_jar)
    # If no custom targets have been provided then use the default targets.
    if not targets:
        targets += default_stub_files()
        targets += default_doc_stub_files()
        targets += default_sdk_values_metadata_zip_files()
        targets += default_annotation_zip_files()
        targets += default_jdiff_files()
        targets += default_api_version_files()
        targets += default_dex_writer_files()
        targets += default_custom_files(top)
    return targets


def main(args):
    top = os.environ.get("ANDROID_BUILD_TOP")
    if not top:
        raise Exception("ANDROID_BUILD_TOP not specified")
    os.chdir(top)

    # Parse command line arguments.
    args = parse_command_line_args(args)

    # Make sure that the output directory does not already exist.
    output_dir = Path(args.directory)
    if output_dir.exists():
        raise Exception(f"{output_dir} exists, please delete or change")

    # Construct the list of targets to build, using defaults where required.
    targets = construct_target_list(args, top)

    # Build the targets.
    build_targets([x.buildFile for x in targets])

    # Create the output directory and copy the targets into it.
    copy_targets(output_dir, targets)


def copy_targets(output_dir, targets):
    print(f"Making output directory: '{output_dir}'")
    os.mkdir(output_dir)
    print()
    print(f"Copying the following targets into '{output_dir}':")
    for t in targets:
        source = t.buildFile
        output_file = t.outputFile
        if output_file == "":
            print(f"    {source}")
            shutil.copy(source, output_dir)
        else:
            destination = f"{output_dir}/{output_file}"
            print(f"    {source} -> {output_file}")
            shutil.copy(source, destination)
    print()


def build_targets(targets):
    print()
    print("Building the following targets:")
    for t in targets:
        print(f"    {t}")
    print()
    subprocess.run(
        ["build/soong/soong_ui.bash", "--make-mode"] + targets, check=True
    )
    print()


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