// Copyright 2015 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.lib.rules.android;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.CommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.SpawnAction.Builder;
import com.google.devtools.build.lib.analysis.config.CompilationMode;
import com.google.devtools.build.lib.rules.android.ResourceContainer.ResourceType;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import javax.annotation.Nullable;

/**
 * Helper class to generate Android aapt actions.
 */
public final class AndroidAaptActionHelper {
  private final RuleContext ruleContext;
  private final Artifact manifest;
  private final Collection<Artifact> inputs = new LinkedHashSet<>();
  private final Iterable<ResourceContainer> resourceContainers;

  /**
   * Constructs an instance of AndroidAaptActionHelper.
   *
   * @param ruleContext RuleContext for which the aapt actions
   *        will be generated.
   * @param manifest Artifact representing the AndroidManifest.xml that will be
   *        used to package resources.
   * @param resourceContainers The transitive closure of the ResourceContainers.
   */
  public AndroidAaptActionHelper(RuleContext ruleContext, Artifact manifest,
      Iterable<ResourceContainer> resourceContainers) {
    this.ruleContext = ruleContext;
    this.manifest = manifest;
    this.resourceContainers = resourceContainers;
  }

  /**
   * Returns the artifacts needed as inputs to process the resources/assets.
   */
  private Iterable<Artifact> getInputs() {
    if (inputs.isEmpty()) {
      inputs.add(AndroidSdkProvider.fromRuleContext(ruleContext).getAndroidJar());
      inputs.add(manifest);
      Iterables.addAll(inputs, Iterables.concat(Iterables.transform(resourceContainers,
          new Function<ResourceContainer, Iterable<Artifact>>() {
        @Override
        public Iterable<Artifact> apply(ResourceContainer container) {
          return container.getArtifacts();
        }
      })));
    }
    return inputs;
  }

  /**
   * Creates an Action that will invoke aapt to generate symbols java sources from the
   * resources and pack them into a srcjar.
   * @param javaSourcesJar Artifact to be generated by executing the action
   *        created by this method.
   * @param rTxt R.txt artifact to be generated by the aapt invocation.
   * @param javaPackage The package for which resources will be generated
   * @param inlineConstants whether or not constants in Java generated sources
   *        should be inlined by the compiler.
   */
  public void createGenerateResourceSymbolsAction(Artifact javaSourcesJar,
      Artifact rTxt, String javaPackage, boolean inlineConstants) {
    // java path from the provided package for the resources
    PathFragment javaPath = new PathFragment(javaPackage.replace('.', '/'));

    PathFragment javaResourcesRoot = javaSourcesJar.getRoot().getExecPath().getRelative(
        ruleContext.getUniqueDirectory("_java_resources"));

    String javaResources = javaResourcesRoot.getRelative(javaPath).getPathString();

    List<String> args = new ArrayList<>();
    args.add(javaSourcesJar.getExecPathString());
    args.add(javaResourcesRoot.getPathString());
    args.add(javaResources);

    args.addAll(createAaptCommand("javasrcs", javaSourcesJar, rTxt, inlineConstants,
        "-J", javaResources, "--custom-package", javaPackage, "--rename-manifest-package",
        javaPackage));
    final Builder builder = new SpawnAction.Builder()
        .addInputs(getInputs())
        .addTool(AndroidSdkProvider.fromRuleContext(ruleContext).getAapt())
        .setExecutable(
            ruleContext.getExecutablePrerequisite("$android_aapt_java_generator", Mode.HOST))
        .addOutput(javaSourcesJar)
        .setCommandLine(CommandLine.of(args, false))
        .useParameterFile(ParameterFileType.UNQUOTED)
        .setProgressMessage("Generating Java resources")
        .setMnemonic("AndroidAapt");
    if (rTxt != null) {
      builder.addOutput(rTxt);
    }
    ruleContext.registerAction(builder.build(ruleContext));
  }

  /**
   * Creates an Action that will invoke aapt to package the android resources
   * into an apk file.
   * @param apk Packed resources artifact to be generated by the aapt invocation.
   */
  public void createGenerateApkAction(Artifact apk, String renameManifestPackage,
      List<String> aaptOpts, List<String> densities) {
    List<String> args;

    if (renameManifestPackage == null) {
      args = createAaptCommand("apk", apk, null, true, "-F", apk.getExecPathString());
    } else {
      args = createAaptCommand("apk", apk, null, true, "-F",
          apk.getExecPathString(), "--rename-manifest-package", renameManifestPackage);
    }

    if (!densities.isEmpty()) {
      args.add(0, "start_densities");
      args.add(1, "end_densities");
      args.addAll(1, densities);
    }

    args.addAll(aaptOpts);

    ruleContext.registerAction(new SpawnAction.Builder()
        .addInputs(getInputs())
        .addTool(AndroidSdkProvider.fromRuleContext(ruleContext).getAapt())
        .addOutput(apk)
        .setExecutable(
            ruleContext.getExecutablePrerequisite("$android_aapt_apk_generator", Mode.HOST))
        .setCommandLine(CommandLine.of(args, false))
        .useParameterFile(ParameterFileType.UNQUOTED)
        .setProgressMessage("Generating apk resources")
        .setMnemonic("AndroidAapt")
        .build(ruleContext));
  }

