// 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.query2.engine;

import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.concurrent.MoreFutures;
import com.google.devtools.build.lib.query2.engine.Lexer.TokenKind;
import com.google.devtools.build.lib.util.Preconditions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;

/**
 * A binary algebraic set operation.
 *
 * <pre>
 * expr ::= expr (INTERSECT expr)+
 *        | expr ('^' expr)+
 *        | expr (UNION expr)+
 *        | expr ('+' expr)+
 *        | expr (EXCEPT expr)+
 *        | expr ('-' expr)+
 * </pre>
 */
public class BinaryOperatorExpression extends QueryExpression {

  private final Lexer.TokenKind operator; // ::= INTERSECT/CARET | UNION/PLUS | EXCEPT/MINUS
  private final ImmutableList<QueryExpression> operands;

  public BinaryOperatorExpression(Lexer.TokenKind operator, List<QueryExpression> operands) {
    Preconditions.checkState(operands.size() > 1);
    this.operator = operator;
    this.operands = ImmutableList.copyOf(operands);
  }

  public Lexer.TokenKind getOperator() {
    return operator;
  }

  public ImmutableList<QueryExpression> getOperands() {
    return operands;
  }

  @Override
  protected <T> void evalImpl(
      QueryEnvironment<T> env, VariableContext<T> context, Callback<T> callback)
          throws QueryException, InterruptedException {

    if (operator == TokenKind.PLUS || operator == TokenKind.UNION) {
      for (QueryExpression operand : operands) {
        env.eval(operand, context, callback);
      }
      return;
    }

    // Once we have fully evaluated the left-hand side, we can stream-process the right-hand side
    // for minus operations. Note that this is suboptimal if the left-hand side results are very
    // large compared to the right-hand side. Which is the case is hard to know before evaluating.
    // We could consider determining this dynamically, however, by evaluating both the left and
    // right hand side partially until one side finishes sooner.
    final Set<T> lhsValue = QueryUtil.evalAll(env, context, operands.get(0));
    if (operator == TokenKind.EXCEPT || operator == TokenKind.MINUS) {
      for (int i = 1; i < operands.size(); i++) {
        env.eval(operands.get(i), context,
            new Callback<T>() {
              @Override
              public void process(Iterable<T> partialResult)
                  throws QueryException, InterruptedException {
                for (T target : partialResult) {
                  lhsValue.remove(target);
                }
              }
            });
      }
      callback.process(lhsValue);
      return;
    }

    // Intersection is not associative, so we are forced to pin both the left-hand and right-hand
    // side of the operation at the same time.
    // TODO(bazel-team): Consider keeping just the name / label of the right-hand side results
    // instead of the potentially heavy-weight instances of type T.
    Preconditions.checkState(operator == TokenKind.INTERSECT || operator == TokenKind.CARET,
        operator);
    for (int i = 1; i < operands.size(); i++) {
      lhsValue.retainAll(QueryUtil.evalAll(env, context, operands.get(i)));
    }
    callback.process(lhsValue);
  }

  @Override
  protected <T> void parEvalImpl(
      QueryEnvironment<T> env,
      VariableContext<T> context,
      ThreadSafeCallback<T> callback,
      ForkJoinPool forkJoinPool)
      throws QueryException, InterruptedException {
    if (operator == TokenKind.PLUS || operator == TokenKind.UNION) {
      parEvalPlus(operands, env, context, callback, forkJoinPool);
    } else if (operator == TokenKind.EXCEPT || operator == TokenKind.MINUS) {
      parEvalMinus(operands, env, context, callback, forkJoinPool);
    } else {
      evalImpl(env, context, callback);
    }
  }

  /**
   * Evaluates an expression of the form "e1 + e2 + ... + eK" by evaluating all the subexpressions
   * in parallel.
   */
  private static <T> void parEvalPlus(
      ImmutableList<QueryExpression> operands,
      final QueryEnvironment<T> env,
      final VariableContext<T> context,
      final ThreadSafeCallback<T> callback,
      ForkJoinPool forkJoinPool)
          throws QueryException, InterruptedException {
    ArrayList<ForkJoinTask<Void>> tasks = new ArrayList<>(operands.size());
    for (final QueryExpression operand : operands) {
      tasks.add(ForkJoinTask.adapt(
          new Callable<Void>() {
            @Override
            public Void call() throws QueryException, InterruptedException {
              env.eval(operand, context, callback);
              return null;
            }
          }));
    }
    for (ForkJoinTask<?> task : tasks) {
      forkJoinPool.submit(task);
    }
    try {
      MoreFutures.waitForAllInterruptiblyFailFast(tasks);
    } catch (ExecutionException e) {
      Throwables.propagateIfPossible(
          e.getCause(), QueryException.class, InterruptedException.class);
      throw new IllegalStateException(e);
    }
  }

  /**
   * Evaluates an expression of the form "e1 - e2 - ... - eK" by noting its equivalence to
   * "e1 - (e2 + ... + eK)" and evaluating the subexpressions on the right-hand-side in parallel.
   */
  private static <T> void parEvalMinus(
      ImmutableList<QueryExpression> operands,
      QueryEnvironment<T> env,
      VariableContext<T> context,
      ThreadSafeCallback<T> callback,
      ForkJoinPool forkJoinPool)
          throws QueryException, InterruptedException {
    final Set<T> lhsValue =
        Sets.newConcurrentHashSet(QueryUtil.evalAll(env, context, operands.get(0)));
    ThreadSafeCallback<T> subtractionCallback = new ThreadSafeCallback<T>() {
      @Override
      public void process(Iterable<T> partialResult) throws QueryException, InterruptedException {
        for (T target : partialResult) {
          lhsValue.remove(target);
        }
      }
    };
    parEvalPlus(
        operands.subList(1, operands.size()), env, context, subtractionCallback, forkJoinPool);
    callback.process(lhsValue);
  }

  @Override
  public void collectTargetPatterns(Collection<String> literals) {
    for (QueryExpression subExpression : operands) {
      subExpression.collectTargetPatterns(literals);
    }
  }

  @Override
  public QueryExpression getMapped(QueryExpressionMapper mapper) {
    return mapper.map(this);
  }

  @Override
  public String toString() {
    StringBuilder result = new StringBuilder();
    for (int i = 1; i < operands.size(); i++) {
      result.append("(");
    }
    result.append(operands.get(0));
    for (int i = 1; i < operands.size(); i++) {
      result.append(" " + operator.getPrettyName() + " " + operands.get(i) + ")");
    }
    return result.toString();
  }
}
