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

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.ArchivedTreeArtifact;
import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact;
import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
import com.google.devtools.build.lib.actions.FilesetOutputTree;
import com.google.devtools.build.lib.actions.InputMetadataProvider;
import com.google.devtools.build.lib.actions.PathMapper;
import com.google.devtools.build.lib.actions.RunfilesTree;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.VirtualActionInput;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.skyframe.TreeArtifactValue;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * A helper class for spawn strategies to turn runfiles suppliers into input mappings. This class
 * performs no I/O operations, but only rearranges the files according to how the runfiles should be
 * laid out.
 */
public final class SpawnInputExpander {

  private final boolean expandArchivedTreeArtifacts;

  public SpawnInputExpander() {
    this(/* expandArchivedTreeArtifacts= */ true);
  }

  public SpawnInputExpander(boolean expandArchivedTreeArtifacts) {
    this.expandArchivedTreeArtifacts = expandArchivedTreeArtifacts;
  }

  private static void addMapping(
      Map<PathFragment, ActionInput> inputMap,
      PathFragment targetLocation,
      ActionInput input,
      PathFragment baseDirectory) {
    Preconditions.checkArgument(!targetLocation.isAbsolute(), targetLocation);
    inputMap.put(baseDirectory.getRelative(targetLocation), input);
  }

  @VisibleForTesting
  void addSingleRunfilesTreeToInputs(
      RunfilesTree runfilesTree,
      Map<PathFragment, ActionInput> inputMap,
      InputMetadataProvider inputMetadataProvider,
      PathMapper pathMapper,
      PathFragment baseDirectory) {
    addSingleRunfilesTreeToInputs(
        inputMap,
        runfilesTree.getExecPath(),
        runfilesTree.getMapping(),
        inputMetadataProvider,
        pathMapper,
        baseDirectory);
  }

  /**
   * Gathers the mapping for a single runfiles tree into {@code inputMap}.
   *
   * <p>This should not be a public interface, it's only there to support legacy code until we
   * figure out how not to call this method (or else how to make this method more palatable)
   */
  public void addSingleRunfilesTreeToInputs(
      Map<PathFragment, ActionInput> inputMap,
      PathFragment root,
      Map<PathFragment, Artifact> mappings,
      InputMetadataProvider inputMetadataProvider,
      PathMapper pathMapper,
      PathFragment baseDirectory) {
    Preconditions.checkArgument(!root.isAbsolute(), root);
    for (Map.Entry<PathFragment, Artifact> mapping : mappings.entrySet()) {
      PathFragment location = root.getRelative(mapping.getKey());
      Artifact artifact = mapping.getValue();
      if (artifact == null) {
        addMapping(
            inputMap,
            mapForRunfiles(pathMapper, root, location),
            VirtualActionInput.EMPTY_MARKER,
            baseDirectory);
        continue;
      }
      Preconditions.checkArgument(!artifact.isRunfilesTree(), artifact);
      if (artifact.isTreeArtifact()) {
        TreeArtifactValue treeArtifactValue = inputMetadataProvider.getTreeMetadata(artifact);
        ArchivedTreeArtifact archivedTreeArtifact =
            expandArchivedTreeArtifacts ? null : treeArtifactValue.getArchivedArtifact();
        if (archivedTreeArtifact != null) {
          // TODO(bazel-team): Add path mapping support for archived tree artifacts.
          addMapping(inputMap, location, archivedTreeArtifact, baseDirectory);
        } else if (treeArtifactValue.getChildren().isEmpty()) {
          addMapping(inputMap, mapForRunfiles(pathMapper, root, location), artifact, baseDirectory);
        } else {
          for (TreeFileArtifact input : treeArtifactValue.getChildren()) {
            addMapping(
                inputMap,
                mapForRunfiles(pathMapper, root, location)
                    .getRelative(input.getParentRelativePath()),
                input,
                baseDirectory);
          }
        }
      } else if (artifact.isFileset()) {
        // TODO(bazel-team): Add path mapping support for filesets.
        FilesetOutputTree filesetOutput = inputMetadataProvider.getFileset(artifact);
        addFilesetManifest(location, artifact, filesetOutput, inputMap, baseDirectory);
      } else {
        addMapping(inputMap, mapForRunfiles(pathMapper, root, location), artifact, baseDirectory);
      }
    }
  }

