// Copyright 2014 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 com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration.StrictDepsMode;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.rules.cpp.CppFileTypes;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * An object that captures the temporary state we need to pass around while the initialization hook
 * for a java rule is running.
 */
public class JavaTargetAttributes {

  private static void checkJar(Artifact classPathEntry) {
    if (!JavaSemantics.JAR.matches(classPathEntry.getFilename())) {
      throw new IllegalArgumentException("not a jar file: " + classPathEntry.prettyPrint());
    }
  }

  /** A builder class for JavaTargetAttributes. */
  public static class Builder {

    // The order of source files is important, and there must not be duplicates.
    // Unfortunately, there is no interface in Java that represents a collection
    // without duplicates that has a stable and deterministic iteration order,
    // but is not sorted according to a property of the elements. Thus we are
    // stuck with Set.
    private final Set<Artifact> sourceFiles = new LinkedHashSet<>();

    private final NestedSetBuilder<Artifact> runtimeClassPath = NestedSetBuilder.naiveLinkOrder();

    private final NestedSetBuilder<Artifact> compileTimeClassPath =
        NestedSetBuilder.naiveLinkOrder();

    private final List<Artifact> bootClassPath = new ArrayList<>();
    private final List<Artifact> sourcePath = new ArrayList<>();
    private final List<Artifact> nativeLibraries = new ArrayList<>();

    private JavaPluginInfoProvider plugins = JavaPluginInfoProvider.empty();

    private final Map<PathFragment, Artifact> resources = new LinkedHashMap<>();
    private final NestedSetBuilder<Artifact> resourceJars = NestedSetBuilder.stableOrder();
    private final List<Artifact> messages = new ArrayList<>();
    private final List<Artifact> instrumentationMetadata = new ArrayList<>();
    private final List<Artifact> sourceJars = new ArrayList<>();

    private final List<Artifact> classPathResources = new ArrayList<>();

    private final Set<Artifact> additionalOutputs = new LinkedHashSet<>();

    /** @see {@link #setStrictJavaDeps}. */
    private StrictDepsMode strictJavaDeps = StrictDepsMode.ERROR;

    private final NestedSetBuilder<Artifact> directJars = NestedSetBuilder.naiveLinkOrder();
    private final NestedSetBuilder<Artifact> compileTimeDependencyArtifacts =
        NestedSetBuilder.stableOrder();
    private Label targetLabel;
    @Nullable private String injectingRuleKind;

    private final NestedSetBuilder<Artifact> excludedArtifacts = NestedSetBuilder.naiveLinkOrder();

    private boolean built = false;

    private final JavaSemantics semantics;

    public Builder(JavaSemantics semantics) {
      this.semantics = semantics;
    }

    public Builder addSourceArtifacts(Iterable<Artifact> sourceArtifacts) {
      Preconditions.checkArgument(!built);
      for (Artifact srcArtifact : sourceArtifacts) {
        String srcFilename = srcArtifact.getExecPathString();
        if (JavaSemantics.SOURCE_JAR.matches(srcFilename)) {
          sourceJars.add(srcArtifact);
        } else if (JavaSemantics.PROPERTIES.matches(srcFilename)) {
          // output files of the message compiler
          resources.put(
              semantics.getDefaultJavaResourcePath(srcArtifact.getRootRelativePath()), srcArtifact);
        } else if (JavaSemantics.JAVA_SOURCE.matches(srcFilename)) {
          sourceFiles.add(srcArtifact);
        } else {
          // try specific cases from the semantics.
          semantics.addArtifactToJavaTargetAttribute(this, srcArtifact);
        }
      }
      return this;
    }

    public Builder addSourceFiles(Iterable<Artifact> sourceFiles) {
      Preconditions.checkArgument(!built);
      for (Artifact artifact : sourceFiles) {
        if (JavaSemantics.JAVA_SOURCE.matches(artifact.getFilename())) {
          this.sourceFiles.add(artifact);
        }
      }
      return this;
    }

