// Copyright 2015 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.proto;

import static com.google.devtools.build.lib.collect.nestedset.Order.STABLE_ORDER;
import static com.google.devtools.build.lib.packages.Type.STRING;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Interner;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.actions.SymlinkAction;
import com.google.devtools.build.lib.analysis.config.CoreOptionConverters.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.concurrent.BlazeInterners;
import com.google.devtools.build.lib.packages.BuildType;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import javax.annotation.Nullable;

/**
 * Utility functions for proto_library and proto aspect implementations.
 */
public class ProtoCommon {
  private ProtoCommon() {
    throw new UnsupportedOperationException();
  }

  // Keep in sync with the migration label in
  // https://github.com/bazelbuild/rules_proto/blob/master/proto/defs.bzl.
  @VisibleForTesting
  public static final String PROTO_RULES_MIGRATION_LABEL =
      "__PROTO_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__";

  private static final Interner<PathFragment> PROTO_SOURCE_ROOT_INTERNER =
      BlazeInterners.newWeakInterner();

  /**
   * Returns a memory efficient version of the passed protoSourceRoot.
   *
   * <p>Any sizable proto graph will contain many {@code .proto} sources with the same source root.
   * We can't afford to have all of them represented as individual objects in memory.
   *
   * @param protoSourceRoot
   * @return
   */
  static PathFragment memoryEfficientProtoSourceRoot(PathFragment protoSourceRoot) {
    return PROTO_SOURCE_ROOT_INTERNER.intern(protoSourceRoot);
  }

  /**
   * Gets the direct sources of a proto library. If protoSources is not empty, the value is just
   * protoSources. Otherwise, it's the combined sources of all direct dependencies of the given
   * RuleContext.
   *
   * @param sources the direct proto sources.
   * @param deps the proto dependencies.
   * @return the direct sources of a proto library.
   */
  private static NestedSet<Artifact> computeStrictImportableProtosForDependents(
      ImmutableList<ProtoSource> sources, ImmutableList<ProtoInfo> deps) {

    if (sources.isEmpty()) {
      /* a proxy/alias library, return the sources of the direct deps */
      NestedSetBuilder<Artifact> builder = NestedSetBuilder.stableOrder();
      for (ProtoInfo provider : deps) {
        builder.addTransitive(provider.getStrictImportableProtoSourcesForDependents());
      }
      return builder.build();
    } else {
      return NestedSetBuilder.wrap(
          STABLE_ORDER, Iterables.transform(sources, ProtoSource::getSourceFile));
    }
  }

  private static NestedSet<ProtoSource> computeExportedProtos(
      ImmutableList<ProtoSource> directSources, ImmutableList<ProtoInfo> deps) {
    if (!directSources.isEmpty()) {
      return NestedSetBuilder.wrap(STABLE_ORDER, directSources);
    }

    /* a proxy/alias library, return the sources of the direct deps */
    NestedSetBuilder<ProtoSource> builder = NestedSetBuilder.stableOrder();
    for (ProtoInfo provider : deps) {
      builder.addTransitive(provider.getExportedSources());
    }
    return builder.build();
  }

  private static NestedSet<ProtoSource> computeTransitiveProtoSources(
      ImmutableList<ProtoInfo> protoDeps, Library library) {
    NestedSetBuilder<ProtoSource> result = NestedSetBuilder.naiveLinkOrder();
    result.addAll(library.getSources());
    for (ProtoInfo dep : protoDeps) {
      result.addTransitive(dep.getTransitiveSources());
    }
    return result.build();
  }

  /**
   * Collects all .proto files in this lib and its transitive dependencies.
   *
   * <p>Each import is a Artifact/Label pair.
   */
  private static NestedSet<Artifact> computeTransitiveProtoSourceArtifacts(
      ImmutableList<ProtoSource> sources, ImmutableList<ProtoInfo> deps) {
    NestedSetBuilder<Artifact> result = NestedSetBuilder.naiveLinkOrder();
    result.addAll(Iterables.transform(sources, ProtoSource::getSourceFile));
    for (ProtoInfo dep : deps) {
      result.addTransitive(dep.getTransitiveProtoSources());
    }
    return result.build();
  }

