// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.
// Copyright 2017 The Bazel Authors. All rights reserved.
package com.google.devtools.build.android;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;

import com.android.aapt.Resources.Reference;
import com.android.aapt.Resources.XmlNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.Converters.PathListConverter;
import com.google.devtools.build.android.aapt2.Aapt2ConfigOptions;
import com.google.devtools.build.android.aapt2.CompiledResources;
import com.google.devtools.build.android.aapt2.ResourceLinker;
import com.google.devtools.build.android.aapt2.StaticLibrary;
import com.google.devtools.build.android.resources.Visibility;
import com.google.devtools.build.android.xml.ProtoXmlUtils;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/** Performs resource validation and static linking for compiled android resources. */
public final class ValidateAndLinkResourcesAction {

  /** Action configuration options. */
  public static class Options extends OptionsBase {
    /**
     * TODO(b/64570523): Still used by blaze. Will be removed as part of the command line cleanup.
     *
     * @deprecated Use --resources.
     */
    @Option(
        name = "compiled",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.ExistingPathConverter.class,
        category = "input",
        help = "Compiled resources to link.",
        deprecationWarning = "Use --resources.")
    @Deprecated
    public Path compiled;

    @Option(
        name = "compiledDep",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.PathListConverter.class,
        category = "input",
        allowMultiple = true,
        help = "Compiled resource dependencies to link.")
    public List<Path> compiledDeps;

    /**
     * TODO(b/64570523): Still used by blaze. Will be removed as part of the command line cleanup.
     *
     * @deprecated Use --resources.
     */
    @Option(
        name = "manifest",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.ExistingPathConverter.class,
        category = "input",
        help = "Manifest for the library.",
        deprecationWarning = "Use --resources.")
    @Deprecated
    public Path manifest;

    @Option(
        name = "resources",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.CompiledResourcesConverter.class,
        category = "input",
        help = "Compiled resources to link.")
    public CompiledResources resources;

    // TODO(b/64570523): remove this flag when it is no longer used.
    @Option(
        name = "libraries",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.StaticLibraryListConverter.class,
        category = "input",
        help = "Static libraries to link against. Deprecated, use --library")
    public List<StaticLibrary> deprecatedLibraries;

    @Option(
        name = "library",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.StaticLibraryConverter.class,
        category = "input",
        allowMultiple = true,
        help = "Static libraries to link against.")
    public List<StaticLibrary> libraries;

    @Option(
        name = "packageForR",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        category = "input",
        help = "Package for the resources.")
    public String packageForR;

    @Option(
        name = "staticLibraryOut",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.PathConverter.class,
        category = "output",
        help = "Static library produced.")
    public Path staticLibraryOut;

    @Option(
        name = "rTxtOut",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        defaultValue = "null",
        converter = Converters.PathConverter.class,
        category = "output",
        help = "R.txt out.")
    public Path rTxtOut;

    @Option(
        name = "sourceJarOut",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        converter = Converters.PathConverter.class,
        defaultValue = "null",
        category = "output",
        help = "Generated java classes from the resources.")
    public Path sourceJarOut;

    @Option(
        name = "resourceApks",
        defaultValue = "null",
        category = "input",
        converter = PathListConverter.class,
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "List of reource only APK files to link against.")
    public List<Path> resourceApks;
  }

