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

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.rules.cpp.CppCompileAction;
import com.google.devtools.build.lib.rules.cpp.IncludeProcessing;
import com.google.devtools.build.lib.rules.cpp.IncludeScanner.IncludeScannerSupplier;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
 * Returns all inclusions that were discovered by the header scanner tool to implement the header
 * thinning feature.
 *
 * <p>Reads the .headers_list output file if one was generated for the actions source file and
 * returns the Artifact objects associated with the headers that were found.
 */
@AutoCodec
public class HeaderThinning implements IncludeProcessing {
  private final Iterable<Artifact> potentialInputs;

  public HeaderThinning(Iterable<Artifact> potentialInputs) {
    // Just store this, don't create map of potential inputs at construction so that it is done
    // later at execution time rather than analysis time when this is instantiated.
    this.potentialInputs = potentialInputs;
  }

  private Map<PathFragment, Artifact> getAllowedInputsMap() {
    Map<PathFragment, Artifact> allowedInputsMap = new HashMap<>();
    for (Artifact input : potentialInputs) {
      allowedInputsMap.put(input.getExecPath(), input);
    }
    return allowedInputsMap;
  }

  @Nullable
  private static Artifact findHeadersListFile(NestedSet<Artifact> artifacts) {
    for (Artifact artifact : artifacts) {
      if (artifact.getExtension().equals("headers_list")) {
        return artifact;
      }
    }
    return null;
  }

  @Override
  public Iterable<Artifact> determineAdditionalInputs(
      @Nullable IncludeScannerSupplier includeScannerSupplier,
      CppCompileAction action,
      ActionExecutionContext actionExecutionContext)
      throws ExecException {
    Artifact headersListFile = findHeadersListFile(action.getMandatoryInputs());
    if (headersListFile == null) {
      return null;
    }
    return findRequiredHeaderInputs(action.getSourceFile(), headersListFile, getAllowedInputsMap());
  }

  /**
   * Reads the header scanning output file and discovers all of those headers as input artifacts.
   *
   * @param sourceFile the source that requires these headers
   * @param headersListFile .headers_list file output from header_scanner tool to be read
   * @param inputArtifactsMap map of PathFragment to Artifact of possible headers
   * @return collection of header artifacts that are required for {@code action} to compile
   * @throws ExecException on environmental (IO) or user errors
   */
  public static Iterable<Artifact> findRequiredHeaderInputs(
      Artifact sourceFile, Artifact headersListFile, Map<PathFragment, Artifact> inputArtifactsMap)
      throws ExecException {
    try {
      ImmutableList.Builder<Artifact> includeBuilder = ImmutableList.builder();
      List<PathFragment> missing = new ArrayList<>();
      for (String line :
          FileSystemUtils.readLines(headersListFile.getPath(), StandardCharsets.UTF_8)) {
        if (line.isEmpty()) {
          continue;
        }

        PathFragment headerPath = PathFragment.create(line);
        Artifact header = inputArtifactsMap.get(headerPath);
        if (header == null) {
          missing.add(headerPath);
        } else {
          includeBuilder.add(header);
        }
      }

      if (!missing.isEmpty()) {
        includeBuilder.addAll(
            findRequiredHeaderInputsInTreeArtifacts(sourceFile, inputArtifactsMap, missing));
      }
      return includeBuilder.build();
    } catch (IOException ex) {
      throw new EnvironmentalExecException(
          String.format("Error reading headers file %s", headersListFile.getExecPathString()), ex);
    }
  }

  /**
   * Headers inside a TreeArtifact will not have their ExecPath as a key in the map as they do not
   * have their own Artifact object. These headers must be mapped to their containing TreeArtifact.
   * We are unable to select individual files from within a TreeArtifact so must discover the entire
   * TreeArtifact as an input.
   */
  private static Iterable<Artifact> findRequiredHeaderInputsInTreeArtifacts(
      Artifact sourceFile,
      Map<PathFragment, Artifact> inputArtifactsMap,
      List<PathFragment> missing)
      throws ExecException {
    ImmutableList.Builder<Artifact> includeBuilder = ImmutableList.builder();
    ImmutableList.Builder<PathFragment> treeArtifactPathsBuilder = ImmutableList.builder();
    for (Map.Entry<PathFragment, Artifact> inputEntry : inputArtifactsMap.entrySet()) {
      if (inputEntry.getValue().isTreeArtifact()) {
        treeArtifactPathsBuilder.add(inputEntry.getKey());
      }
    }

    ImmutableList<PathFragment> treeArtifactPaths = treeArtifactPathsBuilder.build();
    for (PathFragment missingPath : missing) {
      includeBuilder.add(
          findRequiredHeaderInputInTreeArtifacts(
              sourceFile, treeArtifactPaths, inputArtifactsMap, missingPath));
    }
    return includeBuilder.build();
  }

  private static Artifact findRequiredHeaderInputInTreeArtifacts(
      Artifact sourceFile,
      List<PathFragment> treeArtifactPaths,
      Map<PathFragment, Artifact> inputArtifactsMap,
      PathFragment missingPath)
      throws ExecException {
    for (PathFragment treeArtifactPath : treeArtifactPaths) {
      if (missingPath.startsWith(treeArtifactPath)) {
        return inputArtifactsMap.get(treeArtifactPath);
      }
    }

    throw new UserExecException(
        String.format(
            "Unable to map header file (%s) found during header scanning of %s."
                + " This is usually the result of a case mismatch",
            missingPath, sourceFile.getExecPathString()));
  }
}
