# 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.

import json
import logging
import os
import pathlib
import re
import shutil
import sys
import zipfile

sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'gyp'))

from util import build_utils
from util import md5_check
from util import resource_utils
import bundletool

# "system_apks" is "default", but with locale list and compressed dex.
_SYSTEM_MODES = ('system', 'system_apks')
BUILD_APKS_MODES = _SYSTEM_MODES + ('default', 'universal')
OPTIMIZE_FOR_OPTIONS = (
    'ABI',
    'SCREEN_DENSITY',
    'LANGUAGE',
    'TEXTURE_COMPRESSION_FORMAT',
)

_ALL_ABIS = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']


def _BundleMinSdkVersion(bundle_path):
    manifest_data = bundletool.RunBundleTool(
        ['dump', 'manifest', '--bundle', bundle_path]
    )
    return int(re.search(r'minSdkVersion.*?(\d+)', manifest_data).group(1))


def _CreateDeviceSpec(bundle_path, sdk_version, locales):
    if not sdk_version:
        # Target 32 by default since it supports sparse configs in resources.arsc.
        sdk_version = max(_BundleMinSdkVersion(bundle_path), 32)

    # Setting sdkVersion=minSdkVersion prevents multiple per-minSdkVersion .apk
    # files from being created within the .apks file.
    return {
        'screenDensity': 1000,  # Ignored since we don't split on density.
        'sdkVersion': sdk_version,
        'supportedAbis': _ALL_ABIS,  # Our .aab files are already split on abi.
        'supportedLocales': locales,
    }


def _FixBundleDexCompressionGlob(src_bundle, dst_bundle):
    # Modifies the BundleConfig.pb of the given .aab to add "classes*.dex" to the
    # "uncompressedGlob" list.
    with (
        zipfile.ZipFile(src_bundle) as src,
        zipfile.ZipFile(dst_bundle, 'w') as dst,
    ):
        for info in src.infolist():
            data = src.read(info)
            if info.filename == 'BundleConfig.pb':
                # A classesX.dex entry is added by create_app_bundle.py so that we can
                # modify it here in order to have it take effect. b/176198991
                data = data.replace(b'classesX.dex', b'classes*.dex')
            dst.writestr(info, data)


def GenerateBundleApks(
    bundle_path,
    bundle_apks_path,
    aapt2_path,
    keystore_path,
    keystore_password,
    keystore_alias,
    mode=None,
    local_testing=False,
    minimal=False,
    minimal_sdk_version=None,
    check_for_noop=True,
    system_image_locales=None,
    optimize_for=None,
):
    """Generate an .apks archive from a an app bundle if needed.

    Args:
      bundle_path: Input bundle file path.
      bundle_apks_path: Output bundle .apks archive path. Name must end with
        '.apks' or this operation will fail.
      aapt2_path: Path to aapt2 build tool.
      keystore_path: Path to keystore.
      keystore_password: Keystore password, as a string.
      keystore_alias: Keystore signing key alias.
      mode: Build mode, which must be either None or one of BUILD_APKS_MODES.
      minimal: Create the minimal set of apks possible (english-only).
      minimal_sdk_version: Use this sdkVersion when |minimal| or
        |system_image_locales| args are present.
      check_for_noop: Use md5_check to short-circuit when inputs have not changed.
      system_image_locales: Locales to package in the APK when mode is "system"
        or "system_compressed".
      optimize_for: Overrides split configuration, which must be None or
        one of OPTIMIZE_FOR_OPTIONS.
    """
    device_spec = None
    if minimal_sdk_version:
        assert minimal or system_image_locales, (
            'minimal_sdk_version is only used when minimal or system_image_locales '
            'is specified'
        )
    if minimal:
        # Measure with one language split installed. Use Hindi because it is
        # popular. resource_size.py looks for splits/base-hi.apk.
        # Note: English is always included since it's in base-master.apk.
        device_spec = _CreateDeviceSpec(
            bundle_path, minimal_sdk_version, ['hi']
        )
    elif mode in _SYSTEM_MODES:
        if not system_image_locales:
            raise Exception('system modes require system_image_locales')
        # Bundletool doesn't seem to understand device specs with locales in the
        # form of "<lang>-r<region>", so just provide the language code instead.
        locales = [
            resource_utils.ToAndroidLocaleName(l).split('-')[0]
            for l in system_image_locales
        ]
        device_spec = _CreateDeviceSpec(
            bundle_path, minimal_sdk_version, locales
        )

    def rebuild():
        logging.info('Building %s', bundle_apks_path)
        with build_utils.TempDir() as tmp_dir:
            tmp_apks_file = os.path.join(tmp_dir, 'output.apks')
            cmd_args = [
                'build-apks',
                '--aapt2=%s' % aapt2_path,
                '--output=%s' % tmp_apks_file,
                '--ks=%s' % keystore_path,
                '--ks-pass=pass:%s' % keystore_password,
                '--ks-key-alias=%s' % keystore_alias,
                '--overwrite',
            ]
            input_bundle_path = bundle_path
            # Work around bundletool not respecting uncompressDexFiles setting.
            # b/176198991
            if (
                mode not in _SYSTEM_MODES
                and _BundleMinSdkVersion(bundle_path) >= 27
            ):
                input_bundle_path = os.path.join(tmp_dir, 'system.aab')
                _FixBundleDexCompressionGlob(bundle_path, input_bundle_path)

            cmd_args += ['--bundle=%s' % input_bundle_path]

            if local_testing:
                cmd_args += ['--local-testing']

            if mode is not None:
                if mode not in BUILD_APKS_MODES:
                    raise Exception(
                        'Invalid mode parameter %s (should be in %s)'
                        % (mode, BUILD_APKS_MODES)
                    )
                if mode != 'system_apks':
                    cmd_args += ['--mode=' + mode]
                else:
                    # Specify --optimize-for to prevent language splits being created.
                    cmd_args += ['--optimize-for=device_tier']

            if optimize_for:
                if optimize_for not in OPTIMIZE_FOR_OPTIONS:
                    raise Exception(
                        'Invalid optimize_for parameter %s '
                        '(should be in %s)' % (mode, OPTIMIZE_FOR_OPTIONS)
                    )
                cmd_args += ['--optimize-for=' + optimize_for]

            if device_spec:
                data = json.dumps(device_spec)
                logging.debug('Device Spec: %s', data)
                spec_file = pathlib.Path(tmp_dir) / 'device.json'
                spec_file.write_text(data)
                cmd_args += ['--device-spec=' + str(spec_file)]

            if minimal:
                cmd_args += ['--enable-sparse-encoding']

            bundletool.RunBundleTool(cmd_args)

            shutil.move(tmp_apks_file, bundle_apks_path)

    if check_for_noop:
        input_paths = [
            bundle_path,
            bundletool.BUNDLETOOL_JAR_PATH,
            aapt2_path,
            keystore_path,
        ]
        input_strings = [
            keystore_password,
            keystore_alias,
            device_spec,
        ]
        if mode is not None:
            input_strings.append(mode)

        # Avoid rebuilding (saves ~20s) when the input files have not changed. This
        # is essential when calling the apk_operations.py script multiple times with
        # the same bundle (e.g. out/Debug/bin/monochrome_public_bundle run).
        md5_check.CallAndRecordIfStale(
            rebuild,
            input_paths=input_paths,
            input_strings=input_strings,
            output_paths=[bundle_apks_path],
        )
    else:
        rebuild()
