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

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.cmdline.ResolvedTargets;
import com.google.devtools.build.lib.cmdline.ResolvedTargets.Builder;
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.Immutable;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.pkgcache.FilteringPolicies;
import com.google.devtools.build.lib.pkgcache.FilteringPolicy;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.InterruptibleSupplier;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * A value referring to a computed set of resolved targets. This is used for the results of target
 * pattern parsing.
 */
@Immutable
@ThreadSafe
public final class TargetPatternValue implements SkyValue {

  private ResolvedTargets<Label> targets;

  TargetPatternValue(ResolvedTargets<Label> targets) {
    this.targets = Preconditions.checkNotNull(targets);
  }

  private void writeObject(ObjectOutputStream out) throws IOException {
    List<String> ts = new ArrayList<>();
    List<String> filteredTs = new ArrayList<>();
    for (Label target : targets.getTargets()) {
      ts.add(target.toString());
    }
    for (Label target : targets.getFilteredTargets()) {
      filteredTs.add(target.toString());
    }

    out.writeObject(ts);
    out.writeObject(filteredTs);
  }

  private Label labelFromString(String labelString) {
    try {
      return Label.parseAbsolute(labelString);
    } catch (LabelSyntaxException e) {
      throw new IllegalStateException(e);
    }
  }

  @SuppressWarnings("unchecked")
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    List<String> ts = (List<String>) in.readObject();
    List<String> filteredTs = (List<String>) in.readObject();

    Builder<Label> builder = ResolvedTargets.<Label>builder();
    for (String labelString : ts) {
      builder.add(labelFromString(labelString));
    }

