@SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:MethodLength"})
  public static Expression convert(Predicate predicate) {
    Operation op = FILTERS.get(predicate.name());
    if (op != null) {
      switch (op) {
        case TRUE:
          return Expressions.alwaysTrue();

        case FALSE:
          return Expressions.alwaysFalse();

        case IS_NULL:
          return isRef(child(predicate)) ? isNull(SparkUtil.toColumnName(child(predicate))) : null;

        case NOT_NULL:
          return isRef(child(predicate)) ? notNull(SparkUtil.toColumnName(child(predicate))) : null;

        case LT:
          if (isRef(leftChild(predicate)) && isLiteral(rightChild(predicate))) {
            String columnName = SparkUtil.toColumnName(leftChild(predicate));
            return lessThan(columnName, convertLiteral(rightChild(predicate)));
          } else if (isRef(rightChild(predicate)) && isLiteral(leftChild(predicate))) {
            String columnName = SparkUtil.toColumnName(rightChild(predicate));
            return greaterThan(columnName, convertLiteral(leftChild(predicate)));
          } else {
            return null;
          }

        case LT_EQ:
          if (isRef(leftChild(predicate)) && isLiteral(rightChild(predicate))) {
            String columnName = SparkUtil.toColumnName(leftChild(predicate));
            return lessThanOrEqual(columnName, convertLiteral(rightChild(predicate)));
          } else if (isRef(rightChild(predicate)) && isLiteral(leftChild(predicate))) {
            String columnName = SparkUtil.toColumnName(rightChild(predicate));
            return greaterThanOrEqual(columnName, convertLiteral(leftChild(predicate)));
          } else {
            return null;
          }

        case GT:
          if (isRef(leftChild(predicate)) && isLiteral(rightChild(predicate))) {
            String columnName = SparkUtil.toColumnName(leftChild(predicate));
            return greaterThan(columnName, convertLiteral(rightChild(predicate)));
          } else if (isRef(rightChild(predicate)) && isLiteral(leftChild(predicate))) {
            String columnName = SparkUtil.toColumnName(rightChild(predicate));
            return lessThan(columnName, convertLiteral(leftChild(predicate)));
          } else {
            return null;
          }

        case GT_EQ:
          if (isRef(leftChild(predicate)) && isLiteral(rightChild(predicate))) {
            String columnName = SparkUtil.toColumnName(leftChild(predicate));
            return greaterThanOrEqual(columnName, convertLiteral(rightChild(predicate)));
          } else if (isRef(rightChild(predicate)) && isLiteral(leftChild(predicate))) {
            String columnName = SparkUtil.toColumnName(rightChild(predicate));
            return lessThanOrEqual(columnName, convertLiteral(leftChild(predicate)));
          } else {
            return null;
          }

        case EQ: // used for both eq and null-safe-eq
          Object value;
          String columnName;
          if (isRef(leftChild(predicate)) && isLiteral(rightChild(predicate))) {
            columnName = SparkUtil.toColumnName(leftChild(predicate));
            value = convertLiteral(rightChild(predicate));
          } else if (isRef(rightChild(predicate)) && isLiteral(leftChild(predicate))) {
            columnName = SparkUtil.toColumnName(rightChild(predicate));
            value = convertLiteral(leftChild(predicate));
          } else {
            return null;
          }

          if (predicate.name().equals(EQ)) {
            // comparison with null in normal equality is always null. this is probably a mistake.
            Preconditions.checkNotNull(
                value, "Expression is always false (eq is not null-safe): %s", predicate);
            return handleEqual(columnName, value);
          } else { // "<=>"
            if (value == null) {
              return isNull(columnName);
            } else {
              return handleEqual(columnName, value);
            }
          }

        case IN:
          if (isSupportedInPredicate(predicate)) {
            return in(
                SparkUtil.toColumnName(childAtIndex(predicate, 0)),
                Arrays.stream(predicate.children())
                    .skip(1)
                    .map(val -> convertLiteral(((Literal<?>) val)))
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList()));
          } else {
            return null;
          }

        case NOT:
          Not notPredicate = (Not) predicate;
          Predicate childPredicate = notPredicate.child();
          if (childPredicate.name().equals(IN) && isSupportedInPredicate(childPredicate)) {
            // infer an extra notNull predicate for Spark NOT IN filters
            // as Iceberg expressions don't follow the 3-value SQL boolean logic
            // col NOT IN (1, 2) in Spark is equal to notNull(col) && notIn(col, 1, 2) in Iceberg
            Expression notIn =
                notIn(
                    SparkUtil.toColumnName(childAtIndex(childPredicate, 0)),
                    Arrays.stream(childPredicate.children())
                        .skip(1)
                        .map(val -> convertLiteral(((Literal<?>) val)))
                        .filter(Objects::nonNull)
                        .collect(Collectors.toList()));
            return and(notNull(SparkUtil.toColumnName(childAtIndex(childPredicate, 0))), notIn);
          } else if (hasNoInFilter(childPredicate)) {
            Expression child = convert(childPredicate);
            if (child != null) {
              return not(child);
            }
          }
          return null;

        case AND:
          {
            And andPredicate = (And) predicate;
            Expression left = convert(andPredicate.left());
            Expression right = convert(andPredicate.right());
            if (left != null && right != null) {
              return and(left, right);
            }
            return null;
          }

        case OR:
          {
            Or orPredicate = (Or) predicate;
            Expression left = convert(orPredicate.left());
            Expression right = convert(orPredicate.right());
            if (left != null && right != null) {
              return or(left, right);
            }
            return null;
          }

        case STARTS_WITH:
          String colName = SparkUtil.toColumnName(leftChild(predicate));
          return startsWith(colName, convertLiteral(rightChild(predicate)).toString());
      }
    }

    return null;
  }
@Test
  public void testNotIn() {
    NamedReference namedReference = FieldReference.apply("col");
    LiteralValue v1 = new LiteralValue(1, DataTypes.IntegerType);
    LiteralValue v2 = new LiteralValue(2, DataTypes.IntegerType);
    org.apache.spark.sql.connector.expressions.Expression[] attrAndValue =
        new org.apache.spark.sql.connector.expressions.Expression[] {namedReference, v1, v2};

    Predicate in = new Predicate("IN", attrAndValue);
    Not not = new Not(in);

    Expression actual = SparkV2Filters.convert(not);
    Expression expected =
        Expressions.and(Expressions.notNull("col"), Expressions.notIn("col", 1, 2));
    Assert.assertEquals("Expressions should match", expected.toString(), actual.toString());
  }