#!/usr/bin/env python3
#
# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Process Android resource directories to generate .resources.zip and R.txt
files."""

import argparse
import os
import shutil
import sys
import zipfile

from util import build_utils
from util import jar_info_utils
from util import md5_check
from util import resources_parser
from util import resource_utils
import action_helpers  # build_utils adds //build to sys.path.
import zip_helpers


def _ParseArgs(args):
    """Parses command line options.

    Returns:
      An options object as from argparse.ArgumentParser.parse_args()
    """
    parser = argparse.ArgumentParser(description=__doc__)
    action_helpers.add_depfile_arg(parser)

    parser.add_argument(
        '--res-sources-path',
        required=True,
        help='Path to a list of input resources for this target.',
    )

    parser.add_argument(
        '--r-text-in',
        help='Path to pre-existing R.txt. Its resource IDs override those found '
        'in the generated R.txt when generating R.java.',
    )

    parser.add_argument(
        '--allow-missing-resources',
        action='store_true',
        help='Do not fail if some resources exist in the res/ dir but are not '
        'listed in the sources.',
    )

    parser.add_argument(
        '--resource-zip-out',
        help='Path to a zip archive containing all resources from '
        '--resource-dirs, merged into a single directory tree.',
    )

    parser.add_argument(
        '--r-text-out', help='Path to store the generated R.txt file.'
    )

    parser.add_argument(
        '--strip-drawables',
        action="store_true",
        help='Remove drawables from the resources.',
    )

    options = parser.parse_args(args)

    with open(options.res_sources_path, encoding='utf-8') as f:
        options.sources = f.read().splitlines()
    options.resource_dirs = resource_utils.DeduceResourceDirsFromFileList(
        options.sources
    )

    return options


def _CheckAllFilesListed(resource_files, resource_dirs):
    resource_files = set(resource_files)
    missing_files = []
    for path, _ in resource_utils.IterResourceFilesInDirectories(resource_dirs):
        if path not in resource_files:
            missing_files.append(path)

    if missing_files:
        sys.stderr.write(
            'Error: Found files not listed in the sources list of '
            'the BUILD.gn target:\n'
        )
        for path in missing_files:
            sys.stderr.write('{}\n'.format(path))
        sys.exit(1)


def _ZipResources(resource_dirs, zip_path, ignore_pattern):
    # ignore_pattern is a string of ':' delimited list of globs used to ignore
    # files that should not be part of the final resource zip.
    files_to_zip = []
    path_info = resource_utils.ResourceInfoFile()
    for index, resource_dir in enumerate(resource_dirs):
        attributed_aar = None
        if not resource_dir.startswith('..'):
            aar_source_info_path = os.path.join(
                os.path.dirname(resource_dir), 'source.info'
            )
            if os.path.exists(aar_source_info_path):
                attributed_aar = jar_info_utils.ReadAarSourceInfo(
                    aar_source_info_path
                )

        for path, archive_path in resource_utils.IterResourceFilesInDirectories(
            [resource_dir], ignore_pattern
        ):
            attributed_path = path
            if attributed_aar:
                attributed_path = os.path.join(
                    attributed_aar, 'res', path[len(resource_dir) + 1 :]
                )
            # Use the non-prefixed archive_path in the .info file.
            path_info.AddMapping(archive_path, attributed_path)

            resource_dir_name = os.path.basename(resource_dir)
            archive_path = '{}_{}/{}'.format(
                index, resource_dir_name, archive_path
            )
            files_to_zip.append((archive_path, path))

    path_info.Write(zip_path + '.info')

    with zipfile.ZipFile(zip_path, 'w') as z:
        # This magic comment signals to resource_utils.ExtractDeps that this zip is
        # not just the contents of a single res dir, without the encapsulating res/
        # (like the outputs of android_generated_resources targets), but instead has
        # the contents of possibly multiple res/ dirs each within an encapsulating
        # directory within the zip.
        z.comment = resource_utils.MULTIPLE_RES_MAGIC_STRING
        zip_helpers.add_files_to_zip(files_to_zip, z)


def _GenerateRTxt(options, r_txt_path):
    """Generate R.txt file.

    Args:
      options: The command-line options tuple.
      r_txt_path: Locates where the R.txt file goes.
    """
    ignore_pattern = resource_utils.AAPT_IGNORE_PATTERN
    if options.strip_drawables:
        ignore_pattern += ':*drawable*'

    resources_parser.RTxtGenerator(
        options.resource_dirs, ignore_pattern
    ).WriteRTxtFile(r_txt_path)


def _OnStaleMd5(options):
    with resource_utils.BuildContext() as build:
        if options.sources and not options.allow_missing_resources:
            _CheckAllFilesListed(options.sources, options.resource_dirs)
        if options.r_text_in:
            r_txt_path = options.r_text_in
        else:
            _GenerateRTxt(options, build.r_txt_path)
            r_txt_path = build.r_txt_path

        if options.r_text_out:
            shutil.copyfile(r_txt_path, options.r_text_out)

        if options.resource_zip_out:
            ignore_pattern = resource_utils.AAPT_IGNORE_PATTERN
            if options.strip_drawables:
                ignore_pattern += ':*drawable*'
            _ZipResources(
                options.resource_dirs, options.resource_zip_out, ignore_pattern
            )


def main(args):
    args = build_utils.ExpandFileArgs(args)
    options = _ParseArgs(args)

    # Order of these must match order specified in GN so that the correct one
    # appears first in the depfile.
    output_paths = [
        options.resource_zip_out,
        options.resource_zip_out + '.info',
        options.r_text_out,
    ]

    input_paths = [options.res_sources_path]
    if options.r_text_in:
        input_paths += [options.r_text_in]

    # Resource files aren't explicitly listed in GN. Listing them in the depfile
    # ensures the target will be marked stale when resource files are removed.
    depfile_deps = []
    resource_names = []
    for resource_dir in options.resource_dirs:
        for resource_file in build_utils.FindInDirectory(resource_dir, '*'):
            # Don't list the empty .keep file in depfile. Since it doesn't end up
            # included in the .zip, it can lead to -w 'dupbuild=err' ninja errors
            # if ever moved.
            if not resource_file.endswith(os.path.join('empty', '.keep')):
                input_paths.append(resource_file)
                depfile_deps.append(resource_file)
            resource_names.append(os.path.relpath(resource_file, resource_dir))

    # Resource filenames matter to the output, so add them to strings as well.
    # This matters if a file is renamed but not changed (http://crbug.com/597126).
    input_strings = sorted(resource_names) + [
        options.strip_drawables,
    ]

    # Since android_resources targets like *__all_dfm_resources depend on java
    # targets that they do not need (in reality it only needs the transitive
    # resource targets that those java targets depend on), md5_check is used to
    # prevent outputs from being re-written when real inputs have not changed.
    md5_check.CallAndWriteDepfileIfStale(
        lambda: _OnStaleMd5(options),
        options,
        input_paths=input_paths,
        input_strings=input_strings,
        output_paths=output_paths,
        depfile_deps=depfile_deps,
    )


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