@Nullable
  private static TerminalOperation fromCollector(@NotNull PsiType elementType,
                                                 @NotNull PsiType resultType,
                                                 PsiMethod collector,
                                                 PsiExpression[] collectorArgs) {
    String collectorName = collector.getName();
    FunctionHelper fn;
    switch (collectorName) {
      case "toList" -> {
        if (collectorArgs.length != 0) return null;
        return ToCollectionTerminalOperation.toList(resultType);
      }
      case "toUnmodifiableList" -> {
        if (collectorArgs.length != 0) return null;
        return new WrappedCollectionTerminalOperation(ToCollectionTerminalOperation.toList(resultType), "unmodifiableList", resultType);
      }
      case "toSet" -> {
        if (collectorArgs.length != 0) return null;
        return ToCollectionTerminalOperation.toSet(resultType);
      }
      case "toUnmodifiableSet" -> {
        if (collectorArgs.length != 0) return null;
        return new WrappedCollectionTerminalOperation(ToCollectionTerminalOperation.toSet(resultType), "unmodifiableSet", resultType);
      }
      case "toCollection" -> {
        if (collectorArgs.length != 1) return null;
        fn = FunctionHelper.create(collectorArgs[0], 0);
        return fn == null ? null : new ToCollectionTerminalOperation(resultType, fn, null);
      }
      case "collectingAndThen" -> {
        if (collectorArgs.length != 2) return null;
        PsiExpression collectorCall = collectorArgs[0];
        PsiType downstreamResultType = PsiUtil.substituteTypeParameter(collectorCall.getType(), "java.util.stream.Collector", 2, false);
        if (downstreamResultType == null) return null;
        CollectorBasedTerminalOperation downstream =
          tryCast(fromCollector(elementType, downstreamResultType, collectorCall), CollectorBasedTerminalOperation.class);
        if (downstream == null) return null;
        FunctionHelper andThen = FunctionHelper.create(collectorArgs[1], 1);
        return andThen != null ? new WrappedCollectionTerminalOperation(downstream, andThen) : null;
      }
      case "toUnmodifiableMap", "toMap" -> {
        if (collectorArgs.length < 2 || collectorArgs.length > 4) return null;
        FunctionHelper key = FunctionHelper.create(collectorArgs[0], 1);
        FunctionHelper value = FunctionHelper.create(collectorArgs[1], 1);
        if (key == null || value == null) return null;
        PsiExpression merger = collectorArgs.length > 2 ? collectorArgs[2] : null;
        FunctionHelper supplier = collectorArgs.length == 4
                                  ? FunctionHelper.create(collectorArgs[3], 0)
                                  : FunctionHelper.newObjectSupplier(resultType, CommonClassNames.JAVA_UTIL_HASH_MAP);
        if (supplier == null) return null;
        CollectorBasedTerminalOperation operation = new ToMapTerminalOperation(key, value, merger, supplier, resultType);
        return collectorName.equals("toUnmodifiableMap")
               ? new WrappedCollectionTerminalOperation(operation, "unmodifiableMap", resultType)
               : operation;
      }
      case "reducing" -> {
        switch (collectorArgs.length) {
          case 1 -> {
            return ReduceToOptionalTerminalOperation.create(collectorArgs[0], resultType);
          }
          case 2 -> {
            fn = FunctionHelper.create(collectorArgs[1], 2);
            return fn == null ? null : new ReduceTerminalOperation(collectorArgs[0], fn, resultType);
          }
          case 3 -> {
            FunctionHelper mapper = FunctionHelper.create(collectorArgs[1], 1);
            fn = FunctionHelper.create(collectorArgs[2], 2);
            return fn == null || mapper == null
                   ? null
                   : new MappingTerminalOperation(mapper, new ReduceTerminalOperation(collectorArgs[0], fn, resultType));
          }
        }
        return null;
      }
      case "counting" -> {
        if (collectorArgs.length != 0) return null;
        return TemplateBasedOperation.counting();
      }
      case "summingInt", "summingLong", "summingDouble" -> {
        if (collectorArgs.length != 1) return null;
        fn = FunctionHelper.create(collectorArgs[0], 1);
        PsiPrimitiveType type = PsiPrimitiveType.getUnboxedType(resultType);
        return fn == null || type == null ? null : new InlineMappingTerminalOperation(fn, TemplateBasedOperation.summing(type));
      }
      case "summarizingInt", "summarizingLong", "summarizingDouble" -> {
        if (collectorArgs.length != 1) return null;
        fn = FunctionHelper.create(collectorArgs[0], 1);
        return fn == null ? null : new InlineMappingTerminalOperation(fn, TemplateBasedOperation.summarizing(resultType));
      }
      case "averagingInt", "averagingLong", "averagingDouble" -> {
        if (collectorArgs.length != 1) return null;
        fn = FunctionHelper.create(collectorArgs[0], 1);
        return fn == null
               ? null
               : new InlineMappingTerminalOperation(fn, new AverageTerminalOperation(collectorName.equals("averagingDouble"), false));
      }
      case "mapping" -> {
        if (collectorArgs.length != 2) return null;
        fn = FunctionHelper.create(collectorArgs[0], 1);
        if (fn == null) return null;
        TerminalOperation downstreamOp = fromCollector(fn.getResultType(), resultType, collectorArgs[1]);
        return downstreamOp == null ? null : new MappingTerminalOperation(fn, downstreamOp);
      }
      case "groupingBy", "partitioningBy" -> {
        if (collectorArgs.length == 0 || collectorArgs.length > 3
            || collectorArgs.length == 3 && collectorName.equals("partitioningBy")) return null;
        fn = FunctionHelper.create(collectorArgs[0], 1);
        if (fn == null) return null;
        PsiType resultSubType = PsiUtil.substituteTypeParameter(resultType, CommonClassNames.JAVA_UTIL_MAP, 1, false);
        if (resultSubType == null) return null;
        CollectorOperation downstreamCollector;
        if (collectorArgs.length == 1) {
          downstreamCollector = ToCollectionTerminalOperation.toList(resultSubType).asCollector();
        }
        else {
          PsiExpression downstream = collectorArgs[collectorArgs.length - 1];
          TerminalOperation downstreamOp = fromCollector(elementType, resultSubType, downstream);
          if (downstreamOp == null) return null;
          downstreamCollector = downstreamOp.asCollector();
        }
        if (downstreamCollector == null) return null;
        if (collectorName.equals("partitioningBy")) {
          return new PartitionByTerminalOperation(fn, resultType, downstreamCollector);
        }
        FunctionHelper supplier = collectorArgs.length == 3
                                  ? FunctionHelper.create(collectorArgs[1], 0)
                                  : FunctionHelper.newObjectSupplier(resultType, CommonClassNames.JAVA_UTIL_HASH_MAP);
        return new GroupByTerminalOperation(fn, supplier, resultType, downstreamCollector);
      }
      case "minBy", "maxBy" -> {
        if (collectorArgs.length != 1) return null;
        return MinMaxTerminalOperation.create(collectorArgs[0], elementType, collectorName.equals("maxBy"));
      }
      case "joining" -> {
        PsiElementFactory factory = JavaPsiFacade.getElementFactory(collector.getProject());
        switch (collectorArgs.length) {
          case 0 -> {
            return new TemplateBasedOperation("sb", factory.createTypeFromText(CommonClassNames.JAVA_LANG_STRING_BUILDER, collector),
                                              "new " + CommonClassNames.JAVA_LANG_STRING_BUILDER + "()",
                                              "{acc}.append({item});",
                                              "{acc}.toString()");
          }
          case 1, 3 -> {
            String initializer =
              "new java.util.StringJoiner(" + StreamEx.of(collectorArgs).map(PsiElement::getText).joining(",") + ")";
            return new TemplateBasedOperation("joiner", factory.createTypeFromText("java.util.StringJoiner", collector), initializer,
                                              "{acc}.add({item});", "{acc}.toString()");
          }
        }
        return null;
      }
    }
    return null;
  }
@Nullable
  private static TerminalOperation fromCollector(@NotNull PsiType elementType,
                                                 @NotNull PsiType resultType,
                                                 PsiMethod collector,
                                                 PsiExpression[] collectorArgs)