    for (String labelString : filteredTs) {
      builder.remove(labelFromString(labelString));
    }
    this.targets = builder.build();
  }

  @SuppressWarnings("unused")
  private void readObjectNoData() {
    throw new IllegalStateException();
  }

  /**
   * Create a target pattern {@link SkyKey}. Throws {@link TargetParsingException} if the provided
   * {@code pattern} cannot be parsed.
   *
   * @param pattern The pattern, eg "-foo/biz...". If the first character is "-", the pattern is
   *     treated as a negative pattern.
   * @param policy The filtering policy, eg "only return test targets"
   * @param offset The offset to apply to relative target patterns.
   */
  @ThreadSafe
  public static SkyKey key(String pattern, FilteringPolicy policy, String offset)
      throws TargetParsingException {
    return Iterables.getOnlyElement(keys(ImmutableList.of(pattern), policy, offset)).getSkyKey();
  }

  /**
   * Returns an iterable of {@link TargetPatternSkyKeyOrException}, with {@link TargetPatternKey}
   * arguments, in the same order as the list of patterns provided as input. If a provided pattern
   * fails to parse, the element in the returned iterable corresponding to it will throw when its
   * {@link TargetPatternSkyKeyOrException#getSkyKey} method is called.
   *
   * @param patterns The list of patterns, eg "-foo/biz...". If a pattern's first character is "-",
   *     it is treated as a negative pattern.
   * @param policy The filtering policy, eg "only return test targets"
   * @param offset The offset to apply to relative target patterns.
   */
  @ThreadSafe
  public static Iterable<TargetPatternSkyKeyOrException> keys(List<String> patterns,
      FilteringPolicy policy, String offset) {
    TargetPattern.Parser parser = new TargetPattern.Parser(offset);
    ImmutableList.Builder<TargetPatternSkyKeyOrException> builder = ImmutableList.builder();
    for (String pattern : patterns) {
      boolean positive = !pattern.startsWith("-");
      String absoluteValueOfPattern = positive ? pattern : pattern.substring(1);
      TargetPattern targetPattern;
      try {
        targetPattern = parser.parse(absoluteValueOfPattern);
      } catch (TargetParsingException e) {
        builder.add(new TargetPatternSkyKeyException(e, absoluteValueOfPattern));
        continue;
      }
      TargetPatternKey targetPatternKey =
          new TargetPatternKey(
              targetPattern,
              positive ? policy : FilteringPolicies.NO_FILTER, /*isNegative=*/
              !positive,
              offset,
              ImmutableSet.<PathFragment>of());
      SkyKey skyKey = SkyKey.create(SkyFunctions.TARGET_PATTERN, targetPatternKey);
      builder.add(new TargetPatternSkyKeyValue(skyKey));
    }
    return builder.build();
  }

  public ResolvedTargets<Label> getTargets() {
    return targets;
  }

  /**
   * A TargetPatternKey is a tuple of pattern (eg, "foo/..."), filtering policy, a relative pattern
   * offset, whether it is a positive or negative match, and a set of excluded subdirectories.
   */
  @ThreadSafe
  public static class TargetPatternKey implements Serializable {

    private final TargetPattern parsedPattern;
    private final FilteringPolicy policy;
    private final boolean isNegative;

    private final String offset;
    private final ImmutableSet<PathFragment> excludedSubdirectories;

    public TargetPatternKey(
        TargetPattern parsedPattern,
        FilteringPolicy policy,
        boolean isNegative,
        String offset,
        ImmutableSet<PathFragment> excludedSubdirectories) {
      this.parsedPattern = Preconditions.checkNotNull(parsedPattern);
      this.policy = Preconditions.checkNotNull(policy);
      this.isNegative = isNegative;
      this.offset = offset;
      this.excludedSubdirectories = Preconditions.checkNotNull(excludedSubdirectories);
    }

    public String getPattern() {
      return parsedPattern.getOriginalPattern();
    }

    public TargetPattern getParsedPattern() {
      return parsedPattern;
    }

    public boolean isNegative() {
      return isNegative;
    }

    public FilteringPolicy getPolicy() {
      return policy;
    }

    public String getOffset() {
      return offset;
    }

    public ImmutableSet<PathFragment> getExcludedSubdirectories() {
      return excludedSubdirectories;
    }

    ImmutableSet<PathFragment> getAllSubdirectoriesToExclude(
        Iterable<PathFragment> blacklistedPackagePrefixes) throws InterruptedException {
      return getAllSubdirectoriesToExclude(
          new InterruptibleSupplier.Instance<>(blacklistedPackagePrefixes));
    }

    public ImmutableSet<PathFragment> getAllSubdirectoriesToExclude(
        InterruptibleSupplier<? extends Iterable<PathFragment>> blacklistedPackagePrefixes)
        throws InterruptedException {
      ImmutableSet.Builder<PathFragment> excludedPathsBuilder = ImmutableSet.builder();
      excludedPathsBuilder.addAll(getExcludedSubdirectories());
      if (parsedPattern.getType() == Type.TARGETS_BELOW_DIRECTORY) {
        for (PathFragment blacklistedPackagePrefix : blacklistedPackagePrefixes.get()) {
          PackageIdentifier pkgIdForBlacklistedDirectorPrefix = PackageIdentifier.create(
              parsedPattern.getDirectory().getRepository(),
              blacklistedPackagePrefix);
          if (parsedPattern.containsBelowDirectory(pkgIdForBlacklistedDirectorPrefix)) {
            excludedPathsBuilder.add(blacklistedPackagePrefix);
          }
        }
      }
      return excludedPathsBuilder.build();
    }

    @Override
    public String toString() {
      return (isNegative ? "-" : "") + parsedPattern.getOriginalPattern();
    }

    @Override
    public int hashCode() {
      return Objects.hash(parsedPattern, isNegative, policy, offset,
          excludedSubdirectories);
    }

    @Override
    public boolean equals(Object obj) {
      if (!(obj instanceof TargetPatternKey)) {
        return false;
      }
      TargetPatternKey other = (TargetPatternKey) obj;

      return other.isNegative == this.isNegative && other.parsedPattern.equals(this.parsedPattern)
          && other.offset.equals(this.offset) && other.policy.equals(this.policy)
          && other.excludedSubdirectories.equals(this.excludedSubdirectories);
    }
  }

  /**
   * Wrapper for a target pattern {@link SkyKey} or the {@link TargetParsingException} thrown when
   * trying to compute it.
   */
  public interface TargetPatternSkyKeyOrException {

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

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

  private static final class TargetPatternSkyKeyValue implements TargetPatternSkyKeyOrException {

    private final SkyKey value;

    private TargetPatternSkyKeyValue(SkyKey value) {
      this.value = value;
    }

    @Override
    public SkyKey getSkyKey() throws TargetParsingException {
      return value;
    }

    @Override
    public String getOriginalPattern() {
      return ((TargetPatternKey) value.argument()).getPattern();
    }
  }

  private static final class TargetPatternSkyKeyException implements
      TargetPatternSkyKeyOrException {

    private final TargetParsingException exception;
    private final String originalPattern;

    private TargetPatternSkyKeyException(TargetParsingException exception, String originalPattern) {
      this.exception = exception;
      this.originalPattern = originalPattern;
    }

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

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