    public Builder merge(JavaCompilationArgsProvider context) {
      Preconditions.checkArgument(!built);
      addCompileTimeClassPathEntries(context.getTransitiveCompileTimeJars());
      addRuntimeClassPathEntries(context.getRuntimeJars());
      addInstrumentationMetadataEntries(context.getInstrumentationMetadata());
      return this;
    }

    public Builder addSourceJars(Collection<Artifact> sourceJars) {
      Preconditions.checkArgument(!built);
      this.sourceJars.addAll(sourceJars);
      return this;
    }

    public Builder addSourceJar(Artifact sourceJar) {
      Preconditions.checkArgument(!built);
      this.sourceJars.add(sourceJar);
      return this;
    }

    public Builder addRuntimeClassPathEntry(Artifact classPathEntry) {
      Preconditions.checkArgument(!built);
      checkJar(classPathEntry);
      runtimeClassPath.add(classPathEntry);
      return this;
    }

    public Builder addRuntimeClassPathEntries(NestedSet<Artifact> classPathEntries) {
      Preconditions.checkArgument(!built);
      runtimeClassPath.addTransitive(classPathEntries);
      return this;
    }

    public Builder addCompileTimeClassPathEntries(NestedSet<Artifact> entries) {
      Preconditions.checkArgument(!built);
      compileTimeClassPath.addTransitive(entries);
      return this;
    }

    public Builder setTargetLabel(Label targetLabel) {
      Preconditions.checkArgument(!built);
      this.targetLabel = targetLabel;
      return this;
    }

    public Builder setInjectingRuleKind(@Nullable String injectingRuleKind) {
      Preconditions.checkArgument(!built);
      this.injectingRuleKind = injectingRuleKind;
      return this;
    }

    /**
     * Sets the bootclasspath to be passed to the Java compiler.
     *
     * <p>If this method is called, then the bootclasspath specified in this JavaTargetAttributes
     * instance overrides the default bootclasspath.
     */
    public Builder setBootClassPath(List<Artifact> jars) {
      Preconditions.checkArgument(!built);
      Preconditions.checkArgument(!jars.isEmpty());
      Preconditions.checkState(bootClassPath.isEmpty());
      bootClassPath.addAll(jars);
      return this;
    }

    /** Sets the sourcepath to be passed to the Java compiler. */
    public Builder setSourcePath(List<Artifact> artifacts) {
      Preconditions.checkArgument(!built);
      Preconditions.checkArgument(sourcePath.isEmpty());
      sourcePath.addAll(artifacts);
      return this;
    }

    public Builder addExcludedArtifacts(NestedSet<Artifact> toExclude) {
      Preconditions.checkArgument(!built);
      excludedArtifacts.addTransitive(toExclude);
      return this;
    }

    /**
     * Controls how strict the javac compiler will be in checking correct use of direct
     * dependencies.
     *
     * <p>Defaults to {@link StrictDepsMode#ERROR}.
     *
     * @param strictDeps one of WARN, ERROR or OFF
     */
    public Builder setStrictJavaDeps(StrictDepsMode strictDeps) {
      Preconditions.checkArgument(!built);
      strictJavaDeps = strictDeps;
      return this;
    }

    /**
     * In tandem with strictJavaDeps, directJars represents a subset of the compile-time classpath
     * jars that were provided by direct dependencies. When strictJavaDeps is OFF, there is no need
     * to provide directJars, and no extra information is passed to javac. When strictJavaDeps is
     * set to WARN or ERROR, the compiler command line will include extra flags to indicate the
     * warning/error policy and to map the classpath jars to direct or transitive dependencies,
     * using the information in directJars. The compiler command line will include an extra flag to
     * indicate which classpath jars are direct dependencies.
     */
    public Builder addDirectJars(NestedSet<Artifact> directJars) {
      Preconditions.checkArgument(!built);
      this.directJars.addTransitive(directJars);
      return this;
    }