  private static NestedSet<Artifact> computeTransitiveOriginalProtoSources(
      ImmutableList<ProtoInfo> protoDeps, ImmutableList<Artifact> originalProtoSources) {
    NestedSetBuilder<Artifact> result = NestedSetBuilder.naiveLinkOrder();

    result.addAll(originalProtoSources);

    for (ProtoInfo dep : protoDeps) {
      result.addTransitive(dep.getOriginalTransitiveProtoSources());
    }

    return result.build();
  }

  static NestedSet<Artifact> computeDependenciesDescriptorSets(ImmutableList<ProtoInfo> deps) {
    return computeTransitiveDescriptorSets(null, deps);
  }

  private static NestedSet<Artifact> computeTransitiveDescriptorSets(
      @Nullable Artifact directDescriptorSet, ImmutableList<ProtoInfo> deps) {
    NestedSetBuilder<Artifact> result = NestedSetBuilder.stableOrder();
    if (directDescriptorSet != null) {
      result.add(directDescriptorSet);
    }
    for (ProtoInfo dep : deps) {
      result.addTransitive(dep.getTransitiveDescriptorSets());
    }
    return result.build();
  }

  /**
   * Returns all proto source roots in this lib ({@code currentProtoSourceRoot}) and in its
   * transitive dependencies.
   *
   * <p>Assumes {@code currentProtoSourceRoot} is the same as the package name.
   */
  private static NestedSet<String> computeTransitiveProtoSourceRoots(
      ImmutableList<ProtoInfo> protoDeps, String currentProtoSourceRoot) {
    NestedSetBuilder<String> protoPath = NestedSetBuilder.stableOrder();

    protoPath.add(currentProtoSourceRoot);
    for (ProtoInfo provider : protoDeps) {
      protoPath.addTransitive(provider.getTransitiveProtoSourceRoots());
    }

    return protoPath.build();
  }

  /** Basically a {@link Pair}. */
  private static final class Library {
    private final ImmutableList<ProtoSource> sources;
    private final PathFragment sourceRoot;

    Library(ImmutableList<ProtoSource> sources, PathFragment sourceRoot) {
      this.sources = sources;
      this.sourceRoot = sourceRoot;
    }

    public ImmutableList<ProtoSource> getSources() {
      return sources;
    }

    public PathFragment getSourceRoot() {
      return sourceRoot;
    }
  }

  /**
   * Returns the {@link Library} representing this <code>proto_library</code> rule.
   *
   * <p>Assumes that <code>strip_import_prefix</code> and <code>import_prefix</code> are unset and
   * that there are no generated .proto files that need to be compiled.
   */
  @Nullable
  public static Library createLibraryWithoutVirtualSourceRoot(
      PathFragment protoSourceRoot, ImmutableList<Artifact> directSources) {
    ImmutableList.Builder<ProtoSource> sources = ImmutableList.builder();
    for (Artifact protoSource : directSources) {
      sources.add(
          new ProtoSource(
              /* sourceFile */ protoSource,
              /* sourceRoot */ memoryEfficientProtoSourceRoot(
                  protoSourceRoot.getRelative(protoSource.getRoot().getExecPath()))));
    }
    return new Library(sources.build(), memoryEfficientProtoSourceRoot(protoSourceRoot));
  }

  private static PathFragment getPathFragmentAttribute(
      RuleContext ruleContext, String attributeName) {
    if (!ruleContext.attributes().has(attributeName)) {
      return null;
    }

    if (!ruleContext.attributes().isAttributeValueExplicitlySpecified(attributeName)) {
      return null;
    }

    String asString = ruleContext.attributes().get(attributeName, STRING);
    if (!PathFragment.isNormalized(asString)) {
      ruleContext.attributeError(
          attributeName, "should be normalized (without uplevel references or '.' path segments)");
      return null;
    }

    return PathFragment.create(asString);
  }