  private List<String> createAaptCommand(String actionKind, Artifact output,
      Artifact rTxtOutput, boolean inlineConstants, String... outputArgs) {
    return createAaptCommand(
        actionKind, output, rTxtOutput, inlineConstants, Arrays.asList(outputArgs));
  }

  private List<String> createAaptCommand(String actionKind, Artifact output,
      Artifact rTxtOutput, boolean inlineConstants, Collection<String> outputArgs) {
    List<String> args = new ArrayList<>();
    args.addAll(getArgs(output, actionKind, ResourceType.RESOURCES));
    args.addAll(getArgs(output, actionKind, ResourceType.ASSETS));
    args.add(
        AndroidSdkProvider.fromRuleContext(ruleContext).getAapt().getExecutable().getExecPathString());
    args.add("package");
    args.addAll(outputArgs);
    // Allow overlay in case the same resource appears in more than one target,
    // giving precedence to the order in which they are found. This is needed
    // in order to support android library projects.
    args.add("--auto-add-overlay");
    if (rTxtOutput != null) {
      args.add("--output-text-symbols");
      args.add(rTxtOutput.getExecPath().getParentDirectory().getPathString());
    }
    if (!inlineConstants) {
      args.add("--non-constant-id");
    }
    if (ruleContext.getConfiguration().getCompilationMode() != CompilationMode.OPT) {
      args.add("--debug-mode");
    }
    args.add("-I");
    args.add(AndroidSdkProvider.fromRuleContext(ruleContext).getAndroidJar().getExecPathString());
    args.add("-M");
    args.add(manifest.getExecPathString());
    args.addAll(getResourcesDirArg(output, actionKind, "-S", ResourceType.RESOURCES));
    args.addAll(getResourcesDirArg(output, actionKind, "-A", ResourceType.ASSETS));
    return args;
  }

  @VisibleForTesting
  public List<String> getArgs(Artifact output, String actionKind, ResourceType resourceType) {
    PathFragment outputPath = outputPath(output, actionKind, resourceType);
    List<String> args = new ArrayList<>();
    args.add("start_" + resourceType.getAttribute());
    args.add(outputPath.getPathString());
    // First make sure path elements are unique
    Collection<String> paths = new LinkedHashSet<>();
    for (ResourceContainer container : resourceContainers) {
      for (Artifact artifact : container.getArtifacts(resourceType)) {
        paths.add(artifact.getExecPathString());
      }
    }
    // Than populate the command line
    for (String path : paths) {
      args.add(path);
      args.add(path);
    }
    args.add("end_" + resourceType.getAttribute());

    // if there is at least one artifact
    if (args.size() > 3) {
      return ImmutableList.copyOf(args);
    } else {
      return ImmutableList.<String>of();
    }
  }

  /**
   * Returns optional part of the <code>aapt</code> command line:
   * optionName output_path.
   */
  @VisibleForTesting
  public List<String> getResourcesDirArg(Artifact output, String actionKind, String resourceArg,
      ResourceType resourceType) {
    PathFragment outputPath = outputPath(output, actionKind, resourceType);
    List<String> dirArgs = new ArrayList<>();
    Collection<String> paths = new LinkedHashSet<>();
    // First make sure roots are unique
    for (ResourceContainer container : resourceContainers) {
      for (PathFragment root : container.getRoots(resourceType)) {
        paths.add(outputPath.getRelative(root).getPathString());
      }
    }
    // Than populate the command line
    for (String path : paths) {
      dirArgs.add(resourceArg);
      dirArgs.add(path);
    }

    return ImmutableList.copyOf(dirArgs);
  }

  /**
   * Returns a resourceType specific unique output location for the given action kind.
   */
  private PathFragment outputPath(Artifact output, String actionKind, ResourceType resourceType) {
    return output.getRoot().getExecPath().getRelative(ruleContext.getUniqueDirectory(
        "_" + resourceType.getAttribute() + "_" + actionKind));
  }

  public void createGenerateProguardAction(
      Artifact outputSpec, @Nullable Artifact outputMainDexSpec) {
    ImmutableList.Builder<Artifact> outputs = ImmutableList.builder();
    ImmutableList.Builder<String> aaptArgs = ImmutableList.builder();

    outputs.add(outputSpec);
    aaptArgs.add("-G").add(outputSpec.getExecPathString());

    if (outputMainDexSpec != null) {
      aaptArgs.add("-D").add(outputMainDexSpec.getExecPathString());
      outputs.add(outputMainDexSpec);
    }

    List<String> aaptCommand =
        createAaptCommand("proguard", outputSpec, null, true, aaptArgs.build());
    ruleContext.registerAction(new SpawnAction.Builder()
        .addInputs(getInputs())
        .addTool(AndroidSdkProvider.fromRuleContext(ruleContext).getAapt())
        .addOutputs(outputs.build())
        .setExecutable(
            ruleContext.getExecutablePrerequisite("$android_aapt_apk_generator", Mode.HOST))
        .setCommandLine(CommandLine.of(aaptCommand, false))
        .useParameterFile(ParameterFileType.UNQUOTED)
        .setProgressMessage("Generating Proguard configuration for resources")
        .setMnemonic("AndroidAapt")
        .build(ruleContext));
  }
}
