// Copyright 2015 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.skyframe;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.cmdline.TargetPattern;
import com.google.devtools.build.lib.cmdline.TargetPattern.Type;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey;
import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternSkyKeyOrException;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;

import java.util.List;

/**
 * The value returned by {@link PrepareDepsOfPatternFunction}. Because that function is
 * invoked only for its side effect (i.e. ensuring the graph contains targets matching the
 * pattern and its transitive dependencies), this value carries no information.
 *
 * <p>Because the returned value is always equal to objects that share its type, this value and the
 * {@link PrepareDepsOfPatternFunction} which computes it are incompatible with change pruning. It
 * should only be requested by consumers who do not require reevaluation when
 * {@link PrepareDepsOfPatternFunction} is reevaluated. Safe consumers include, e.g., top-level
 * consumers, and other functions which invoke {@link PrepareDepsOfPatternFunction} solely for its
 * side-effects.
 */
public class PrepareDepsOfPatternValue implements SkyValue {
  // Note that this value does not guarantee singleton-like reference equality because we use Java
  // deserialization. Java deserialization can create other instances.
  public static final PrepareDepsOfPatternValue INSTANCE = new PrepareDepsOfPatternValue();

  private PrepareDepsOfPatternValue() {
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof PrepareDepsOfPatternValue;
  }

  @Override
  public int hashCode() {
    return 42;
  }

  /**
   * Returns an iterable of {@link PrepareDepsOfPatternSkyKeyOrException}, with {@link
   * TargetPatternKey} arguments. Negative target patterns of type other than {@link
   * Type#TARGETS_BELOW_DIRECTORY} are not permitted. If a provided pattern fails to parse or is
   * negative but not a {@link Type#TARGETS_BELOW_DIRECTORY}, an element in the returned iterable
   * will throw when its {@link PrepareDepsOfPatternSkyKeyOrException#getSkyKey} method is called
   * and will return the failing pattern when its {@link
   * PrepareDepsOfPatternSkyKeyOrException#getOriginalPattern} method is called.
   *
   * <p>There may be fewer returned elements than patterns provided as input. This function will
   * combine negative {@link Type#TARGETS_BELOW_DIRECTORY} patterns with preceding patterns to
   * return an iterable of SkyKeys that avoids loading excluded directories during evaluation.
   *
   * @param patterns The list of patterns, e.g. [//foo/..., -//foo/biz/...]. If a pattern's first
   *     character is "-", it is treated as a negative pattern.
   * @param offset The offset to apply to relative target patterns.
   */
  @ThreadSafe
  public static Iterable<PrepareDepsOfPatternSkyKeyOrException> keys(List<String> patterns,
      String offset) {
    List<TargetPatternSkyKeyOrException> keysMaybe =
        ImmutableList.copyOf(TargetPatternValue.keys(patterns, FilteringPolicies.NO_FILTER,
            offset));

    // This code path is evaluated only for query universe preloading, and the quadratic cost of
    // the code below (i.e. for each pattern, consider each later pattern as a candidate for
    // subdirectory exclusion) is only acceptable because all the use cases for query universe
    // preloading involve short (<10 items) pattern sequences.
    ImmutableList.Builder<PrepareDepsOfPatternSkyKeyOrException> builder = ImmutableList.builder();
    for (int i = 0; i < keysMaybe.size(); i++) {
      TargetPatternSkyKeyOrException keyMaybe = keysMaybe.get(i);
      SkyKey skyKey;
      try {
        skyKey = keyMaybe.getSkyKey();
      } catch (TargetParsingException e) {
        // keyMaybe.getSkyKey() may throw TargetParsingException if its corresponding pattern
        // failed to parse. If so, wrap the exception and return it, so that our caller can
        // deal with it.
        skyKey = null;
        builder.add(new PrepareDepsOfPatternSkyKeyException(e, keyMaybe.getOriginalPattern()));
      }
      if (skyKey != null) {
        TargetPatternKey targetPatternKey = (TargetPatternKey) skyKey.argument();
        if (targetPatternKey.isNegative()) {
          if (!targetPatternKey.getParsedPattern().getType().equals(Type.TARGETS_BELOW_DIRECTORY)) {
            builder.add(
                new PrepareDepsOfPatternSkyKeyException(
                    new TargetParsingException(
                        "Negative target patterns of types other than \"targets below directory\""
                            + " are not permitted."), targetPatternKey.toString()));
          }
          // Otherwise it's a negative TBD pattern which was combined with previous patterns as an
          // excluded directory. These can be skipped because there's no PrepareDepsOfPattern work
          // to be done for them.
        } else {
          builder.add(new PrepareDepsOfPatternSkyKeyValue(setExcludedDirectories(targetPatternKey,
              excludedDirectoriesBeneath(targetPatternKey, i, keysMaybe))));
        }
      }
    }
    return builder.build();
  }

