#!/usr/bin/env python
#
# Copyright (C) 2017 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 os
import subprocess
import sys

import extract_lsdump


def _ExecuteCommand(cmd, **kwargs):
    """Executes a command and returns stdout.

    Args:
        cmd: A list of strings, the command to execute.
        **kwargs: The arguments passed to subprocess.Popen.

    Returns:
        A string, the stdout.
    """
    proc = subprocess.Popen(
        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
    stdout, stderr = proc.communicate()
    if proc.returncode:
        sys.exit("Command failed: %s\nstdout=%s\nstderr=%s" % (
                 cmd, stdout, stderr))
    if stderr:
        print("Warning: cmd=%s\nstdout=%s\nstderr=%s" % (cmd, stdout, stderr))
    return stdout.strip()


def GetBuildVariables(build_top_dir, abs_path, vars):
    """Gets values of variables from build config.

    Args:
        build_top_dir: The path to root directory of Android source.
        abs_path: A boolean, whether to convert the values to absolute paths.
        vars: A list of strings, the names of the variables.

    Returns:
        A list of strings which are the values of the variables.
    """
    cmd = ["build/soong/soong_ui.bash", "--dumpvars-mode",
           ("--abs-vars" if abs_path else "--vars"), " ".join(vars)]
    stdout = _ExecuteCommand(cmd, cwd=build_top_dir)
    print(stdout)
    return [line.split("=", 1)[1].strip("'") for line in stdout.splitlines()]


def _LoadLibraryNamesFromTxt(vndk_lib_list_file):
    """Loads VNDK and VNDK-SP library names from a VNDK library list.

    Args:
        vndk_lib_list_file: A file object of
                            build/make/target/product/vndk/current.txt

    Returns:
        A list of strings, the VNDK and VNDK-SP library names with vndk/vndk-sp
        directory prefixes.
    """
    tags = (
        ("VNDK-core: ", len("VNDK-core: "), False),
        ("VNDK-SP: ", len("VNDK-SP: "), False),
        ("VNDK-private: ", len("VNDK-private: "), True),
        ("VNDK-SP-private: ", len("VNDK-SP-private: "), True),
    )
    lib_names = set()
    lib_names_exclude = set()
    for line in vndk_lib_list_file:
        for tag, tag_len, is_exclude in tags:
            if line.startswith(tag):
                lib_name = line[tag_len:].strip()
                if is_exclude:
                    lib_names_exclude.add(lib_name)
                else:
                    lib_names.add(lib_name)
    return sorted(lib_names - lib_names_exclude)


def _LoadLibraryNames(file_names):
    """Loads library names from files.

    Each element in the input list can be a .so file or a .txt file. The
    returned list consists of:
    - The .so file names in the input list.
    - The libraries tagged with VNDK-core or VNDK-SP in the .txt file.

    Args:
        file_names: A list of strings, the library or text file names.

    Returns:
        A list of strings, the library names (probably with vndk/vndk-sp
        directory prefixes).
    """
    lib_names = []
    for file_name in file_names:
        if file_name.endswith(".so"):
            lib_names.append(file_name)
        else:
            with open(file_name, "r") as txt_file:
                lib_names.extend(_LoadLibraryNamesFromTxt(txt_file))
    return lib_names


def DumpAbi(output_dir, lib_names, lsdump_path):
    """Generates ABI dumps from library lsdumps.

    Args:
        output_dir: The output directory of dump files.
        lib_names: The names of the libraries to dump.
        lsdump_path: The path to the directory containing lsdumps.

    Returns:
        A list of strings, the libraries whose ABI dump fails to be created.
    """
    missing_dumps = []
    for lib_name in lib_names:
        dump_path = os.path.join(output_dir, lib_name + '.abi.dump')
        lib_lsdump_path = os.path.join(lsdump_path, lib_name + '.lsdump')
        if os.path.isfile(lib_lsdump_path + '.gz'):
            lib_lsdump_path += '.gz'

        print(lib_lsdump_path)
        try:
            extract_lsdump.ParseLsdumpFile(lib_lsdump_path, dump_path)
        except extract_lsdump.LsdumpError as e:
            missing_dumps.append(lib_name)
            print(e)
        else:
            print('Output: ' + dump_path)
        print('')
    return missing_dumps


def _GetTargetArchDir(target_arch, target_arch_variant):
    if target_arch == target_arch_variant:
        return target_arch
    return '{}_{}'.format(target_arch, target_arch_variant)


def _GetAbiBitnessFromArch(target_arch):
    arch_bitness = {
        'arm': '32',
        'arm64': '64',
        'x86': '32',
        'x86_64': '64',
    }
    return arch_bitness[target_arch]


def main():
    # Parse arguments
    description = (
        'Generates VTS VNDK ABI test abidumps from lsdump. '
        'Option values are read from build variables if no value is given. '
        'If none of the options are specified, then abidumps for target second '
        'arch are also generated.'
    )
    arg_parser = argparse.ArgumentParser(description=description)
    arg_parser.add_argument("file", nargs="*",
                            help="the libraries to dump. Each file can be "
                                 ".so or .txt. The text file can be found at "
                                 "build/make/target/product/vndk/current.txt.")
    arg_parser.add_argument("--output", "-o", action="store",
                            help="output directory for ABI reference dump. "
                                 "Default value is PLATFORM_VNDK_VERSION.")
    arg_parser.add_argument('--platform-vndk-version',
                            help='platform VNDK version. '
                                 'Default value is PLATFORM_VNDK_VERSION.')
    arg_parser.add_argument('--binder-bitness',
                            choices=['32', '64'],
                            help='bitness of binder interface. '
                                 'Default value is 32 if BINDER32BIT is set '
                                 'else is 64.')
    arg_parser.add_argument('--target-main-arch',
                            choices=['arm', 'arm64', 'x86', 'x86_64'],
                            help='main CPU arch of the device. '
                                 'Default value is TARGET_ARCH.')
    arg_parser.add_argument('--target-arch',
                            choices=['arm', 'arm64', 'x86', 'x86_64'],
                            help='CPU arch of the libraries to dump. '
                                 'Default value is TARGET_ARCH.')
    arg_parser.add_argument('--target-arch-variant',
                            help='CPU arch variant of the libraries to dump. '
                                 'Default value is TARGET_ARCH_VARIANT.')

    args = arg_parser.parse_args()

    build_top_dir = os.getenv("ANDROID_BUILD_TOP")
    if not build_top_dir:
        sys.exit("env var ANDROID_BUILD_TOP is not set")

    # If some options are not specified, read build variables as default values.
    if not all([args.platform_vndk_version,
                args.binder_bitness,
                args.target_main_arch,
                args.target_arch,
                args.target_arch_variant]):
        [platform_vndk_version,
         binder_32_bit,
         target_arch,
         target_arch_variant,
         target_2nd_arch,
         target_2nd_arch_variant] = GetBuildVariables(
            build_top_dir,
            False,
            ['PLATFORM_VNDK_VERSION',
             'BINDER32BIT',
             'TARGET_ARCH',
             'TARGET_ARCH_VARIANT',
             'TARGET_2ND_ARCH',
             'TARGET_2ND_ARCH_VARIANT']
        )
        target_main_arch = target_arch
        binder_bitness = '32' if binder_32_bit else '64'

    if args.platform_vndk_version:
        platform_vndk_version = args.platform_vndk_version

    if args.binder_bitness:
        binder_bitness = args.binder_bitness

    if args.target_main_arch:
        target_main_arch = args.target_main_arch

    if args.target_arch:
        target_arch = args.target_arch

    if args.target_arch_variant:
        target_arch_variant = args.target_arch_variant

    dump_targets = [(platform_vndk_version,
                     binder_bitness,
                     target_main_arch,
                     target_arch,
                     target_arch_variant)]

    # If all options are not specified, then also create dump for 2nd arch.
    if not any([args.platform_vndk_version,
                args.binder_bitness,
                args.target_main_arch,
                args.target_arch,
                args.target_arch_variant]):
        dump_targets.append((platform_vndk_version,
                             binder_bitness,
                             target_main_arch,
                             target_2nd_arch,
                             target_2nd_arch_variant))

    for target_tuple in dump_targets:
        (platform_vndk_version,
         binder_bitness,
         target_main_arch,
         target_arch,
         target_arch_variant) = target_tuple

        # Determine abi_bitness from target architecture
        abi_bitness = _GetAbiBitnessFromArch(target_arch)

        # Generate ABI dump from lsdump in TOP/prebuilts/abi-dumps
        lsdump_path = os.path.join(
            build_top_dir,
            'prebuilts',
            'abi-dumps',
            'vndk',
            platform_vndk_version,
            binder_bitness,
            _GetTargetArchDir(target_arch, target_arch_variant),
            'source-based')
        if not os.path.exists(lsdump_path):
            print('Warning: lsdump path does not exist: ' + lsdump_path)
            print('No abidump created.')
            continue

        output_dir = os.path.join(
            args.output if args.output else platform_vndk_version,
            'binder' + binder_bitness,
            target_main_arch,
            'lib64' if abi_bitness == '64' else 'lib')
        print("OUTPUT_DIR=" + output_dir)

        lib_names = _LoadLibraryNames(args.file)

        missing_dumps = DumpAbi(output_dir, lib_names, lsdump_path)

        if missing_dumps:
            print('Warning: Fails to create ABI dumps for libraries:')
            for lib_name in missing_dumps:
                print(lib_name)


if __name__ == "__main__":
    main()