private static void replaceTailCalls(PsiElement element,
                                         PsiMethod method,
                                         @Nullable String thisVariableName,
                                         boolean tailCallIsContainedInLoop,
                                         boolean isReturnAtTheEndOfWhileLoop,
                                         @NonNls StringBuilder out) {
      PsiMethodCallExpression tailCall;
      if (isImplicitCallOnThis(element, method)) {
        if (thisVariableName != null) {
          out.append(thisVariableName).append('.');
        }
        out.append(element.getText());
      }
      else if (element instanceof PsiQualifiedExpression) {
        out.append(thisVariableName == null ? element.getText() : thisVariableName);
      }
      else if ((tailCall = getTailCall(element, method)) != null) {
        assert element instanceof PsiStatement;
        final PsiExpression[] arguments = tailCall.getArgumentList().getExpressions();
        final PsiParameter[] parameters = method.getParameterList().getParameters();
        final boolean isInBlock = element.getParent() instanceof PsiCodeBlock;
        if (!isInBlock) {
          out.append('{');
        }
        else {
          // remove tabs and spaces at the end
          int index = out.length() - 1;
          char c = out.charAt(index);
          while (c == ' ' || c == '\t') c = out.charAt(--index);
          out.delete(index + 1, out.length());
        }
        for (PsiComment comment : PsiTreeUtil.findChildrenOfType(element, PsiComment.class)) {
          if (!isAtStartOfLine(comment)) out.append(' ');
          out.append(comment.getText()).append('\n');
        }
        PsiExpression current = tailCall;
        List<String> conditions = new ArrayList<>();
        while (true) {
          PsiPolyadicExpression parent =
            ObjectUtils.tryCast(PsiUtil.skipParenthesizedExprUp(current.getParent()), PsiPolyadicExpression.class);
          if (parent == null) {
            break;
          }
          PsiExpression[] operands = parent.getOperands();
          if (operands.length < 2) break;
          String condition = parent.getText().substring(0, operands[operands.length - 2].getTextRangeInParent().getEndOffset());
          boolean returnTrue = parent.getOperationTokenType() == JavaTokenType.OROR;
          if (!returnTrue) {
            PsiExpression cond = JavaPsiFacade.getElementFactory(method.getProject()).createExpressionFromText(condition, parent);
            condition = BoolUtils.getNegatedExpressionText(cond);
          }
          String ifStatement = "if(" + condition + ") return " + returnTrue + ";\n";
          conditions.add(0, ifStatement);
          current = parent;
        }
        conditions.forEach(out::append);
        final Graph<Integer> graph = buildGraph(parameters, arguments);
        // When replacing recursion with iteration, new values are assigned to the parameters,
        // instead of calling the method with the new values. Care needs to be taken to not clobber
        // the value of a parameter which is used later (in some expression assigned to a different
        // parameter). To achieve this a simple graph of the dependencies between the parameters is
        // built and analysed/ordered. If the graph is a directed acyclic graph, the assignments
        // are ordered in such a way that making a defensive copy is unnecessary (topological
        // ordering). If the graph has a cycle, a copy of the value of at least one parameter needs
        // to be made before assigning a new value.
        final DFSTBuilder<Integer> builder = new DFSTBuilder<>(graph);
        final List<Integer> sortedNodes = builder.getSortedNodes();
        final Set<Integer> seen = new HashSet<>();
        final Map<PsiElement, String> replacements = new HashMap<>();
        for (Integer index : sortedNodes) {
          final PsiParameter parameter = parameters[index];
          final String parameterName = parameter.getName();
          final PsiExpression argument = PsiUtil.skipParenthesizedExprDown(arguments[index]);
          assert argument != null;
          if (argument instanceof PsiReferenceExpression referenceExpression) {
            if (parameter.equals(referenceExpression.resolve())) {
              // parameter keeps same value
              continue;
            }
          }
          final Iterator<Integer> dependants = graph.getIn(index); // parameters which depend on parameter 'index'
          boolean copy = false;
          while (dependants.hasNext()) {
            if (!seen.contains(dependants.next())) {
              // current parameter which depends on value of parameter 'index' has not yet received its value (cycle)
              // if 'dependants' was some collection instead of an iterator this would have been a nice containsAll expression
              copy = true;
              break;
            }
          }
          if (copy) {
            final String variableName =
              JavaCodeStyleManager.getInstance(method.getProject()).suggestUniqueVariableName(parameterName, element, false);
            out.append(parameter.getType().getCanonicalText()).append(' ').append(variableName).append('=');
            out.append(parameterName).append(';');
            replacements.put(parameter, variableName);
          }
          out.append(parameterName).append('=');
          buildText(argument, replacements, out);
          out.append(';');
          seen.add(index);
        }
        if (thisVariableName != null) {
          final PsiReferenceExpression methodExpression = tailCall.getMethodExpression();
          final PsiExpression qualifier = methodExpression.getQualifierExpression();
          if (qualifier != null) {
            out.append(thisVariableName).append('=');
            replaceTailCalls(qualifier, method, thisVariableName, tailCallIsContainedInLoop, isReturnAtTheEndOfWhileLoop, out);
            out.append(';');
          }
        }
        final PsiCodeBlock body = method.getBody();
        assert body != null;
        if ((element instanceof PsiReturnStatement && ControlFlowUtils.blockCompletesWithStatement(body, (PsiStatement)element)) ||
            (element instanceof PsiExpressionStatement && (!isReturnAtTheEndOfWhileLoop || isBeforeVoidReturn(element, method)))) {
          //don't do anything, as the continue statement is unnecessary
        }
        else if (tailCallIsContainedInLoop) {
          out.append("continue ").append(method.getName()).append(';');
        }
        else {
          out.append("continue;");
        }
        if (!isInBlock) {
          out.append('}');
        }
      }
      else {
        final PsiCodeBlock body = method.getBody();
        assert body != null;
        if (isVoidReturn(element)) {
          final PsiElement prevElement = PsiTreeUtil.skipWhitespacesAndCommentsBackward(element);
          final PsiExpressionStatement prevExpressionStatement = ObjectUtils.tryCast(prevElement, PsiExpressionStatement.class);
          tailCall = getTailCall(prevExpressionStatement, method);
          if (tailCall != null) {
            out.append("continue;");
            return;
          }
        }
        final PsiElement[] children = element.getChildren();
        if (children.length == 0) {
          out.append(element.getText());
        }
        else {
          for (final PsiElement child : children) {
            replaceTailCalls(child, method, thisVariableName, tailCallIsContainedInLoop, isReturnAtTheEndOfWhileLoop, out);
          }
        }
      }
    }
private static void replaceTailCalls(PsiElement element,
                                         PsiMethod method,
                                         @Nullable String thisVariableName,
                                         boolean tailCallIsContainedInLoop,
                                         boolean isReturnAtTheEndOfWhileLoop,
                                         @NonNls StringBuilder out)