  private static TargetPatternKey setExcludedDirectories(
      TargetPatternKey original, ImmutableSet<PathFragment> excludedSubdirectories) {
    return new TargetPatternKey(original.getParsedPattern(), original.getPolicy(),
        original.isNegative(), original.getOffset(), excludedSubdirectories);
  }

  private static ImmutableSet<PathFragment> excludedDirectoriesBeneath(
      TargetPatternKey targetPatternKey,
      int position,
      List<TargetPatternSkyKeyOrException> keysMaybe) {
    ImmutableSet.Builder<PathFragment> excludedDirectoriesBuilder = ImmutableSet.builder();
    for (int j = position + 1; j < keysMaybe.size(); j++) {
      TargetPatternSkyKeyOrException laterPatternMaybe = keysMaybe.get(j);
      SkyKey laterSkyKey;
      try {
        laterSkyKey = laterPatternMaybe.getSkyKey();
      } catch (TargetParsingException ignored) {
        laterSkyKey = null;
      }
      if (laterSkyKey != null) {
        TargetPatternKey laterTargetPatternKey = (TargetPatternKey) laterSkyKey.argument();
        TargetPattern laterParsedPattern = laterTargetPatternKey.getParsedPattern();
        if (laterTargetPatternKey.isNegative()
            && laterParsedPattern.getType() == Type.TARGETS_BELOW_DIRECTORY
            && targetPatternKey.getParsedPattern().containsDirectoryOfTBDForTBD(
                laterParsedPattern)) {
          excludedDirectoriesBuilder.add(
              laterParsedPattern.getDirectoryForTargetsUnderDirectory().getPackageFragment());
        }
      }
    }
    return excludedDirectoriesBuilder.build();
  }

  /**
   * Wrapper for a prepare deps of pattern {@link SkyKey} or the {@link TargetParsingException}
   * thrown when trying to create it.
   */
  public interface PrepareDepsOfPatternSkyKeyOrException {

    /**
     * Returns the stored {@link SkyKey} or throws {@link TargetParsingException} if one was thrown
     * when creating the key.
     */
    SkyKey getSkyKey() throws TargetParsingException;

    /**
     * Returns the pattern that resulted in the stored {@link SkyKey} or {@link
     * TargetParsingException}.
     */
    String getOriginalPattern();
  }


  private static class PrepareDepsOfPatternSkyKeyException implements
      PrepareDepsOfPatternSkyKeyOrException {

    private final TargetParsingException exception;
    private final String originalPattern;

    public PrepareDepsOfPatternSkyKeyException(TargetParsingException exception,
        String originalPattern) {
      this.exception = exception;
      this.originalPattern = originalPattern;
    }

    @Override
    public SkyKey getSkyKey() throws TargetParsingException {
      throw exception;
    }

    @Override
    public String getOriginalPattern() {
      return originalPattern;
    }
  }

  private static class PrepareDepsOfPatternSkyKeyValue implements
      PrepareDepsOfPatternSkyKeyOrException {

    private final TargetPatternKey targetPatternKey;

    public PrepareDepsOfPatternSkyKeyValue(TargetPatternKey targetPatternKey) {
      this.targetPatternKey = targetPatternKey;
    }

    @Override
    public SkyKey getSkyKey() throws TargetParsingException {
      return SkyKey.create(SkyFunctions.PREPARE_DEPS_OF_PATTERN, targetPatternKey);
    }

    @Override
    public String getOriginalPattern() {
      return targetPatternKey.getPattern();
    }
  }
}
