// 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.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.collect.nestedset.Depset;
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.concurrent.ThreadSafety.Immutable;
import com.google.devtools.build.lib.packages.BuiltinProvider;
import com.google.devtools.build.lib.packages.NativeInfo;
import com.google.devtools.build.lib.rules.java.JavaPluginInfo.JavaPluginData;
import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider.JavaOutput;
import com.google.devtools.build.lib.starlarkbuildapi.java.JavaPluginInfoApi;
import java.util.ArrayList;
import java.util.List;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Sequence;
import net.starlark.java.eval.Starlark;

/** Provider for users of Java plugins. */
@Immutable
@AutoValue
public abstract class JavaPluginInfo extends NativeInfo
    implements JavaPluginInfoApi<Artifact, JavaPluginData, JavaOutput> {
  public static final String PROVIDER_NAME = "JavaPluginInfo";
  public static final Provider PROVIDER = new Provider();

  private static final JavaPluginInfo EMPTY =
      new AutoValue_JavaPluginInfo(
          ImmutableList.of(), JavaPluginData.empty(), JavaPluginData.empty());

  @Override
  public Provider getProvider() {
    return PROVIDER;
  }

  /** Provider class for {@link JavaPluginInfo} objects. */
  public static class Provider extends BuiltinProvider<JavaPluginInfo>
      implements JavaPluginInfoApi.Provider<JavaInfo> {
    private Provider() {
      super(PROVIDER_NAME, JavaPluginInfo.class);
    }

    @Override
    public JavaPluginInfoApi<Artifact, JavaPluginData, JavaOutput> javaPluginInfo(
        Sequence<?> runtimeDeps, Object processorClass, Object processorData, Boolean generatesApi)
        throws EvalException {
      NestedSet<String> processorClasses =
          processorClass == Starlark.NONE
              ? NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER)
              : NestedSetBuilder.create(Order.NAIVE_LINK_ORDER, (String) processorClass);
      JavaInfo javaInfos =
          JavaInfo.merge(
              Sequence.cast(runtimeDeps, JavaInfo.class, "runtime_deps"),
              /* mergeJavaOutputs= */ true,
              /* mergeSourceJars= */ true);

      NestedSet<Artifact> processorClasspath =
          javaInfos.getProvider(JavaCompilationArgsProvider.class).getRuntimeJars();

      final NestedSet<Artifact> data;
      if (processorData instanceof Depset) {
        data = Depset.cast(processorData, Artifact.class, "data");
      } else {
        data =
            NestedSetBuilder.wrap(
                Order.NAIVE_LINK_ORDER, Sequence.cast(processorData, Artifact.class, "data"));
      }

      return JavaPluginInfo.create(
          JavaPluginData.create(processorClasses, processorClasspath, data),
          generatesApi,
          javaInfos.getJavaOutputs());
    }
  }

  /** Information about a Java plugin, except for whether it generates API. */
  @Immutable
  @AutoValue
  public abstract static class JavaPluginData implements JavaPluginInfoApi.JavaPluginDataApi {
    private static final JavaPluginData EMPTY =
        create(
            NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER),
            NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER),
            NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER));

    public static JavaPluginData create(
        NestedSet<String> processorClasses,
        NestedSet<Artifact> processorClasspath,
        NestedSet<Artifact> data) {
      return new AutoValue_JavaPluginInfo_JavaPluginData(
          processorClasses, processorClasspath, data);
    }

    public static JavaPluginData empty() {
      return EMPTY;
    }

    public static JavaPluginData merge(Iterable<JavaPluginData> plugins) {
      NestedSetBuilder<String> processorClasses = NestedSetBuilder.naiveLinkOrder();
      NestedSetBuilder<Artifact> processorClasspath = NestedSetBuilder.naiveLinkOrder();
      NestedSetBuilder<Artifact> data = NestedSetBuilder.naiveLinkOrder();
      for (JavaPluginData plugin : plugins) {
        processorClasses.addTransitive(plugin.processorClasses());
        processorClasspath.addTransitive(plugin.processorClasspath());
        data.addTransitive(plugin.data());
      }
      return create(processorClasses.build(), processorClasspath.build(), data.build());
    }

    /**
     * Returns the class that should be passed to javac in order to run the annotation processor
     * this class represents.
     */
    public abstract NestedSet<String> processorClasses();

    /** Returns the artifacts to add to the runtime classpath for this plugin. */
    public abstract NestedSet<Artifact> processorClasspath();

    public abstract NestedSet<Artifact> data();

    @Override
    public Depset /*<FileApi>*/ getProcessorJarsForStarlark() {
      return Depset.of(Artifact.TYPE, processorClasspath());
    }

    @Override
    public Depset /*<String>*/ getProcessorClassesForStarlark() {
      return Depset.of(Depset.ElementType.STRING, processorClasses());
    }

    @Override
    public Depset /*<FileApi>*/ getProcessorDataForStarlark() {
      return Depset.of(Artifact.TYPE, data());
    }

    public boolean isEmpty() {
      return processorClasses().isEmpty() && processorClasspath().isEmpty() && data().isEmpty();
    }

    private JavaPluginData disableAnnotationProcessing() {
      return JavaPluginData.create(
          /* processorClasses= */ NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER),
          // Preserve the processor path, since it may contain Error Prone plugins which
          // will be service-loaded by JavaBuilder.
          processorClasspath(),
          // Preserve data, which may be used by Error Prone plugins.
          data());
    }
  }

  public static JavaPluginInfo mergeWithoutJavaOutputs(JavaPluginInfo a, JavaPluginInfo b) {
    return a.isEmpty() ? b : b.isEmpty() ? a : mergeWithoutJavaOutputs(ImmutableList.of(a, b));
  }

  public static JavaPluginInfo mergeWithoutJavaOutputs(Iterable<JavaPluginInfo> providers) {
    List<JavaPluginData> plugins = new ArrayList<>();
    List<JavaPluginData> apiGeneratingPlugins = new ArrayList<>();
    for (JavaPluginInfo provider : providers) {
      if (!provider.plugins().isEmpty()) {
        plugins.add(provider.plugins());
      }
      if (!provider.apiGeneratingPlugins().isEmpty()) {
        apiGeneratingPlugins.add(provider.apiGeneratingPlugins());
      }
    }
    if (plugins.isEmpty() && apiGeneratingPlugins.isEmpty()) {
      return JavaPluginInfo.empty();
    }
    return new AutoValue_JavaPluginInfo(
        ImmutableList.of(),
        JavaPluginData.merge(plugins),
        JavaPluginData.merge(apiGeneratingPlugins));
  }

  public static JavaPluginInfo create(
      JavaPluginData javaPluginData, boolean generatesApi, ImmutableList<JavaOutput> javaOutputs) {
    return new AutoValue_JavaPluginInfo(
        javaOutputs, javaPluginData, generatesApi ? javaPluginData : JavaPluginData.empty());
  }

  public static JavaPluginInfo empty() {
    return EMPTY;
  }

  public abstract JavaPluginData plugins();

  public abstract JavaPluginData apiGeneratingPlugins();

  /** Returns true if the provider has no associated data. */
  public boolean isEmpty() {
    // apiGeneratingPlugins is a subset of plugins, so checking if plugins is empty is sufficient
    return plugins().isEmpty();
  }

  /**
   * Returns true if the provider has any associated annotation processors (regardless of whether it
   * has a classpath or data).
   */
  public boolean hasProcessors() {
    // apiGeneratingPlugins is a subset of plugins, so checking if plugins is empty is sufficient
    return !plugins().processorClasses().isEmpty();
  }

  /**
   * Returns a copy of this {@code JavaPluginInfo} with annotation processors disabled. Does not
   * remove the processor path or data, which may be needed for Error Prone plugins.
   */
  public JavaPluginInfo disableAnnotationProcessing() {
    return JavaPluginInfo.create(
        plugins().disableAnnotationProcessing(), /* generatesApi= */ false, getJavaOutputs());
  }
}
