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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactRoot;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.test.InstrumentedFilesProvider;
import com.google.devtools.build.lib.rules.cpp.CcLibraryHelper.Info;
import com.google.devtools.build.lib.rules.cpp.CcToolchainFeatures.FeatureConfiguration;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * An inclusion library that maps header files into a different directory. This
 * is mostly used for third party libraries that have version-specific sub-directories,
 * and also for libraries that provide architecture-dependent header files.
 * In both cases, we want normal code to be able to include files without requiring
 * the version number or the architecture name in the include statement.
 *
 * <p>Example: a <code>cc_inc_library</code> rule in
 * <code>third_party/foo</code> has the name <code>bar</code>. It will create a
 * symlink in <code>include_directory/third_party/foo/bar/third_party/foo</code>
 * pointing to <code>third_party/foo/1.0</code>. This results in the include
 * directory <code>include_directory/third_party/foo/bar</code> to be used for
 * compilations, which makes inclusions like
 * <code>#include "third_party/foo/header.h"</code> work.
 */
public abstract class CcIncLibrary implements RuleConfiguredTargetFactory {

  private final CppSemantics semantics;

  protected CcIncLibrary(CppSemantics semantics) {
    this.semantics = semantics;
  }
  
  @Override
  public ConfiguredTarget create(final RuleContext ruleContext)
      throws RuleErrorException, InterruptedException {
    CcToolchainProvider ccToolchain =
        CppHelper.getToolchainUsingDefaultCcToolchainAttribute(ruleContext);
    FeatureConfiguration featureConfiguration =
        CcCommon.configureFeatures(ruleContext, ccToolchain);
    PathFragment packageFragment = ruleContext.getPackageDirectory();

    // The rule needs a unique location for the include directory, which doesn't conflict with any
    // other rule. For that reason, the include directory is at:
    // configuration/package_name/_/target_name
    // And then the symlink is placed at:
    // configuration/package_name/_/target_name/package_name
    // So that these inclusions can be resolved correctly:
    // #include "package_name/a.h"
    //
    // The target of the symlink is:
    // package_name/targetPrefix/
    // All declared header files must be below that directory.
    String expandedIncSymlinkAttr = ruleContext.attributes().get("prefix", Type.STRING);

    // We use an additional "_" directory here to avoid conflicts between this and previous Blaze
    // versions. Previous Blaze versions created a directory symlink; the new version does not
    // detect that the output directory isn't a directory, and tries to put the symlinks into what
    // is actually a symlink into the source tree.
    PathFragment includeDirectory = PathFragment.create("_")
        .getRelative(ruleContext.getTarget().getName());
    ArtifactRoot configIncludeDirectory =
        ruleContext.getConfiguration().getIncludeDirectory(ruleContext.getRule().getRepository());
    PathFragment includePath =
        configIncludeDirectory
            .getExecPath()
            .getRelative(packageFragment)
            .getRelative(includeDirectory);
    Path includeRoot =
        configIncludeDirectory.getRoot().getRelative(packageFragment).getRelative(includeDirectory);

    // For every source artifact, we compute a virtual artifact that is below the include directory.
    // These are used for include checking.
    PathFragment prefixFragment = packageFragment.getRelative(expandedIncSymlinkAttr);
    if (!prefixFragment.isNormalized()) {
      ruleContext.attributeWarning("prefix", "should not contain '.' or '..' elements");
    }
    ImmutableSortedMap.Builder<Artifact, Artifact> virtualArtifactMapBuilder =
        ImmutableSortedMap.orderedBy(Artifact.EXEC_PATH_COMPARATOR);
    prefixFragment = prefixFragment.normalize();
    ImmutableList<Artifact> hdrs = ruleContext.getPrerequisiteArtifacts("hdrs", Mode.TARGET).list();
    for (Artifact src : hdrs) {
      // All declared header files must start with package/targetPrefix.
      if (!src.getRootRelativePath().startsWith(prefixFragment)) {
        ruleContext.attributeError("hdrs", src + " does not start with '"
            + prefixFragment.getPathString() + "'");
        return null;
      }

      // Remove the targetPrefix from within the exec path of the source file, and prepend the
      // unique directory prefix, e.g.:
      // third_party/foo/1.2/bar/a.h -> third_party/foo/name/third_party/foo/bar/a.h
      PathFragment suffix = src.getRootRelativePath().relativeTo(prefixFragment);
      PathFragment virtualPath = includeDirectory.getRelative(packageFragment)
          .getRelative(suffix);

      // These virtual artifacts have the symlink action as generating action.
      Artifact virtualArtifact =
          ruleContext.getPackageRelativeArtifact(virtualPath, configIncludeDirectory);
      virtualArtifactMapBuilder.put(virtualArtifact, src);
    }
    ImmutableSortedMap<Artifact, Artifact> virtualArtifactMap = virtualArtifactMapBuilder.build();
    ruleContext.registerAction(
        new CreateIncSymlinkAction(ruleContext.getActionOwner(), virtualArtifactMap, includeRoot));
    FdoSupportProvider fdoSupport =
        CppHelper.getFdoSupportUsingDefaultCcToolchainAttribute(ruleContext);
    Info.CompilationInfo compilationInfo =
        new CcLibraryHelper(ruleContext, semantics, featureConfiguration, ccToolchain, fdoSupport)
            .addIncludeDirs(Arrays.asList(includePath))
            .addPublicHeaders(virtualArtifactMap.keySet())
            .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
            .compile();
    Info.LinkingInfo linkingInfo =
        new CcLibraryHelper(ruleContext, semantics, featureConfiguration, ccToolchain, fdoSupport)
            .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
            .link(
                compilationInfo.getCcCompilationOutputs(),
                compilationInfo.getCppCompilationContext());

    // cc_inc_library doesn't compile any file - no compilation outputs available.
    InstrumentedFilesProvider instrumentedFilesProvider =
          new CcCommon(ruleContext).getInstrumentedFilesProvider(
              new ArrayList<Artifact>(),
              /*withBaselineCoverage=*/true);

    return new RuleConfiguredTargetBuilder(ruleContext)
        .addProviders(compilationInfo.getProviders())
        .addProviders(linkingInfo.getProviders())
        .addSkylarkTransitiveInfo(CcSkylarkApiProvider.NAME, new CcSkylarkApiProvider())
        .addOutputGroups(
            Info.mergeOutputGroups(
                compilationInfo.getOutputGroups(), linkingInfo.getOutputGroups()))
        .add(InstrumentedFilesProvider.class, instrumentedFilesProvider)
        .add(RunfilesProvider.class, RunfilesProvider.simple(Runfiles.EMPTY))
        .build();
  }
}