  public static void main(String[] args) throws Exception {
    OptionsParser optionsParser =
        OptionsParser.builder()
            .optionsClasses(
                Options.class, Aapt2ConfigOptions.class, ResourceProcessorCommonOptions.class)
            .argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
            .build();
    optionsParser.parse(args);

    Options options = optionsParser.getOptions(Options.class);
    final Aapt2ConfigOptions aapt2Options = optionsParser.getOptions(Aapt2ConfigOptions.class);
    final Profiler profiler = LoggingProfiler.createAndStart("manifest");

    try (ScopedTemporaryDirectory scopedTmp =
            new ScopedTemporaryDirectory("android_resources_tmp");
        ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) {
      CompiledResources resources =
          // TODO(b/64570523): Remove when the flags are standardized.
          Optional.ofNullable(options.resources)
              .orElseGet(
                  () ->
                      CompiledResources.from(
                          Preconditions.checkNotNull(options.compiled),
                          Preconditions.checkNotNull(options.manifest)))
              // We need to make the manifest aapt safe (w.r.t., placeholders). For now, just stub
              // it out.
              .processManifest(
                  manifest ->
                      AndroidManifest.parseFrom(manifest)
                          .writeDummyManifestForAapt(
                              scopedTmp.getPath().resolve("manifest-aapt-dummy"),
                              options.packageForR));
      ImmutableList<CompiledResources> includes =
          options.compiledDeps.stream().map(CompiledResources::from).collect(toImmutableList());
      profiler.recordEndOf("manifest").startTask("validate");

      // TODO(b/146663858): distinguish direct/transitive deps for "strict deps".
      // TODO(b/128711690): validate AndroidManifest.xml
      checkVisibilityOfResourceReferences(
          /* androidManifest= */ XmlNode.getDefaultInstance(), resources, includes);

      ImmutableList<StaticLibrary> resourceApks = ImmutableList.of();
      if (options.resourceApks != null) {
        resourceApks =
            options.resourceApks.stream().map(StaticLibrary::from).collect(toImmutableList());
      }

      profiler.recordEndOf("validate").startTask("link");
      ResourceLinker.create(aapt2Options.aapt2, executorService, scopedTmp.getPath())
          .profileUsing(profiler)
          // NB: these names are really confusing.
          //   .dependencies is meant for linking in android.jar
          //   .include is meant for regular dependencies
          //   .resourceApks is meant for linking runtime resource only apks
          .dependencies(Optional.ofNullable(options.deprecatedLibraries).orElse(options.libraries))
          .include(includes)
          .resourceApks(resourceApks)
          .buildVersion(aapt2Options.buildToolsVersion)
          .outputAsProto(aapt2Options.resourceTableAsProto)
          .linkStatically(resources)
          .copyLibraryTo(options.staticLibraryOut)
          .copySourceJarTo(options.sourceJarOut)
          .copyRTxtTo(options.rTxtOut);
      profiler.recordEndOf("link");
    }
  }

  /**
   * Validates that resources referenced from AndroidManifest.xml and res/ are visible.
   *
   * @param androidManifest AndroidManifest in protobuf format; while {@link CompiledResources} also
   *     contains a manifest, aapt2 requires it to be regular XML and only converts it to protobuf
   *     after "linking".
   * @param compiled resources of a compilation unit (i.e. an android_library rule)
   * @param deps resources from the transitive closure of the rule's "deps" attribute
   */
  static void checkVisibilityOfResourceReferences(
      XmlNode androidManifest, CompiledResources compiled, List<CompiledResources> deps) {
    ImmutableSet<String> privateResourceNames =
        deps.stream()
            .flatMap(
                cr ->
                    AndroidCompiledDataDeserializer.create(
                        /* includeFileContentsForValidation= */ false)
                        .read(DependencyInfo.UNKNOWN, cr.getZip())
                        .entrySet()
                        .stream())
            .filter(entry -> entry.getValue().getVisibility() == Visibility.PRIVATE)
            .map(entry -> ((FullyQualifiedName) entry.getKey()).asQualifiedReference())
            .collect(toImmutableSet());

    StringBuilder errorMessage = new StringBuilder();
    {
      ImmutableList<String> referencedPrivateResources =
          ProtoXmlUtils.getAllResourceReferences(androidManifest).stream()
              .map(Reference::getName)
              .filter(privateResourceNames::contains)
              .collect(toImmutableList());
      if (!referencedPrivateResources.isEmpty()) {
        errorMessage
            .append("AndroidManifest.xml references external private resources ")
            .append(referencedPrivateResources)
            .append('\n');
      }
    }

    for (Map.Entry<DataKey, DataResource> resource :
        AndroidCompiledDataDeserializer.create(/*includeFileContentsForValidation=*/ true)
            .read(DependencyInfo.UNKNOWN, compiled.getZip())
            .entrySet()) {
      ImmutableList<String> referencedPrivateResources =
          resource.getValue().getReferencedResources().stream()
              .map(Reference::getName)
              .filter(privateResourceNames::contains)
              .collect(toImmutableList());

      if (!referencedPrivateResources.isEmpty()) {
        errorMessage
            .append(resource.getKey().toPrettyString())
            .append(" (defined in ")
            .append(resource.getValue().source().getPath())
            .append(") references external private resources ")
            .append(referencedPrivateResources)
            .append('\n');
      }
    }

    if (errorMessage.length() != 0) {
      throw new UserException(errorMessage.toString());
    }
  }

  private ValidateAndLinkResourcesAction() {}
}
