// Copyright 2016 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.objc;

import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.vfs.PathFragment;

/**
 * A provider that provides all protos and portable proto filters information in the transitive
 * closure of its dependencies that are needed for generating and compiling only one version of
 * proto files.
 *
 * <p>This provider also propagates the headers and search path for the protobuf runtime library.
 * This solves the issue that the proto bundling behavior (gather all the protos in the top target
 * and generate, compile and link only one version in the final binary) needs this data at the
 * linking target but the dependency on the runtime library is defined on the objc_proto_library.
 *
 * <p>Ideally we should make objc_binary (and other linking targets such as ios_extension_binary)
 * depend on the runtime library's ObjcProvider. Unfortunately this runs into a bug where Xcode
 * project generation cannot handle the dependency if it points to a label in an external workspace
 * (such as {@code @bazel_tools}). To avoid breaking Xcode project generation for all binary targets
 * all the time (whether protos are used or not), the dependency is specified on objc_proto_library
 * instead.
 */
public class ObjcProtoProvider implements TransitiveInfoProvider {

  private final NestedSet<NestedSet<Artifact>> protoGroups;
  private final NestedSet<Artifact> protobufHeaders;
  private final NestedSet<PathFragment> protobufHeaderSearchPaths;
  private final NestedSet<Artifact> portableProtoFilters;

  private ObjcProtoProvider(
      NestedSet<NestedSet<Artifact>> protoGroups,
      NestedSet<Artifact> portableProtoFilters,
      NestedSet<Artifact> protobufHeaders,
      NestedSet<PathFragment> protobufHeaderSearchPaths) {
    this.protoGroups = Preconditions.checkNotNull(protoGroups);
    this.portableProtoFilters = Preconditions.checkNotNull(portableProtoFilters);
    this.protobufHeaders = Preconditions.checkNotNull(protobufHeaders);
    this.protobufHeaderSearchPaths = Preconditions.checkNotNull(protobufHeaderSearchPaths);
  }

  /** Returns the set of all proto groups that the dependencies of this provider has seen. */
  public NestedSet<NestedSet<Artifact>> getProtoGroups() {
    return protoGroups;
  }

  /** Returns the header artifacts provided by the Protobuf library. */
  public NestedSet<Artifact> getProtobufHeaders() {
    return protobufHeaders;
  }

  /** Returns the header search paths provided by the Protobuf library. */
  public NestedSet<PathFragment> getProtobufHeaderSearchPaths() {
    return protobufHeaderSearchPaths;
  }

  /**
   * Returns the set of all the associated filters to the collected protos.
   */
  public NestedSet<Artifact> getPortableProtoFilters() {
    return portableProtoFilters;
  }

  /**
   * A builder for this context with an API that is optimized for collecting information from
   * several transitive dependencies.
   */
  public static final class Builder {
    private final NestedSetBuilder<NestedSet<Artifact>> protoGroups =
        NestedSetBuilder.stableOrder();
    private final NestedSetBuilder<Artifact> portableProtoFilters = NestedSetBuilder.stableOrder();
    private final NestedSetBuilder<Artifact> protobufHeaders = NestedSetBuilder.stableOrder();
    private final NestedSetBuilder<PathFragment> protobufHeaderSearchPaths =
        NestedSetBuilder.linkOrder();

    /**
     * Adds a proto group to be propagated. Each group represents a proto_library target and
     * contains protos to be built along with their transitive dependencies. We propagate protos as
     * groups because the grouping provides relationship information between the protos, which can
     * be used to limit the number of inputs to each proto generation action.
     */
    public Builder addProtoGroup(NestedSet<Artifact> protoGroup) {
      this.protoGroups.add(protoGroup);
      return this;
    }

    /** Adds the header artifacts provided by the Protobuf library. */
    public Builder addProtobufHeaders(NestedSet<Artifact> protobufHeaders) {
      this.protobufHeaders.addTransitive(protobufHeaders);
      return this;
    }

    /** Adds the header search paths provided by the Protobuf library. */
    public Builder addProtobufHeaderSearchPaths(NestedSet<PathFragment> protobufHeaderSearchPaths) {
      this.protobufHeaderSearchPaths.addTransitive(protobufHeaderSearchPaths);
      return this;
    }

    /**
     * Adds all the proto filters to the set of dependencies.
     */
    public Builder addPortableProtoFilters(NestedSet<Artifact> protoFilters) {
      this.portableProtoFilters.addTransitive(protoFilters);
      return this;
    }

    /**
     * Add all protos and filters from providers, and propagate them to any (transitive) dependers
     * on this ObjcProtoProvider.
     */
    public Builder addTransitive(Iterable<ObjcProtoProvider> providers) {
      for (ObjcProtoProvider provider : providers) {
        this.protoGroups.addTransitive(provider.getProtoGroups());
        this.portableProtoFilters.addTransitive(provider.getPortableProtoFilters());
        this.protobufHeaders.addTransitive(provider.getProtobufHeaders());
        this.protobufHeaderSearchPaths.addTransitive(provider.getProtobufHeaderSearchPaths());
      }
      return this;
    }

    /**
     * Whether this provider has any protos or filters.
     */
    public boolean isEmpty() {
      return protoGroups.isEmpty() && portableProtoFilters.isEmpty();
    }

    public ObjcProtoProvider build() {
      return new ObjcProtoProvider(
          protoGroups.build(),
          portableProtoFilters.build(),
          protobufHeaders.build(),
          protobufHeaderSearchPaths.build());
    }
  }
}
