// 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.cpp;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ActionExecutionMetadata;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.packages.NoSuchPackageException;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import javax.annotation.Nullable;

/**
 * Scans source files to determine the bounding set of transitively referenced include files.
 *
 * <p>Note that include scanning is performance-critical code.
 */
public interface IncludeScanner {
  /**
   * Processes source files and a list of includes extracted from command line flags. Adds all found
   * files to the provided set {@code includes}.
   *
   * <p>The resulting set will include {@code mainSource} and {@code sources}. This has no real
   * impact in the case that we are scanning a single source file, since it is already known to be
   * an input. However, this is necessary when we have more than one source to scan from, for
   * example when building C++ modules. In that case we have one of two possibilities:
   *
   * <ol>
   *   <li>We compile a header module - there, the .cppmap file is the main source file (which we do
   *       not include-scan, as that would require an extra parser), and thus already in the input;
   *       all headers in the .cppmap file are our entry points for include scanning, but are not
   *       yet in the inputs - they get added here.
   *   <li>We compile an object file that uses a header module; currently using a header module
   *       requires all headers it can reference to be available for the compilation. The header
   *       module can reference headers that are not in the transitive include closure of the
   *       current translation unit. Therefore, {@link CppCompileAction} adds all headers specified
   *       transitively for compiled header modules as include scanning entry points, and we need to
   *       add the entry points to the inputs here.
   * </ol>
   *
   * <p>{@code mainSource} is the source file relative to which the {@code cmdlineIncludes} are
   * interpreted.
   *
   * <p>Additional dependencies may be requested via {@link
   * ActionExecutionContext#getEnvironmentForDiscoveringInputs}. If any dependency is not
   * immediately available, processing will be short-circuited. The caller should check {@link
   * com.google.devtools.build.skyframe.SkyFunction.Environment#valuesMissing} - if it returns
   * {@code true}, then include scanning did not complete and a skyframe restart is necessary.
   *
   * @throws NoSuchPackageException if hint collection fails due to package problems
   */
  void processAsync(
      Artifact mainSource,
      Collection<Artifact> sources,
      IncludeScanningHeaderData includeScanningHeaderData,
      List<String> cmdlineIncludes,
      Set<Artifact> includes,
      ActionExecutionMetadata actionExecutionMetadata,
      ActionExecutionContext actionExecutionContext,
      Artifact grepIncludes)
      throws IOException, NoSuchPackageException, ExecException, InterruptedException;

  /**
   * Holds pre-aggregated information that the {@link IncludeScanner} needs from the compilation
   * action.
   */
  final class IncludeScanningHeaderData {
    /**
     * Lookup table to find the {@link Artifact}s of generated files based on their {@link
     * Artifact#getExecPath}.
     */
    private final Map<PathFragment, Artifact> pathToDeclaredHeader;

    /**
     * The set of headers that are modular, i.e. are going to be read as a serialized AST rather
     * than from the textual source file. Depending on the implementation, it is likely that further
     * input discovery through such headers is unnecessary as the serialized AST is self-contained.
     */
    private final Set<Artifact> modularHeaders;

    /**
     * The list of "-isystem" include paths that should be used by the IncludeScanner for this
     * action. The compiler searches these paths ahead of the built-in system include paths, but
     * after all other paths. "-isystem" paths are treated the same as normal system directories.
     */
    private final List<PathFragment> systemIncludeDirs;

    /**
     * A list of "-include" inclusions specified explicitly on the command line of this action. The
     * compiler will imagine that these files have been quote-included at the beginning of each
     * source file.
     */
    private final List<String> cmdlineIncludes;

    /**
     * Tests whether the given artifact is a valid header even if it is not declared, i.e. a
     * transitive dependency. If null, assume all headers can be included.
     */
    @Nullable private final Predicate<Artifact> isValidUndeclaredHeader;

    public IncludeScanningHeaderData(
        Map<PathFragment, Artifact> pathToDeclaredHeader,
        Set<Artifact> modularHeaders,
        List<PathFragment> systemIncludeDirs,
        List<String> cmdlineIncludes,
        @Nullable Predicate<Artifact> isValidUndeclaredHeader) {
      this.pathToDeclaredHeader = pathToDeclaredHeader;
      this.modularHeaders = modularHeaders;
      this.systemIncludeDirs = systemIncludeDirs;
      this.cmdlineIncludes = cmdlineIncludes;
      this.isValidUndeclaredHeader = isValidUndeclaredHeader;
    }

    public boolean isDeclaredHeader(PathFragment header) {
      return pathToDeclaredHeader.containsKey(header);
    }

    public Artifact getHeaderArtifact(PathFragment header) {
      return pathToDeclaredHeader.get(header);
    }

    public boolean isModularHeader(Artifact header) {
      return modularHeaders.contains(header);
    }

    public List<PathFragment> getSystemIncludeDirs() {
      return systemIncludeDirs;
    }

    public List<String> getCmdlineIncludes() {
      return cmdlineIncludes;
    }

    public boolean isLegalHeader(Artifact header) {
      return isValidUndeclaredHeader == null
          || pathToDeclaredHeader.containsKey(header.getExecPath())
          || isValidUndeclaredHeader.test(header);
    }

    public static class Builder {
      private final Map<PathFragment, Artifact> pathToDeclaredHeader;
      private final Set<Artifact> modularHeaders;
      private List<PathFragment> systemIncludeDirs = ImmutableList.of();
      private List<String> cmdlineIncludes = ImmutableList.of();
      @Nullable private Predicate<Artifact> isValidUndeclaredHeader = null;

      public Builder(
          Map<PathFragment, Artifact> pathToDeclaredHeader, Set<Artifact> modularHeaders) {
        this.pathToDeclaredHeader = pathToDeclaredHeader;
        this.modularHeaders = modularHeaders;
      }

      @CanIgnoreReturnValue
      public Builder setSystemIncludeDirs(List<PathFragment> systemIncludeDirs) {
        this.systemIncludeDirs = systemIncludeDirs;
        return this;
      }

      @CanIgnoreReturnValue
      public Builder setCmdlineIncludes(List<String> cmdlineIncludes) {
        this.cmdlineIncludes = cmdlineIncludes;
        return this;
      }

      @CanIgnoreReturnValue
      public Builder setIsValidUndeclaredHeader(
          @Nullable Predicate<Artifact> isValidUndeclaredHeader) {
        this.isValidUndeclaredHeader = isValidUndeclaredHeader;
        return this;
      }

      public IncludeScanningHeaderData build() {
        return new IncludeScanningHeaderData(
            pathToDeclaredHeader,
            modularHeaders,
            systemIncludeDirs,
            cmdlineIncludes,
            isValidUndeclaredHeader);
      }
    }
  }
}
