// Copyright 2017 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.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.config.PerLabelOptions;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
import com.google.devtools.build.lib.rules.cpp.CppCompileAction.DotdFile;
import com.google.devtools.build.lib.util.FileType;
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.List;
import java.util.Map;
import javax.annotation.Nullable;

/** The compile command line for the C++ compile action. */
public final class CompileCommandLine {

  private final Artifact sourceFile;
  private final Artifact outputFile;
  private final Label sourceLabel;
  private final DotdFile dotdFile;
  private final List<String> copts;
  private final Predicate<String> coptsFilter;
  private final Collection<String> features;
  private final FeatureConfiguration featureConfiguration;
  @VisibleForTesting public final CcToolchainFeatures.Variables variables;
  private final String actionName;
  private final CppConfiguration cppConfiguration;

  public CompileCommandLine(
      Artifact sourceFile,
      Artifact outputFile,
      Label sourceLabel,
      ImmutableList<String> copts,
      Predicate<String> coptsFilter,
      Collection<String> features,
      FeatureConfiguration featureConfiguration,
      CppConfiguration cppConfiguration,
      CcToolchainFeatures.Variables variables,
      String actionName,
      DotdFile dotdFile) {
    this.sourceFile = Preconditions.checkNotNull(sourceFile);
    this.outputFile = Preconditions.checkNotNull(outputFile);
    this.sourceLabel = Preconditions.checkNotNull(sourceLabel);
    this.copts = Preconditions.checkNotNull(copts);
    this.coptsFilter = coptsFilter;
    this.features = Preconditions.checkNotNull(features);
    this.featureConfiguration = Preconditions.checkNotNull(featureConfiguration);
    this.cppConfiguration = Preconditions.checkNotNull(cppConfiguration);
    this.variables = variables;
    this.actionName = actionName;
    this.dotdFile = isGenerateDotdFile(sourceFile) ? Preconditions.checkNotNull(dotdFile) : null;
  }

  /** Returns true if Dotd file should be generated. */
  private boolean isGenerateDotdFile(Artifact sourceArtifact) {
    return CppFileTypes.headerDiscoveryRequired(sourceArtifact)
        && !featureConfiguration.isEnabled(CppRuleClasses.PARSE_SHOWINCLUDES);
  }

  /** Returns the environment variables that should be set for C++ compile actions. */
  protected Map<String, String> getEnvironment() {
    return featureConfiguration.getEnvironmentVariables(actionName, variables);
  }

  protected List<String> getArgv(
      PathFragment outputFile, CcToolchainFeatures.Variables overwrittenVariables) {
    List<String> commandLine = new ArrayList<>();

    // first: The command name.
    if (!featureConfiguration.actionIsConfigured(actionName)) {
      commandLine.add(
          cppConfiguration.getToolPathFragment(CppConfiguration.Tool.GCC).getPathString());
    } else {
      commandLine.add(
          featureConfiguration
              .getToolForAction(actionName)
              .getToolPath(cppConfiguration.getCrosstoolTopPathFragment())
              .getPathString());
    }

    // second: The compiler options.
    commandLine.addAll(getCompilerOptions(overwrittenVariables));

    if (!featureConfiguration.isEnabled("compile_action_flags_in_flag_set")) {
      // third: The file to compile!
      commandLine.add("-c");
      commandLine.add(sourceFile.getExecPathString());

      // finally: The output file. (Prefixed with -o).
      commandLine.add("-o");
      commandLine.add(outputFile.getPathString());
    }

    return commandLine;
  }

  private boolean isObjcCompile(String actionName) {
    return (actionName.equals(CppCompileAction.OBJC_COMPILE)
        || actionName.equals(CppCompileAction.OBJCPP_COMPILE));
  }

  public List<String> getCompilerOptions(
      @Nullable CcToolchainFeatures.Variables overwrittenVariables) {
    List<String> options = new ArrayList<>();
    CppConfiguration toolchain = cppConfiguration;

    addFilteredOptions(options, toolchain.getCompilerOptions(features));

    String sourceFilename = sourceFile.getExecPathString();
    if (CppFileTypes.C_SOURCE.matches(sourceFilename)) {
      addFilteredOptions(options, toolchain.getCOptions());
    }
    if (CppFileTypes.CPP_SOURCE.matches(sourceFilename)
        || CppFileTypes.CPP_HEADER.matches(sourceFilename)
        || CppFileTypes.CPP_MODULE_MAP.matches(sourceFilename)
        || CppFileTypes.CLIF_INPUT_PROTO.matches(sourceFilename)) {
      addFilteredOptions(options, toolchain.getCxxOptions(features));
    }

    // TODO(bazel-team): This needs to be before adding getUnfilteredCompilerOptions() and after
    // adding the warning flags until all toolchains are migrated; currently toolchains use the
    // unfiltered compiler options to inject include paths, which is superseded by the feature
    // configuration; on the other hand toolchains switch off warnings for the layering check
    // that will be re-added by the feature flags.
    CcToolchainFeatures.Variables updatedVariables = variables;
    if (variables != null && overwrittenVariables != null) {
      CcToolchainFeatures.Variables.Builder variablesBuilder =
          new CcToolchainFeatures.Variables.Builder();
      variablesBuilder.addAll(variables);
      variablesBuilder.addAndOverwriteAll(overwrittenVariables);
      updatedVariables = variablesBuilder.build();
    }
    addFilteredOptions(options, featureConfiguration.getCommandLine(actionName, updatedVariables));

    // Users don't expect the explicit copts to be filtered by coptsFilter, add them verbatim.
    // Make sure these are added after the options from the feature configuration, so that
    // those options can be overriden.
    options.addAll(copts);

    // Unfiltered compiler options contain system include paths. These must be added after
    // the user provided options, otherwise users adding include paths will not pick up their
    // own include paths first.
    if (!isObjcCompile(actionName)) {
      options.addAll(toolchain.getUnfilteredCompilerOptions(features));
    }

    // Add the options of --per_file_copt, if the label or the base name of the source file
    // matches the specified regular expression filter.
    for (PerLabelOptions perLabelOptions : cppConfiguration.getPerFileCopts()) {
      if ((sourceLabel != null && perLabelOptions.isIncluded(sourceLabel))
          || perLabelOptions.isIncluded(sourceFile)) {
        options.addAll(perLabelOptions.getOptions());
      }
    }

    if (!featureConfiguration.isEnabled("compile_action_flags_in_flag_set")) {
      if (FileType.contains(outputFile, CppFileTypes.ASSEMBLER, CppFileTypes.PIC_ASSEMBLER)) {
        options.add("-S");
      } else if (FileType.contains(
          outputFile,
          CppFileTypes.PREPROCESSED_C,
          CppFileTypes.PREPROCESSED_CPP,
          CppFileTypes.PIC_PREPROCESSED_C,
          CppFileTypes.PIC_PREPROCESSED_CPP)) {
        options.add("-E");
      }
    }

    return options;
  }

  // For each option in 'in', add it to 'out' unless it is matched by the 'coptsFilter' regexp.
  private void addFilteredOptions(List<String> out, List<String> in) {
    Iterables.addAll(out, Iterables.filter(in, coptsFilter));
  }

  public Artifact getSourceFile() {
    return sourceFile;
  }

  public DotdFile getDotdFile() {
    return dotdFile;
  }

  public List<String> getCopts() {
    return copts;
  }
}