    public Builder addCompileTimeDependencyArtifacts(NestedSet<Artifact> dependencyArtifacts) {
      Preconditions.checkArgument(!built);
      compileTimeDependencyArtifacts.addTransitive(dependencyArtifacts);
      return this;
    }

    public Builder addInstrumentationMetadataEntries(Iterable<Artifact> metadataEntries) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(instrumentationMetadata, metadataEntries);
      return this;
    }

    public Builder addNativeLibrary(Artifact nativeLibrary) {
      Preconditions.checkArgument(!built);
      String name = nativeLibrary.getFilename();
      if (CppFileTypes.INTERFACE_SHARED_LIBRARY.matches(name)) {
        return this;
      }
      if (!(CppFileTypes.SHARED_LIBRARY.matches(name)
          || CppFileTypes.VERSIONED_SHARED_LIBRARY.matches(name))) {
        throw new IllegalArgumentException("not a shared library :" + nativeLibrary.prettyPrint());
      }
      nativeLibraries.add(nativeLibrary);
      return this;
    }

    public Builder addNativeLibraries(Iterable<Artifact> nativeLibraries) {
      Preconditions.checkArgument(!built);
      for (Artifact nativeLibrary : nativeLibraries) {
        addNativeLibrary(nativeLibrary);
      }
      return this;
    }

    public Builder addMessages(Collection<Artifact> messages) {
      Preconditions.checkArgument(!built);
      this.messages.addAll(messages);
      return this;
    }

    public Builder addMessage(Artifact messagesArtifact) {
      Preconditions.checkArgument(!built);
      this.messages.add(messagesArtifact);
      return this;
    }

    public Builder addResource(PathFragment execPath, Artifact resource) {
      Preconditions.checkArgument(!built);
      this.resources.put(execPath, resource);
      return this;
    }

    public Builder addResourceJars(NestedSet<Artifact> resourceJars) {
      Preconditions.checkArgument(!built);
      this.resourceJars.addTransitive(resourceJars);
      return this;
    }

    public Builder addPlugin(JavaPluginInfoProvider plugins) {
      Preconditions.checkArgument(!built);
      this.plugins = JavaPluginInfoProvider.merge(this.plugins, plugins);
      return this;
    }

    public Builder addClassPathResources(List<Artifact> classPathResources) {
      Preconditions.checkArgument(!built);
      this.classPathResources.addAll(classPathResources);
      return this;
    }

    public Builder addClassPathResource(Artifact classPathResource) {
      Preconditions.checkArgument(!built);
      this.classPathResources.add(classPathResource);
      return this;
    }