  /**
   * Returns the {@link Library} representing this <code>proto_library</code> rule if import prefix
   * munging is done. Otherwise, returns null.
   */
  private static Library createLibraryWithVirtualSourceRootMaybe(
      RuleContext ruleContext,
      ImmutableList<Artifact> protoSources,
      boolean generatedProtosInVirtualImports)
      throws InterruptedException {
    PathFragment importPrefixAttribute = getPathFragmentAttribute(ruleContext, "import_prefix");
    PathFragment stripImportPrefixAttribute =
        getPathFragmentAttribute(ruleContext, "strip_import_prefix");
    boolean hasGeneratedSources = false;

    if (generatedProtosInVirtualImports) {
      for (Artifact protoSource : protoSources) {
        if (!protoSource.isSourceArtifact()) {
          hasGeneratedSources = true;
          break;
        }
      }
    }

    if (importPrefixAttribute == null
        && stripImportPrefixAttribute == null
        && !hasGeneratedSources) {
      // Simple case, no magic required.
      return null;
    }

    PathFragment stripImportPrefix;
    if (stripImportPrefixAttribute == null) {
      stripImportPrefix = PathFragment.EMPTY_FRAGMENT;
    } else if (stripImportPrefixAttribute.isAbsolute()) {
      stripImportPrefix = stripImportPrefixAttribute.toRelative();
    } else {
      stripImportPrefix =
          ruleContext.getLabel().getPackageFragment().getRelative(stripImportPrefixAttribute);
    }

    PathFragment importPrefix =
        importPrefixAttribute != null ? importPrefixAttribute : PathFragment.EMPTY_FRAGMENT;
    if (importPrefix.isAbsolute()) {
      ruleContext.attributeError("import_prefix", "should be a relative path");
      return null;
    }

    PathFragment sourceRootPath = ruleContext.getUniqueDirectory("_virtual_imports");
    PathFragment sourceRoot =
        memoryEfficientProtoSourceRoot(
            ruleContext.getBinOrGenfilesDirectory().getExecPath().getRelative(sourceRootPath));

    ImmutableList.Builder<ProtoSource> sources = ImmutableList.builder();
    for (Artifact realProtoSource : protoSources) {
      if (!realProtoSource.getRepositoryRelativePath().startsWith(stripImportPrefix)) {
        ruleContext.ruleError(
            String.format(
                ".proto file '%s' is not under the specified strip prefix '%s'",
                realProtoSource.getExecPathString(), stripImportPrefix.getPathString()));
        continue;
      }
      Artifact virtualProtoSource =
          createVirtualProtoSource(
              ruleContext, realProtoSource, sourceRootPath, importPrefix, stripImportPrefix);
      sources.add(
          new ProtoSource(
              /* sourceFile */ virtualProtoSource,
              /* originalSourceFile */ realProtoSource,
              /* sourceRoot */ sourceRoot));
    }
    return new Library(sources.build(), sourceRoot);
  }

  private static Artifact createVirtualProtoSource(
      RuleContext ruleContext,
      Artifact realProtoSource,
      PathFragment sourceRootPath,
      PathFragment importPrefix,
      PathFragment stripImportPrefix) {
    PathFragment importPath =
        importPrefix.getRelative(
            realProtoSource.getRepositoryRelativePath().relativeTo(stripImportPrefix));

    Artifact virtualProtoSource =
        ruleContext.getDerivedArtifact(
            sourceRootPath.getRelative(importPath), ruleContext.getBinOrGenfilesDirectory());

    ruleContext.registerAction(
        SymlinkAction.toArtifact(
            ruleContext.getActionOwner(),
            realProtoSource,
            virtualProtoSource,
            "Symlinking virtual .proto sources for " + ruleContext.getLabel()));

    return virtualProtoSource;
  }

