// 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.
package com.google.devtools.build.android;

import com.android.builder.core.DefaultManifestParser;
import com.android.utils.StdLogger;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException;
import com.google.devtools.build.android.AndroidResourceMerger.MergingException;
import com.google.devtools.build.android.AndroidResourceProcessor.AaptConfigOptions;
import com.google.devtools.build.android.Converters.ExistingPathConverter;
import com.google.devtools.build.android.Converters.PathConverter;
import com.google.devtools.build.android.Converters.SerializedAndroidDataConverter;
import com.google.devtools.build.android.Converters.SerializedAndroidDataListConverter;
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.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Provides an entry point for the compiled resource merging action.
 *
 * <p>This action merges compiled intermediate resource files from aapt2 and reports merge
 * conflicts. It also provides a merged manifest file to {@link ValidateAndLinkResourcesAction} and
 * builds the resource class jar for the lib jar
 */
public class AndroidCompiledResourceMergingAction {

  private static final StdLogger stdLogger = new StdLogger(StdLogger.Level.WARNING);

  private static final Logger logger =
      Logger.getLogger(AndroidCompiledResourceMergingAction.class.getName());

  /** Flag specifications for this action. */
  public static class Options extends OptionsBase {
    @Option(
        name = "primaryData",
        defaultValue = "null",
        converter = SerializedAndroidDataConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "The directory containing the primary resource directory. The contents will override"
                + " the contents of any other resource directories during merging."
                + " The expected format is "
                + SerializedAndroidData.EXPECTED_FORMAT)
    public SerializedAndroidData primaryData;

    @Option(
        name = "primaryManifest",
        defaultValue = "null",
        converter = ExistingPathConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path to primary resource's manifest file.")
    public Path primaryManifest;

    @Option(
        name = "data",
        defaultValue = "",
        converter = SerializedAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Transitive Data dependencies. These values will be used if not defined in the "
                + "primary resources. The expected format is "
                + SerializedAndroidData.EXPECTED_FORMAT
                + "[&...]")
    public List<SerializedAndroidData> transitiveData;

    @Option(
        name = "directData",
        defaultValue = "",
        converter = SerializedAndroidDataListConverter.class,
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Direct Data dependencies. These values will be used if not defined in the "
                + "primary resources. The expected format is "
                + SerializedAndroidData.EXPECTED_FORMAT
                + "[&...]")
    public List<SerializedAndroidData> directData;

    @Option(
        name = "classJarOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the generated java class jar.")
    public Path classJarOutput;

    @Option(
        name = "manifestOutput",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Path for the output processed AndroidManifest.xml.")
    public Path manifestOutput;

    @Option(
        name = "packageForR",
        defaultValue = "null",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "Custom java package to generate the R symbols files.")
    public String packageForR;

    @Option(
        name = "throwOnResourceConflict",
        defaultValue = "false",
        category = "config",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "If passed, resource merge conflicts will be treated as errors instead of warnings")
    public boolean throwOnResourceConflict;

    @Option(
        name = "targetLabel",
        defaultValue = "null",
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "A label to add to the output jar's manifest as 'Target-Label'")
    public String targetLabel;

    @Option(
        name = "injectingRuleKind",
        defaultValue = "null",
        category = "input",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help = "A string to add to the output jar's manifest as 'Injecting-Rule-Kind'")
    public String injectingRuleKind;

    @Option(
        name = "annotate_r_fields_from_transitive_deps",
        defaultValue = "false",
        documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "If enabled, annotates R with 'targetLabel' and transitive fields with their"
                + " respective labels.")
    public boolean annotateTransitiveFields;

    @Option(
        name = "rTxtOut",
        defaultValue = "null",
        converter = PathConverter.class,
        category = "output",
        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Path to where an R.txt file declaring potentially-used resources should be written.")
    public Path rTxtOut;
  }

  public static void main(String[] args) throws Exception {
    final Stopwatch timer = Stopwatch.createStarted();
    OptionsParser optionsParser =
        OptionsParser.builder()
            .optionsClasses(
                Options.class, AaptConfigOptions.class, ResourceProcessorCommonOptions.class)
            .argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
            .build();
    optionsParser.parseAndExitUponError(args);
    AaptConfigOptions aaptConfigOptions = optionsParser.getOptions(AaptConfigOptions.class);
    Options options = optionsParser.getOptions(Options.class);

    Preconditions.checkNotNull(options.primaryData);
    Preconditions.checkNotNull(options.primaryManifest);
    Preconditions.checkNotNull(options.manifestOutput);
    Preconditions.checkNotNull(options.classJarOutput);

    try (ScopedTemporaryDirectory scopedTmp =
            new ScopedTemporaryDirectory("android_resource_merge_tmp");
        ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) {
      Path tmp = scopedTmp.getPath();
      Path generatedSources = tmp.resolve("generated_resources");
      Path processedManifest = tmp.resolve("manifest-processed/AndroidManifest.xml");

      logger.fine(String.format("Setup finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

      String packageForR = options.packageForR;
      if (packageForR == null) {
        packageForR =
            Strings.nullToEmpty(
                new DefaultManifestParser(
                        options.primaryManifest.toFile(),
                        /* canParseManifest= */ () -> true,
                        /* isManifestFileRequired= */ true,
                        /* issueReporter= */ null)
                    .getPackage());
      }
      AndroidResourceClassWriter resourceClassWriter =
          AndroidResourceClassWriter.createWith(
              options.targetLabel, aaptConfigOptions.androidJar, generatedSources, packageForR);
      resourceClassWriter.setIncludeClassFile(true);
      resourceClassWriter.setIncludeJavaFile(false);
      resourceClassWriter.setAnnotateTransitiveFields(options.annotateTransitiveFields);

      PlaceholderRTxtWriter rTxtWriter =
          options.rTxtOut != null ? PlaceholderRTxtWriter.create(options.rTxtOut) : null;

      AndroidResourceMerger.mergeCompiledData(
          options.primaryData,
          options.primaryManifest,
          options.directData,
          options.transitiveData,
          resourceClassWriter,
          rTxtWriter,
          options.throwOnResourceConflict,
          executorService);
      logger.fine(String.format("Merging finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

      AndroidResourceOutputs.createClassJar(
          generatedSources, options.classJarOutput, options.targetLabel, options.injectingRuleKind);
      logger.fine(
          String.format("Create classJar finished at %sms", timer.elapsed(TimeUnit.MILLISECONDS)));

      // Until enough users with manifest placeholders migrate to the new manifest merger,
      // we need to replace ${applicationId} and ${packageName} with options.packageForR to make
      // the manifests compatible with the old manifest merger.
      processedManifest =
          AndroidManifestProcessor.with(stdLogger)
              .processLibraryManifest(
                  options.packageForR,
                  options.primaryManifest,
                  processedManifest,
                  optionsParser.getOptions(ResourceProcessorCommonOptions.class).logWarnings);

      Files.createDirectories(options.manifestOutput.getParent());
      Files.copy(processedManifest, options.manifestOutput);
    } catch (MergeConflictException e) {
      logger.log(Level.SEVERE, e.getMessage());
      throw e;
    } catch (MergingException e) {
      logger.log(Level.SEVERE, "Error during merging resources", e);
      throw e;
    } catch (AndroidManifestProcessor.ManifestProcessingException e) {
      throw e;
    } catch (Exception e) {
      logger.log(Level.SEVERE, "Unexpected", e);
      throw e;
    }
    logger.fine(String.format("Resources merged in %sms", timer.elapsed(TimeUnit.MILLISECONDS)));
  }
}