    /** Adds additional outputs to this target's compile action. */
    public Builder addAdditionalOutputs(Iterable<Artifact> outputs) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(additionalOutputs, outputs);
      return this;
    }

    public JavaTargetAttributes build() {
      built = true;
      return new JavaTargetAttributes(
          sourceFiles,
          runtimeClassPath,
          compileTimeClassPath,
          bootClassPath,
          sourcePath,
          nativeLibraries,
          plugins,
          resources,
          resourceJars.build(),
          messages,
          sourceJars,
          classPathResources,
          additionalOutputs,
          directJars.build(),
          compileTimeDependencyArtifacts.build(),
          targetLabel,
          injectingRuleKind,
          excludedArtifacts,
          strictJavaDeps);
    }

    // TODO(bazel-team): delete the following methods - users should use the built
    // JavaTargetAttributes instead of accessing mutable state in the Builder.

    /** @deprecated prefer {@link JavaTargetAttributes#getSourceFiles} */
    @Deprecated
    public Set<Artifact> getSourceFiles() {
      return sourceFiles;
    }

    /** @deprecated prefer {@link JavaTargetAttributes#hasSources} */
    @Deprecated
    public boolean hasSources() {
      return !sourceFiles.isEmpty() || !sourceJars.isEmpty();
    }

    /**
     * @deprecated prefer to use a built {@link JavaTargetAttributes} instead of accessing mutable
     *     state in the {@link Builder}.
     */
    @Deprecated
    public boolean hasSourceFiles() {
      return !sourceFiles.isEmpty();
    }

    /**
     * @deprecated prefer to use a built {@link JavaTargetAttributes} instead of accessing mutable
     *     state in the {@link Builder}.
     */
    @Deprecated
    public List<Artifact> getInstrumentationMetadata() {
      return instrumentationMetadata;
    }

    /**
     * @deprecated prefer to use a built {@link JavaTargetAttributes} instead of accessing mutable
     *     state in the {@link Builder}.
     */
    @Deprecated
    public boolean hasSourceJars() {
      return !sourceJars.isEmpty();
    }
  }

  //
  // -------------------------- END OF BUILDER CLASS -------------------------
  //

  private final ImmutableSet<Artifact> sourceFiles;

  private final NestedSet<Artifact> runtimeClassPath;
  private final NestedSet<Artifact> compileTimeClassPath;

  private final ImmutableList<Artifact> bootClassPath;
  private final ImmutableList<Artifact> sourcePath;
  private final ImmutableList<Artifact> nativeLibraries;

  private final JavaPluginInfoProvider plugins;

  private final ImmutableMap<PathFragment, Artifact> resources;
  private final NestedSet<Artifact> resourceJars;

  private final ImmutableList<Artifact> messages;
  private final ImmutableList<Artifact> sourceJars;

  private final ImmutableList<Artifact> classPathResources;

  private final ImmutableSet<Artifact> additionalOutputs;

  private final NestedSet<Artifact> directJars;
  private final NestedSet<Artifact> compileTimeDependencyArtifacts;
  private final Label targetLabel;
  @Nullable private final String injectingRuleKind;

  private final NestedSet<Artifact> excludedArtifacts;
  private final StrictDepsMode strictJavaDeps;

  /** Constructor of JavaTargetAttributes. */
  private JavaTargetAttributes(
      Set<Artifact> sourceFiles,
      NestedSetBuilder<Artifact> runtimeClassPath,
      NestedSetBuilder<Artifact> compileTimeClassPath,
      List<Artifact> bootClassPath,
      List<Artifact> sourcePath,
      List<Artifact> nativeLibraries,
      JavaPluginInfoProvider plugins,
      Map<PathFragment, Artifact> resources,
      NestedSet<Artifact> resourceJars,
      List<Artifact> messages,
      List<Artifact> sourceJars,
      List<Artifact> classPathResources,
      Set<Artifact> additionalOutputs,
      NestedSet<Artifact> directJars,
      NestedSet<Artifact> compileTimeDependencyArtifacts,
      Label targetLabel,
      @Nullable String injectingRuleKind,
      NestedSetBuilder<Artifact> excludedArtifacts,
      StrictDepsMode strictJavaDeps) {
    this.sourceFiles = ImmutableSet.copyOf(sourceFiles);
    this.runtimeClassPath = runtimeClassPath.build();
    this.directJars = directJars;
    this.compileTimeClassPath =
        NestedSetBuilder.<Artifact>naiveLinkOrder()
            .addTransitive(directJars)
            .addTransitive(compileTimeClassPath.build())
            .build();
    this.bootClassPath = ImmutableList.copyOf(bootClassPath);
    this.sourcePath = ImmutableList.copyOf(sourcePath);
    this.nativeLibraries = ImmutableList.copyOf(nativeLibraries);
    this.plugins = plugins;
    this.resources = ImmutableMap.copyOf(resources);
    this.resourceJars = resourceJars;
    this.messages = ImmutableList.copyOf(messages);
    this.sourceJars = ImmutableList.copyOf(sourceJars);
    this.classPathResources = ImmutableList.copyOf(classPathResources);
    this.additionalOutputs = ImmutableSet.copyOf(additionalOutputs);
    this.compileTimeDependencyArtifacts = compileTimeDependencyArtifacts;
    this.targetLabel = targetLabel;
    this.injectingRuleKind = injectingRuleKind;
    this.excludedArtifacts = excludedArtifacts.build();
    this.strictJavaDeps = strictJavaDeps;
  }

  public NestedSet<Artifact> getDirectJars() {
    return directJars;
  }

  public NestedSet<Artifact> getCompileTimeDependencyArtifacts() {
    return compileTimeDependencyArtifacts;
  }

  public ImmutableList<Artifact> getSourceJars() {
    return sourceJars;
  }

  public Map<PathFragment, Artifact> getResources() {
    return resources;
  }

  public NestedSet<Artifact> getResourceJars() {
    return resourceJars;
  }

  public List<Artifact> getMessages() {
    return messages;
  }

  public ImmutableList<Artifact> getClassPathResources() {
    return classPathResources;
  }

  public ImmutableSet<Artifact> getAdditionalOutputs() {
    return additionalOutputs;
  }

  private NestedSet<Artifact> getExcludedArtifacts() {
    return excludedArtifacts;
  }

  /**
   * Returns the artifacts needed on the runtime classpath of this target.
   *
   * <p>See also {@link #getRuntimeClassPathForArchive()}.
   */
  public NestedSet<Artifact> getRuntimeClassPath() {
    return runtimeClassPath;
  }

  /**
   * Returns the classpath artifacts needed in a deploy jar for this target.
   *
   * <p>This excludes the artifacts made available by jars in the deployment environment.
   */
  public Iterable<Artifact> getRuntimeClassPathForArchive() {
    Iterable<Artifact> runtimeClasspath = getRuntimeClassPath();

    if (getExcludedArtifacts().isEmpty()) {
      return runtimeClasspath;
    } else {
      return Iterables.filter(
          runtimeClasspath, Predicates.not(Predicates.in(getExcludedArtifacts().toSet())));
    }
  }

  /**
   * Adds the classpath artifacts needed in a deploy jar for this target to the passed nested set.
   *
   * <p>This excludes the artifacts made available by jars in the deployment environment.
   */
  public void addRuntimeClassPathForArchiveToNestedSet(NestedSetBuilder<Artifact> builder) {
    NestedSet<Artifact> runtimeClasspath = getRuntimeClassPath();

    if (getExcludedArtifacts().isEmpty()) {
      builder.addTransitive(runtimeClasspath);
    } else {
      builder.addAll(
          Iterables.filter(
              runtimeClasspath, Predicates.not(Predicates.in(getExcludedArtifacts().toSet()))));
    }
  }

  public NestedSet<Artifact> getCompileTimeClassPath() {
    return compileTimeClassPath;
  }

  public ImmutableList<Artifact> getBootClassPath() {
    return bootClassPath;
  }

  public ImmutableList<Artifact> getSourcePath() {
    return sourcePath;
  }

  public JavaPluginInfoProvider plugins() {
    return plugins;
  }

  public ImmutableSet<Artifact> getSourceFiles() {
    return sourceFiles;
  }

  public List<Artifact> getNativeLibraries() {
    return nativeLibraries;
  }

  public boolean hasSources() {
    return !sourceFiles.isEmpty() || !sourceJars.isEmpty();
  }

  public boolean hasResources() {
    return !resources.isEmpty()
        || !messages.isEmpty()
        || !classPathResources.isEmpty()
        || !resourceJars.isEmpty();
  }

  public boolean hasMessages() {
    return !messages.isEmpty();
  }

  public Label getTargetLabel() {
    return targetLabel;
  }

  @Nullable
  public String getInjectingRuleKind() {
    return injectingRuleKind;
  }

  public StrictDepsMode getStrictJavaDeps() {
    return strictJavaDeps;
  }
}
