#!/usr/bin/env python3

# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Creates size-info/*.info files used by SuperSize."""

import argparse
import collections
import os
import re
import sys
import zipfile

from util import build_utils
from util import jar_info_utils
import action_helpers  # build_utils adds //build to sys.path.


_AAR_VERSION_PATTERN = re.compile(r'/[^/]*?(\.aar/|\.jar/)')


def _RemoveDuplicatesFromList(source_list):
    return collections.OrderedDict.fromkeys(source_list).keys()


def _TransformAarPaths(path):
    # .aar files within //third_party/android_deps have a version suffix.
    # The suffix changes each time .aar files are updated, which makes size diffs
    # hard to compare (since the before/after have different source paths).
    # Rather than changing how android_deps works, we employ this work-around
    # to normalize the paths.
    # From: .../androidx_appcompat_appcompat/appcompat-1.1.0.aar/res/...
    #   To: .../androidx_appcompat_appcompat.aar/res/...
    # https://crbug.com/1056455
    if 'android_deps' not in path:
        return path
    return _AAR_VERSION_PATTERN.sub(r'\1', path)


def _MergeResInfoFiles(res_info_path, info_paths):
    # Concatenate them all.
    with action_helpers.atomic_output(res_info_path, encoding='utf-8') as dst:
        for p in info_paths:
            with open(p, encoding='utf-8') as src:
                dst.writelines(_TransformAarPaths(l) for l in src)


def _PakInfoPathsForAssets(assets):
    # Use "in" rather than "endswith" due to suffix. https://crbug.com/357131361
    return [f.split(':')[0] + '.info' for f in assets if '.pak' in f]


def _MergePakInfoFiles(merged_path, pak_infos):
    info_lines = set()
    for pak_info_path in pak_infos:
        with open(pak_info_path, 'r', encoding='utf-8') as src_info_file:
            info_lines.update(_TransformAarPaths(x) for x in src_info_file)
    # only_if_changed=False since no build rules depend on this as an input.
    with action_helpers.atomic_output(
        merged_path, only_if_changed=False, encoding='utf-8'
    ) as f:
        f.writelines(sorted(info_lines))


def _FullJavaNameFromClassFilePath(path):
    # Input:  base/android/java/src/org/chromium/Foo.class
    # Output: base.android.java.src.org.chromium.Foo
    if not path.endswith('.class'):
        return ''
    path = os.path.splitext(path)[0]
    parts = []
    while path:
        # Use split to be platform independent.
        head, tail = os.path.split(path)
        path = head
        parts.append(tail)
    parts.reverse()  # Package comes first
    return '.'.join(parts)


def _MergeJarInfoFiles(output, inputs):
    """Merge several .jar.info files to generate an .apk.jar.info.

    Args:
      output: output file path.
      inputs: List of .jar.info or .jar files.
    """
    info_data = {}
    for path in inputs:
        # For non-prebuilts: .jar.info files are written by compile_java.py and map
        # .class files to .java source paths.
        #
        # For prebuilts: No .jar.info file exists, we scan the .jar files here and
        # map .class files to the .jar.
        #
        # For .aar files: We look for a "source.info" file in the containing
        # directory in order to map classes back to the .aar (rather than mapping
        # them to the extracted .jar file).
        if path.endswith('.info'):
            info_data.update(jar_info_utils.ParseJarInfoFile(path))
        else:
            attributed_path = path
            if not path.startswith('..'):
                parent_path = os.path.dirname(path)
                # See if it's an sub-jar within the .aar.
                if os.path.basename(parent_path) == 'libs':
                    parent_path = os.path.dirname(parent_path)
                aar_source_info_path = os.path.join(parent_path, 'source.info')
                # source.info files exist only for jars from android_aar_prebuilt().
                # E.g. Could have an java_prebuilt() pointing to a generated .jar.
                if os.path.exists(aar_source_info_path):
                    attributed_path = jar_info_utils.ReadAarSourceInfo(
                        aar_source_info_path
                    )

            with zipfile.ZipFile(path) as zip_info:
                for name in zip_info.namelist():
                    fully_qualified_name = _FullJavaNameFromClassFilePath(name)
                    if fully_qualified_name:
                        info_data[fully_qualified_name] = _TransformAarPaths(
                            '{}/{}'.format(attributed_path, name)
                        )

    # only_if_changed=False since no build rules depend on this as an input.
    with action_helpers.atomic_output(
        output, encoding='utf-8', only_if_changed=False
    ) as f:
        jar_info_utils.WriteJarInfoFile(f, info_data)


def _FindJarInputs(jar_paths):
    ret = []
    for jar_path in jar_paths:
        jar_info_path = jar_path + '.info'
        if os.path.exists(jar_info_path):
            ret.append(jar_info_path)
        else:
            ret.append(jar_path)
    return ret


def main(args):
    args = build_utils.ExpandFileArgs(args)
    parser = argparse.ArgumentParser(description=__doc__)
    action_helpers.add_depfile_arg(parser)
    parser.add_argument(
        '--jar-info-path', required=True, help='Output .jar.info file'
    )
    parser.add_argument(
        '--pak-info-path', required=True, help='Output .pak.info file'
    )
    parser.add_argument(
        '--res-info-path', required=True, help='Output .res.info file'
    )
    parser.add_argument(
        '--jar-files',
        required=True,
        action='append',
        help='GN-list of .jar file paths',
    )
    parser.add_argument(
        '--assets',
        required=True,
        action='append',
        help='GN-list of files to add as assets in the form '
        '"srcPath:zipPath", where ":zipPath" is optional.',
    )
    parser.add_argument(
        '--uncompressed-assets',
        required=True,
        action='append',
        help='Same as --assets, except disables compression.',
    )
    parser.add_argument(
        '--in-res-info-path',
        required=True,
        action='append',
        help='Paths to .ap_.info files',
    )

    options = parser.parse_args(args)

    options.jar_files = action_helpers.parse_gn_list(options.jar_files)
    options.assets = action_helpers.parse_gn_list(options.assets)
    options.uncompressed_assets = action_helpers.parse_gn_list(
        options.uncompressed_assets
    )

    jar_inputs = _FindJarInputs(_RemoveDuplicatesFromList(options.jar_files))
    pak_inputs = _PakInfoPathsForAssets(
        options.assets + options.uncompressed_assets
    )
    res_inputs = options.in_res_info_path

    # Just create the info files every time. See https://crbug.com/1045024
    _MergeJarInfoFiles(options.jar_info_path, jar_inputs)
    _MergePakInfoFiles(options.pak_info_path, pak_inputs)
    _MergeResInfoFiles(options.res_info_path, res_inputs)

    all_inputs = jar_inputs + pak_inputs + res_inputs
    action_helpers.write_depfile(
        options.depfile, options.jar_info_path, inputs=all_inputs
    )


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