/**
   * Pattern matches to prevent false positives of the forms:
   *
   * <pre>
   *   (a == b) || a.equals(b)
   *   (a == b) || (a != null ? a.equals(b) : false)
   *   (a == b) || (a != null && a.equals(b))
   * </pre>
   *
   * Returns true iff the given node fits this pattern.
   *
   * @param node
   * @return true iff the node fits the pattern (a == b || a.equals(b))
   */
  private boolean suppressEarlyEquals(final BinaryTree node) {
    // Only handle == binary trees
    if (node.getKind() != Tree.Kind.EQUAL_TO) return false;

    // should strip parens
    final ExpressionTree left = unparenthesize(node.getLeftOperand());
    final ExpressionTree right = unparenthesize(node.getRightOperand());

    // looking for ((a == b || a.equals(b))
    Heuristics.Matcher matcher =
        new Heuristics.Matcher() {

          // Returns true if e is either "e1 != null" or "e2 != null"
          private boolean isNeqNull(ExpressionTree e, ExpressionTree e1, ExpressionTree e2) {
            e = unparenthesize(e);
            if (e.getKind() != Tree.Kind.NOT_EQUAL_TO) {
              return false;
            }
            ExpressionTree neqLeft = ((BinaryTree) e).getLeftOperand();
            ExpressionTree neqRight = ((BinaryTree) e).getRightOperand();
            return (((sameTree(neqLeft, e1) || sameTree(neqLeft, e2))
                    && neqRight.getKind() == Tree.Kind.NULL_LITERAL)
                // also check for "null != e1" and "null != e2"
                || ((sameTree(neqRight, e1) || sameTree(neqRight, e2))
                    && neqLeft.getKind() == Tree.Kind.NULL_LITERAL));
          }

          @Override
          public Boolean visitBinary(BinaryTree tree, Void p) {
            ExpressionTree leftTree = tree.getLeftOperand();
            ExpressionTree rightTree = tree.getRightOperand();

            if (tree.getKind() == Tree.Kind.CONDITIONAL_OR) {
              if (sameTree(leftTree, node)) {
                // left is "a==b"
                // check right, which should be a.equals(b) or b.equals(a) or similar
                return visit(rightTree, p);
              } else {
                return false;
              }
            }

            if (tree.getKind() == Tree.Kind.CONDITIONAL_AND) {
              // looking for: (a != null && a.equals(b)))
              if (isNeqNull(leftTree, left, right)) {
                return visit(rightTree, p);
              }
              return false;
            }

            return false;
          }

          @Override
          public Boolean visitConditionalExpression(ConditionalExpressionTree tree, Void p) {
            // looking for: (a != null ? a.equals(b) : false)
            ExpressionTree cond = tree.getCondition();
            ExpressionTree trueExp = tree.getTrueExpression();
            ExpressionTree falseExp = tree.getFalseExpression();
            if (isNeqNull(cond, left, right)
                && (falseExp.getKind() == Tree.Kind.BOOLEAN_LITERAL)
                && ((LiteralTree) falseExp).getValue().equals(false)) {
              return visit(trueExp, p);
            }
            return false;
          }

          @Override
          public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) {
            if (!isInvocationOfEquals(tree)) {
              return false;
            }

            List<? extends ExpressionTree> args = tree.getArguments();
            if (args.size() != 1) {
              return false;
            }
            ExpressionTree arg = args.get(0);
            // if (arg.getKind() != Tree.Kind.IDENTIFIER) {
            //     return false;
            // }
            // Element argElt = TreeUtils.elementFromUse((IdentifierTree) arg);

            ExpressionTree exp = tree.getMethodSelect();
            if (exp.getKind() != Tree.Kind.MEMBER_SELECT) {
              return false;
            }
            MemberSelectTree member = (MemberSelectTree) exp;
            ExpressionTree receiver = member.getExpression();
            // Element refElt = TreeUtils.elementFromUse(receiver);

            // if (!((refElt.equals(lhs) && argElt.equals(rhs)) ||
            //       ((refElt.equals(rhs) && argElt.equals(lhs))))) {
            //     return false;
            // }

            if (sameTree(receiver, left) && sameTree(arg, right)) {
              return true;
            }
            if (sameTree(receiver, right) && sameTree(arg, left)) {
              return true;
            }

            return false;
          }
        };

    boolean okay =
        Heuristics.Matchers.withIn(Heuristics.Matchers.ofKind(Tree.Kind.CONDITIONAL_OR, matcher))
            .match(getCurrentPath());
    return okay;
  }
  /**
   * Pattern matches to prevent false positives of the form {@code (a == b || a.compareTo(b) == 0)}.
   * Returns true iff the given node fits this pattern.
   *
   * @param node
   * @return true iff the node fits the pattern (a == b || a.compareTo(b) == 0)
   */
  private boolean suppressEarlyCompareTo(final BinaryTree node) {
    // Only handle == binary trees
    if (node.getKind() != Tree.Kind.EQUAL_TO) return false;

    Tree left = node.getLeftOperand();
    Tree right = node.getRightOperand();

    // Only valid if we're comparing identifiers.
    if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER)) {
      return false;
    }

    final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left);
    final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right);

    // looking for ((a == b || a.compareTo(b) == 0)
    Heuristics.Matcher matcher =
        new Heuristics.Matcher() {

          @Override
          public Boolean visitBinary(BinaryTree tree, Void p) {
            if (tree.getKind() == Tree.Kind.EQUAL_TO) { // a.compareTo(b) == 0
              ExpressionTree leftTree =
                  tree.getLeftOperand(); // looking for a.compareTo(b) or b.compareTo(a)
              ExpressionTree rightTree = tree.getRightOperand(); // looking for 0

              if (rightTree.getKind() != Tree.Kind.INT_LITERAL) {
                return false;
              }
              LiteralTree rightLiteral = (LiteralTree) rightTree;
              if (!rightLiteral.getValue().equals(0)) {
                return false;
              }

              return visit(leftTree, p);
            } else {
              // a == b || a.compareTo(b) == 0
              ExpressionTree leftTree = tree.getLeftOperand(); // looking for a==b
              ExpressionTree rightTree =
                  tree.getRightOperand(); // looking for a.compareTo(b) == 0 or b.compareTo(a) == 0
              if (leftTree != node) {
                return false;
              }
              if (rightTree.getKind() != Tree.Kind.EQUAL_TO) {
                return false;
              }
              return visit(rightTree, p);
            }
          }

          @Override
          public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) {
            if (!isInvocationOfCompareTo(tree)) {
              return false;
            }

            List<? extends ExpressionTree> args = tree.getArguments();
            if (args.size() != 1) {
              return false;
            }
            ExpressionTree arg = args.get(0);
            if (arg.getKind() != Tree.Kind.IDENTIFIER) {
              return false;
            }
            Element argElt = TreeUtils.elementFromUse(arg);

            ExpressionTree exp = tree.getMethodSelect();
            if (exp.getKind() != Tree.Kind.MEMBER_SELECT) {
              return false;
            }
            MemberSelectTree member = (MemberSelectTree) exp;
            if (member.getExpression().getKind() != Tree.Kind.IDENTIFIER) {
              return false;
            }

            Element refElt = TreeUtils.elementFromUse(member.getExpression());

            if (!((refElt.equals(lhs) && argElt.equals(rhs))
                || ((refElt.equals(rhs) && argElt.equals(lhs))))) {
              return false;
            }
            return true;
          }
        };

    boolean okay =
        Heuristics.Matchers.withIn(Heuristics.Matchers.ofKind(Tree.Kind.CONDITIONAL_OR, matcher))
            .match(getCurrentPath());
    return okay;
  }
  // TODO: handle != comparisons too!
  private boolean suppressInsideComparison(final BinaryTree node) {
    // Only handle == binary trees
    if (node.getKind() != Tree.Kind.EQUAL_TO) return false;

    Tree left = node.getLeftOperand();
    Tree right = node.getRightOperand();

    // Only valid if we're comparing identifiers.
    if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER))
      return false;

    // If we're not directly in an if statement in a method (ignoring
    // parens and blocks), terminate.
    // TODO: only if it's the first statement in the block
    if (!Heuristics.matchParents(getCurrentPath(), Tree.Kind.IF, Tree.Kind.METHOD)) return false;

    ExecutableElement enclosing = TreeUtils.elementFromDeclaration(visitorState.getMethodTree());
    assert enclosing != null;

    final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left);
    final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right);

    // Matcher to check for if statement that returns zero
    Heuristics.Matcher matcher =
        new Heuristics.Matcher() {

          @Override
          public Boolean visitIf(IfTree tree, Void p) {
            return visit(tree.getThenStatement(), p);
          }

          @Override
          public Boolean visitBlock(BlockTree tree, Void p) {
            if (tree.getStatements().size() > 0) return visit(tree.getStatements().get(0), p);
            return false;
          }

          @Override
          public Boolean visitReturn(ReturnTree tree, Void p) {
            ExpressionTree expr = tree.getExpression();
            return (expr != null
                && expr.getKind() == Tree.Kind.INT_LITERAL
                && ((LiteralTree) expr).getValue().equals(0));
          }
        };

    // Determine whether or not the "then" statement of the if has a single
    // "return 0" statement (for the Comparator.compare heuristic).
    if (overrides(enclosing, Comparator.class, "compare")) {
      final boolean returnsZero =
          Heuristics.Matchers.withIn(Heuristics.Matchers.ofKind(Tree.Kind.IF, matcher))
              .match(getCurrentPath());

      if (!returnsZero) return false;

      assert enclosing.getParameters().size() == 2;
      Element p1 = enclosing.getParameters().get(0);
      Element p2 = enclosing.getParameters().get(1);
      return (p1.equals(lhs) && p2.equals(rhs)) || (p2.equals(lhs) && p1.equals(rhs));

    } else if (overrides(enclosing, Object.class, "equals")) {
      assert enclosing.getParameters().size() == 1;
      Element param = enclosing.getParameters().get(0);
      Element thisElt = getThis(trees.getScope(getCurrentPath()));
      assert thisElt != null;
      return (thisElt.equals(lhs) && param.equals(rhs))
          || (param.equals(lhs) && thisElt.equals(rhs));

    } else if (overrides(enclosing, Comparable.class, "compareTo")) {

      final boolean returnsZero =
          Heuristics.Matchers.withIn(Heuristics.Matchers.ofKind(Tree.Kind.IF, matcher))
              .match(getCurrentPath());

      if (!returnsZero) {
        return false;
      }

      assert enclosing.getParameters().size() == 1;
      Element param = enclosing.getParameters().get(0);
      Element thisElt = getThis(trees.getScope(getCurrentPath()));
      assert thisElt != null;
      return (thisElt.equals(lhs) && param.equals(rhs))
          || (param.equals(lhs) && thisElt.equals(rhs));
    }
    return false;
  }