  @VisibleForTesting
  static void addFilesetManifests(
      Map<Artifact, FilesetOutputTree> filesetMappings,
      Map<PathFragment, ActionInput> inputMap,
      PathFragment baseDirectory) {
    for (Map.Entry<Artifact, FilesetOutputTree> entry : filesetMappings.entrySet()) {
      Artifact fileset = entry.getKey();
      addFilesetManifest(fileset.getExecPath(), fileset, entry.getValue(), inputMap, baseDirectory);
    }
  }

  private static void addFilesetManifest(
      PathFragment location,
      Artifact filesetArtifact,
      FilesetOutputTree filesetOutput,
      Map<PathFragment, ActionInput> inputMap,
      PathFragment baseDirectory) {
    Preconditions.checkArgument(filesetArtifact.isFileset(), filesetArtifact);
    for (FilesetOutputSymlink link : filesetOutput.symlinks()) {
      addMapping(inputMap, location.getRelative(link.name()), link.target(), baseDirectory);
    }
  }

  private void addInputs(
      Map<PathFragment, ActionInput> inputMap,
      NestedSet<? extends ActionInput> inputFiles,
      InputMetadataProvider inputMetadataProvider,
      PathMapper pathMapper,
      PathFragment baseDirectory) {
    // Actions that accept TreeArtifacts as inputs generally expect the directory corresponding
    // to the artifact to be created, even if it is empty. We explicitly keep empty TreeArtifacts
    // here to signal consumers that they should create the directory.
    List<ActionInput> inputs =
        InputMetadataProvider.expandArtifacts(
            inputMetadataProvider,
            inputFiles,
            /* keepEmptyTreeArtifacts= */ true,
            /* keepRunfilesTrees= */ true);
    for (ActionInput input : inputs) {
      switch (input) {
        case TreeFileArtifact child -> {
          Artifact parent = child.getParent();
          PathFragment parentPath = pathMapper.map(parent.getExecPath());
          addMapping(
              inputMap,
              // If the PathMapper was no-op for the parent, we can use the child's exec path and
              // avoid path concatenation.
              parentPath.equals(parent.getExecPath())
                  ? child.getExecPath()
                  : parentPath.getRelative(child.getParentRelativePath()),
              input,
              baseDirectory);
        }
        case Artifact runfilesTreeArtifact when runfilesTreeArtifact.isRunfilesTree() ->
            addSingleRunfilesTreeToInputs(
                inputMetadataProvider.getRunfilesMetadata(runfilesTreeArtifact).getRunfilesTree(),
                inputMap,
                inputMetadataProvider,
                pathMapper,
                baseDirectory);
        case Artifact fileset when fileset.isFileset() ->
            addFilesetManifest(
                fileset.getExecPath(),
                fileset,
                inputMetadataProvider.getFileset(fileset),
                inputMap,
                baseDirectory);
        default -> addMapping(inputMap, pathMapper.map(input.getExecPath()), input, baseDirectory);
      }
    }
  }

  /**
   * Convert the inputs and runfiles of the given spawn to a map from exec-root relative paths to
   * {@link ActionInput}s. The returned map does not contain non-empty tree artifacts as they are
   * expanded to file artifacts. Tree artifacts that would expand to the empty set under the
   * provided {@link InputMetadataProvider} are left untouched so that their corresponding empty
   * directories can be created.
   *
   * <p>The returned map never contains {@code null} values.
   *
   * <p>The returned map contains all runfiles, but not the {@code MANIFEST}.
   */
  public SortedMap<PathFragment, ActionInput> getInputMapping(
      Spawn spawn, InputMetadataProvider inputMetadataProvider, PathFragment baseDirectory) {
    TreeMap<PathFragment, ActionInput> inputMap = new TreeMap<>();
    addInputs(
        inputMap,
        spawn.getInputFiles(),
        inputMetadataProvider,
        spawn.getPathMapper(),
        baseDirectory);
    return inputMap;
  }

  private static PathFragment mapForRunfiles(
      PathMapper pathMapper, PathFragment runfilesDir, PathFragment execPath) {
    if (pathMapper.isNoop()) {
      return execPath;
    }
    String runfilesDirName = runfilesDir.getBaseName();
    Preconditions.checkArgument(runfilesDirName.endsWith(".runfiles"));
    // Derive the path of the executable, apply the path mapping to it and then rederive the path
    // of the runfiles dir.
    PathFragment executable =
        runfilesDir.replaceName(
            runfilesDirName.substring(0, runfilesDirName.length() - ".runfiles".length()));
    return pathMapper
        .map(executable)
        .replaceName(runfilesDirName)
        .getRelative(execPath.relativeTo(runfilesDir));
  }
}