  /**
   * Check that .proto files in sources are from the same package. This is done to avoid clashes
   * with the generated sources.
   */
  public static void checkSourceFilesAreInSamePackage(RuleContext ruleContext) {
    // TODO(bazel-team): this does not work with filegroups that contain files
    // that are not in the package
    for (Label source : ruleContext.attributes().get("srcs", BuildType.LABEL_LIST)) {
      if (!isConfiguredTargetInSamePackage(ruleContext, source)) {
        ruleContext.attributeError(
            "srcs",
            "Proto source with label '" + source + "' must be in same package as consuming rule.");
      }
    }
  }

  private static boolean isConfiguredTargetInSamePackage(RuleContext ruleContext, Label source) {
    return ruleContext.getLabel().getPackageIdentifier().equals(source.getPackageIdentifier());
  }

  /**
   * Creates the {@link ProtoInfo} for the {@code proto_library} rule associated with {@code
   * ruleContext}.
   */
  public static ProtoInfo createProtoInfo(
      RuleContext ruleContext, boolean generatedProtosInVirtualImports)
      throws InterruptedException {
    ImmutableList<Artifact> originalDirectProtoSources =
        ruleContext.getPrerequisiteArtifacts("srcs").list();
    ImmutableList<ProtoInfo> deps =
        ImmutableList.copyOf(ruleContext.getPrerequisites("deps", ProtoInfo.PROVIDER));
    ImmutableList<ProtoInfo> exports =
        ImmutableList.copyOf(ruleContext.getPrerequisites("exports", ProtoInfo.PROVIDER));

    Library library =
        createLibraryWithVirtualSourceRootMaybe(
            ruleContext, originalDirectProtoSources, generatedProtosInVirtualImports);
    if (ruleContext.hasErrors()) {
      return null;
    }

    if (library == null) {
      PathFragment contextProtoSourceRoot =
          ruleContext
              .getLabel()
              .getRepository()
              .getExecPath(ruleContext.getConfiguration().isSiblingRepositoryLayout());
      library =
          createLibraryWithoutVirtualSourceRoot(contextProtoSourceRoot, originalDirectProtoSources);
    }

    ImmutableList<ProtoSource> directSources = library.getSources();
    PathFragment directProtoSourceRoot = library.getSourceRoot();
    NestedSet<ProtoSource> transitiveSources = computeTransitiveProtoSources(deps, library);
    NestedSet<Artifact> transitiveProtoSources =
        computeTransitiveProtoSourceArtifacts(directSources, deps);
    NestedSet<Artifact> transitiveOriginalProtoSources =
        computeTransitiveOriginalProtoSources(deps, originalDirectProtoSources);
    NestedSet<String> transitiveProtoSourceRoots =
        computeTransitiveProtoSourceRoots(deps, directProtoSourceRoot.getSafePathString());
    NestedSet<Artifact> strictImportableProtosForDependents =
        computeStrictImportableProtosForDependents(directSources, deps);
    Artifact directDescriptorSet =
        ruleContext.getGenfilesArtifact(
            ruleContext.getLabel().getName() + "-descriptor-set.proto.bin");
    NestedSet<Artifact> transitiveDescriptorSets =
        computeTransitiveDescriptorSets(directDescriptorSet, deps);

    // Layering checks.
    NestedSet<ProtoSource> exportedSources = computeExportedProtos(directSources, deps);
    NestedSet<ProtoSource> strictImportableSources =
        computeStrictImportableProtos(directSources, deps);
    NestedSet<ProtoSource> publicImportSources = computePublicImportProtos(exports);

    return new ProtoInfo(
        directSources,
        directProtoSourceRoot,
        transitiveSources,
        transitiveProtoSources,
        transitiveOriginalProtoSources,
        transitiveProtoSourceRoots,
        strictImportableProtosForDependents,
        directDescriptorSet,
        transitiveDescriptorSets,
        exportedSources,
        strictImportableSources,
        publicImportSources);
  }

  public static Runfiles.Builder createDataRunfilesProvider(
      final NestedSet<Artifact> transitiveProtoSources, RuleContext ruleContext) {
    // We assume that the proto sources will not have conflicting artifacts
    // with the same root relative path
    return new Runfiles.Builder(
            ruleContext.getWorkspaceName(), ruleContext.getConfiguration().legacyExternalRunfiles())
        .addTransitiveArtifactsWrappedInStableOrder(transitiveProtoSources);
  }

