// 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.lib.rules.java;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
import com.google.devtools.build.lib.actions.ParamFileInfo;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Map;

/** Builds the action to package the resources for a Java rule into a jar. */
public class ResourceJarActionBuilder {
  public static final String MNEMONIC = "JavaResourceJar";

  private static final ParamFileInfo PARAM_FILE_INFO =
      ParamFileInfo.builder(ParameterFileType.SHELL_QUOTED).build();
  private static final ImmutableMap<String, String> EXECUTION_INFO =
      ImmutableMap.of(ExecutionRequirements.SUPPORTS_PATH_MAPPING, "1");

  private Artifact outputJar;
  private Map<PathFragment, Artifact> resources = ImmutableMap.of();
  private NestedSet<Artifact> resourceJars = NestedSetBuilder.emptySet(Order.STABLE_ORDER);
  private ImmutableList<Artifact> classpathResources = ImmutableList.of();
  private ImmutableList<Artifact> messages = ImmutableList.of();
  private JavaToolchainProvider javaToolchain;
  private NestedSet<Artifact> additionalInputs = NestedSetBuilder.emptySet(Order.STABLE_ORDER);

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setOutputJar(Artifact outputJar) {
    this.outputJar = outputJar;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setAdditionalInputs(NestedSet<Artifact> additionalInputs) {
    this.additionalInputs = additionalInputs;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setClasspathResources(
      ImmutableList<Artifact> classpathResources) {
    this.classpathResources = classpathResources;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setResources(Map<PathFragment, Artifact> resources) {
    this.resources = resources;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setResourceJars(NestedSet<Artifact> resourceJars) {
    this.resourceJars = resourceJars;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setTranslations(ImmutableList<Artifact> translations) {
    this.messages = translations;
    return this;
  }

  @CanIgnoreReturnValue
  public ResourceJarActionBuilder setJavaToolchain(JavaToolchainProvider javaToolchain) {
    this.javaToolchain = javaToolchain;
    return this;
  }

  public void build(JavaSemantics semantics, RuleContext ruleContext, String execGroup)
      throws RuleErrorException {
    checkNotNull(outputJar, "outputJar must not be null");
    checkNotNull(javaToolchain, "javaToolchain must not be null");
    checkNotNull(javaToolchain.getJavaRuntime(), "javabase must not be null");

    SpawnAction.Builder builder = new SpawnAction.Builder();
    CustomCommandLine.Builder command =
        CustomCommandLine.builder()
            .add("--normalize")
            .add("--dont_change_compression")
            .add("--exclude_build_data")
            .add("--no_strip_module_info") // bazelbuild/rules_java/issues/293
            .addExecPath("--output", outputJar);
    if (!resourceJars.isEmpty()) {
      command.addExecPaths("--sources", resourceJars);
    }
    addResources(command, semantics);
    if (!classpathResources.isEmpty()) {
      command.addExecPaths("--classpath_resources", classpathResources);
    }

    ruleContext.registerAction(
        builder
            .setExecutable(javaToolchain.getSingleJar())
            .useDefaultShellEnvironment()
            .addOutput(outputJar)
            .addInputs(messages)
            .addInputs(resources.values())
            .addTransitiveInputs(resourceJars)
            .addTransitiveInputs(additionalInputs)
            .addInputs(classpathResources)
            .addCommandLine(command.build(), PARAM_FILE_INFO)
            .setProgressMessage("Building Java resource jar")
            .setMnemonic(MNEMONIC)
            .setExecutionInfo(EXECUTION_INFO)
            .setExecGroup(execGroup)
            .build(ruleContext));
  }

  private void addResources(CustomCommandLine.Builder command, JavaSemantics semantics) {
    if (resources.isEmpty() && messages.isEmpty()) {
      return;
    }

    command.add("--resources");
    ImmutableList<Artifact> resourcesWithDefaultPath;

    // When all resources use the default path (common case), save memory by throwing away those
    // path fragments. The artifacts can be lazily converted to default-prefixed strings.
    if (resources.entrySet().stream()
        .allMatch(e -> e.getKey().equals(defaultResourcePath(e.getValue(), semantics)))) {
      resourcesWithDefaultPath =
          ImmutableList.<Artifact>builderWithExpectedSize(resources.size() + messages.size())
              .addAll(resources.values())
              .addAll(messages)
              .build();
    } else {
      command.addObject(
          Lists.transform(
              ImmutableList.copyOf(resources.entrySet()),
              e -> resourcePrefixedExecPath(e.getKey(), e.getValue())));
      resourcesWithDefaultPath = messages;
    }

    if (!resourcesWithDefaultPath.isEmpty()) {
      command.addObject(
          Lists.transform(
              resourcesWithDefaultPath,
              artifact ->
                  resourcePrefixedExecPath(defaultResourcePath(artifact, semantics), artifact)));
    }
  }

  private static PathFragment defaultResourcePath(Artifact artifact, JavaSemantics semantics) {
    return semantics.getDefaultJavaResourcePath(artifact.getRootRelativePath());
  }

  private static String resourcePrefixedExecPath(PathFragment resourcePath, Artifact artifact) {
    PathFragment execPath = artifact.getExecPath();
    return execPath.equals(resourcePath) ? execPath.getPathString() : execPath + ":" + resourcePath;
  }
}
