// 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.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;
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.util.Preconditions;
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;

/**
 * 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 Set<Artifact> compileTimeJarFiles = 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> nativeLibraries = new ArrayList<>();

    private final Set<Artifact> processorPath = new LinkedHashSet<>();
    // Classpath directories can't be represented as artifacts (TreeArtifact isn't appropriate
    // here since all we need is a path string to apply to the command line).
    private final Set<PathFragment> processorPathDirs = new LinkedHashSet<>();
    private final Set<String> processorNames = new LinkedHashSet<>();

    private final Set<Artifact> apiGeneratingProcessorPath = new LinkedHashSet<>();
    private final Set<String> apiGeneratingProcessorNames = new LinkedHashSet<>();

    private final Map<PathFragment, Artifact> resources = new LinkedHashMap<>();
    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<>();

    private BuildConfiguration.StrictDepsMode strictJavaDeps =
        BuildConfiguration.StrictDepsMode.OFF;
    private final NestedSetBuilder<Artifact> directJars = NestedSetBuilder.naiveLinkOrder();
    private final List<Artifact> compileTimeDependencyArtifacts = new ArrayList<>();
    private String ruleKind;
    private Label targetLabel;

    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(JavaCompilationArgs context) {
      Preconditions.checkArgument(!built);
      addCompileTimeClassPathEntries(context.getCompileTimeJars());
      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 addCompileTimeJarFiles(Iterable<Artifact> jars) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(compileTimeJarFiles, jars);
      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 setRuleKind(String ruleKind) {
      Preconditions.checkArgument(!built);
      this.ruleKind = ruleKind;
      return this;
    }

    public Builder setTargetLabel(Label targetLabel) {
      Preconditions.checkArgument(!built);
      this.targetLabel = targetLabel;
      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;
    }

    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.
     *
     * @param strictDeps one of WARN, ERROR or OFF
     */
    public Builder setStrictJavaDeps(BuildConfiguration.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 extra flags are formatted like this (same for
     * --indirect_dependency): <pre>
     * --direct_dependency
     * foo/bar/lib.jar
     * //java/com/google/foo:bar
     * </pre>
     */
    public Builder addDirectJars(NestedSet<Artifact> directJars) {
      Preconditions.checkArgument(!built);
      this.directJars.addTransitive(directJars);
      return this;
    }

    public Builder addCompileTimeDependencyArtifacts(Iterable<Artifact> dependencyArtifacts) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(this.compileTimeDependencyArtifacts, 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 addProcessorName(String processor) {
      Preconditions.checkArgument(!built);
      processorNames.add(processor);
      return this;
    }

    public Builder addProcessorPath(Iterable<Artifact> jars) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(processorPath, jars);
      return this;
    }

    public Builder addProcessorPathDir(PathFragment dir) {
      Preconditions.checkArgument(!built);
      processorPathDirs.add(dir);
      return this;
    }

    public Builder addApiGeneratingProcessorName(String processor) {
      Preconditions.checkArgument(!built);
      apiGeneratingProcessorNames.add(processor);
      return this;
    }

    public Builder addApiGeneratingProcessorPath(Iterable<Artifact> jars) {
      Preconditions.checkArgument(!built);
      Iterables.addAll(apiGeneratingProcessorPath, jars);
      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,
          compileTimeJarFiles,
          runtimeClassPath,
          compileTimeClassPath,
          bootClassPath,
          nativeLibraries,
          processorPath,
          processorPathDirs,
          processorNames,
          apiGeneratingProcessorPath,
          apiGeneratingProcessorNames,
          resources,
          messages,
          sourceJars,
          classPathResources,
          additionalOutputs,
          directJars.build(),
          compileTimeDependencyArtifacts,
          ruleKind,
          targetLabel,
          excludedArtifacts,
          strictJavaDeps);
    }

    // TODO(bazel-team): Remove these 5 methods.
    @Deprecated
    public Set<Artifact> getSourceFiles() {
      return sourceFiles;
    }

    @Deprecated
    public boolean hasSourceFiles() {
      return !sourceFiles.isEmpty();
    }

    @Deprecated
    public List<Artifact> getInstrumentationMetadata() {
      return instrumentationMetadata;
    }

    @Deprecated
    public boolean hasSourceJars() {
      return !sourceJars.isEmpty();
    }
  }

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

  private final ImmutableSet<Artifact> sourceFiles;
  private final ImmutableSet<Artifact> compileTimeJarFiles;

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

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

  private final ImmutableSet<Artifact> processorPath;
  private final ImmutableSet<PathFragment> processorPathDirs;
  private final ImmutableSet<String> processorNames;

  private final ImmutableSet<Artifact> apiGeneratingProcessorPath;
  private final ImmutableSet<String> apiGeneratingProcessorNames;

  private final ImmutableMap<PathFragment, Artifact> resources;
  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 ImmutableList<Artifact> compileTimeDependencyArtifacts;
  private final String ruleKind;
  private final Label targetLabel;

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

  /** Constructor of JavaTargetAttributes. */
  private JavaTargetAttributes(
      Set<Artifact> sourceFiles,
      Set<Artifact> compileTimeJarFiles,
      NestedSetBuilder<Artifact> runtimeClassPath,
      NestedSetBuilder<Artifact> compileTimeClassPath,
      List<Artifact> bootClassPath,
      List<Artifact> nativeLibraries,
      Set<Artifact> processorPath,
      Set<PathFragment> processorPathDirs,
      Set<String> processorNames,
      Set<Artifact> apiGeneratingProcessorPath,
      Set<String> apiGeneratingProcessorNames,
      Map<PathFragment, Artifact> resources,
      List<Artifact> messages,
      List<Artifact> sourceJars,
      List<Artifact> classPathResources,
      Set<Artifact> additionalOutputs,
      NestedSet<Artifact> directJars,
      List<Artifact> compileTimeDependencyArtifacts,
      String ruleKind,
      Label targetLabel,
      NestedSetBuilder<Artifact> excludedArtifacts,
      BuildConfiguration.StrictDepsMode strictJavaDeps) {
    this.sourceFiles = ImmutableSet.copyOf(sourceFiles);
    this.compileTimeJarFiles = ImmutableSet.copyOf(compileTimeJarFiles);
    this.runtimeClassPath = runtimeClassPath.build();
    this.directJars = directJars;
    this.compileTimeClassPath =
        NestedSetBuilder.<Artifact>naiveLinkOrder()
            .addTransitive(directJars)
            .addTransitive(compileTimeClassPath.build())
            .build();
    this.bootClassPath = ImmutableList.copyOf(bootClassPath);
    this.nativeLibraries = ImmutableList.copyOf(nativeLibraries);
    this.processorPath = ImmutableSet.copyOf(processorPath);
    this.processorPathDirs = ImmutableSet.copyOf(processorPathDirs);
    this.processorNames = ImmutableSet.copyOf(processorNames);
    this.apiGeneratingProcessorPath = ImmutableSet.copyOf(apiGeneratingProcessorPath);
    this.apiGeneratingProcessorNames = ImmutableSet.copyOf(apiGeneratingProcessorNames);
    this.resources = ImmutableMap.copyOf(resources);
    this.messages = ImmutableList.copyOf(messages);
    this.sourceJars = ImmutableList.copyOf(sourceJars);
    this.classPathResources = ImmutableList.copyOf(classPathResources);
    this.additionalOutputs = ImmutableSet.copyOf(additionalOutputs);
    this.compileTimeDependencyArtifacts = ImmutableList.copyOf(compileTimeDependencyArtifacts);
    this.ruleKind = ruleKind;
    this.targetLabel = targetLabel;
    this.excludedArtifacts = excludedArtifacts.build();
    this.strictJavaDeps = strictJavaDeps;
  }

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

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

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

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

  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.
   *
   * See also {@link #getRuntimeClassPathForArchive()}.
   */
  public NestedSet<Artifact> getRuntimeClassPath() {
    return runtimeClassPath;
  }

  /**
   * Returns the classpath artifacts needed in a deploy jar for this target.
   *
   * 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())));
    }
  }

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

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

  public ImmutableSet<Artifact> getProcessorPath() {
    return processorPath;
  }

  public ImmutableSet<PathFragment> getProcessorPathDirs() {
    return processorPathDirs;
  }

  public Collection<Artifact> getApiGeneratingProcessorPath() {
    return apiGeneratingProcessorPath;
  }

  public ImmutableSet<String> getApiGeneratingProcessorNames() {
    return apiGeneratingProcessorNames;
  }

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

  public Set<Artifact> getCompileTimeJarFiles() {
    return compileTimeJarFiles;
  }

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

  public Collection<String> getProcessorNames() {
    return processorNames;
  }

  public boolean hasSourceFiles() {
    return !sourceFiles.isEmpty();
  }

  public boolean hasSourceJars() {
    return !sourceJars.isEmpty();
  }

  public boolean hasResources() {
    return !resources.isEmpty();
  }

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

  public boolean hasClassPathResources() {
    return !classPathResources.isEmpty();
  }

  public String getRuleKind() {
    return ruleKind;
  }

  public Label getTargetLabel() {
    return targetLabel;
  }

  public BuildConfiguration.StrictDepsMode getStrictJavaDeps() {
    return strictJavaDeps;
  }
}