  // =================================================================
  // Protocol compiler invocation stuff.

  /**
   * Each language-specific initialization method will call this to construct Artifacts representing
   * its protocol compiler outputs.
   *
   * @param extension Remove ".proto" and replace it with this to produce the output file name, e.g.
   *     ".pb.cc".
   * @param pythonNames If true, replace hyphens in the file name with underscores, as required for
   *     Python modules.
   */
  public static ImmutableList<Artifact> getGeneratedOutputs(
      RuleContext ruleContext,
      ImmutableList<Artifact> protoSources,
      String extension,
      boolean pythonNames) {
    ImmutableList.Builder<Artifact> outputsBuilder = new ImmutableList.Builder<>();
    ArtifactRoot genfiles = ruleContext.getGenfilesDirectory();
    for (Artifact src : protoSources) {
      PathFragment srcPath =
          src.getOutputDirRelativePath(ruleContext.getConfiguration().isSiblingRepositoryLayout());
      if (pythonNames) {
        srcPath = srcPath.replaceName(srcPath.getBaseName().replace('-', '_'));
      }
      // Note that two proto_library rules can have the same source file, so this is actually a
      // shared action. NB: This can probably result in action conflicts if the proto_library rules
      // are not the same.
      outputsBuilder.add(
          ruleContext.getShareableArtifact(FileSystemUtils.replaceExtension(srcPath, extension),
              genfiles));
    }
    return outputsBuilder.build();
  }

  /**
   * Each language-specific initialization method will call this to construct Artifacts representing
   * its protocol compiler outputs.
   *
   * @param extension Remove ".proto" and replace it with this to produce the output file name, e.g.
   *     ".pb.cc".
   */
  public static ImmutableList<Artifact> getGeneratedOutputs(
      RuleContext ruleContext, ImmutableList<Artifact> protoSources, String extension) {
    return getGeneratedOutputs(ruleContext, protoSources, extension, false);
  }

  public static ImmutableList<Artifact> getGeneratedTreeArtifactOutputs(
      RuleContext ruleContext, ImmutableList<Artifact> protoSources, PathFragment directory) {
    ImmutableList.Builder<Artifact> outputsBuilder = new ImmutableList.Builder<>();
    if (!protoSources.isEmpty()) {
      ArtifactRoot genfiles =
          ruleContext
              .getConfiguration()
              .getGenfilesDirectory(ruleContext.getRule().getRepository());
      outputsBuilder.add(ruleContext.getTreeArtifact(directory, genfiles));
    }
    return outputsBuilder.build();
  }

  private static NestedSet<ProtoSource> computeStrictImportableProtos(
      ImmutableList<ProtoSource> directSources, ImmutableList<ProtoInfo> deps) {
    NestedSetBuilder<ProtoSource> builder = NestedSetBuilder.stableOrder();
    if (!directSources.isEmpty()) {
      builder.addAll(directSources);
      for (ProtoInfo provider : deps) {
        builder.addTransitive(provider.getExportedSources());
      }
    }
    return builder.build();
  }

  /**
   * Returns the .proto files that are the direct srcs of the exported dependencies of this rule.
   */
  private static NestedSet<ProtoSource> computePublicImportProtos(
      ImmutableList<ProtoInfo> exports) {
    NestedSetBuilder<ProtoSource> result = NestedSetBuilder.stableOrder();
    for (ProtoInfo export : exports) {
      result.addTransitive(export.getExportedSources());
    }
    return result.build();
  }

  /**
   * Decides whether this proto_library should check for strict proto deps.
   *
   * <p>Only takes into account the command-line flag --strict_proto_deps.
   */
  @VisibleForTesting
  public static boolean areDepsStrict(RuleContext ruleContext) {
    StrictDepsMode getBool = ruleContext.getFragment(ProtoConfiguration.class).strictProtoDeps();
    return getBool != StrictDepsMode.OFF && getBool != StrictDepsMode.DEFAULT;
  }
}
