ASTIteratorWhileRenderer.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;

import java.util.List;
import org.eclipse.jdt.core.dom.*;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.functional.core.model.LoopModel;
import org.sandbox.functional.core.terminal.ForEachTerminal;

/**
 * ULR-based renderer that converts a {@link LoopModel} into an iterator-based while-loop.
 * 
 * <p>This renderer takes the abstract ULR model and produces JDT AST nodes for
 * the iterator-while pattern:</p>
 * <pre>
 * Iterator&lt;T&gt; it = collection.iterator();
 * while (it.hasNext()) {
 *     T item = it.next();
 *     // body statements
 * }
 * </pre>
 * 
 * <p>This class is the iterator-while counterpart to {@link ASTStreamRenderer},
 * enabling the ULR pipeline to target iterator-while loops instead of streams.</p>
 * 
 * @see LoopModel
 * @see ASTStreamRenderer
 */
public class ASTIteratorWhileRenderer {

	private String iteratorName;

	private final AST ast;
	private final ASTRewrite rewrite;

	public ASTIteratorWhileRenderer(AST ast, ASTRewrite rewrite) {
		this.ast = ast;
		this.rewrite = rewrite;
	}

	/** Lowers ULR filter/map/forEach operations using their original JDT attachments. */
	public void renderPipeline(LoopModel model, JdtStreamContext context, ImportRewrite imports, TextEditGroup group) {
		new ASTImperativeLoopRenderer(rewrite, imports, group).render(model, context, true);
	}

	/** Preserve the iterator's actual generic type and the loop variable's conversions. */
	@SuppressWarnings("unchecked")
	public void renderEnhancedFor(LoopModel model, EnhancedForStatement original, ImportRewrite imports, TextEditGroup group) {
		reserveIteratorName(original);
		var importContext = new ContextSensitiveImportRewriteContext((CompilationUnit) original.getRoot(), original.getStartPosition(), imports);
		ITypeBinding iterable = Bindings.findTypeInHierarchy(original.getExpression().resolveTypeBinding(), "java.lang.Iterable"); //$NON-NLS-1$
		ITypeBinding iterator = Bindings.findMethodInHierarchy(iterable, "iterator", new String[0]).getReturnType(); //$NON-NLS-1$
		VariableDeclarationFragment iteratorFragment = ast.newVariableDeclarationFragment();
		iteratorFragment.setName(ast.newSimpleName(iteratorName));
		MethodInvocation iteratorCall = ast.newMethodInvocation();
		iteratorCall.setExpression((Expression) rewrite.createCopyTarget(original.getExpression()));
		iteratorCall.setName(ast.newSimpleName("iterator")); //$NON-NLS-1$
		iteratorFragment.setInitializer(iteratorCall);
		VariableDeclarationStatement iteratorDeclaration = ast.newVariableDeclarationStatement(iteratorFragment);
		iteratorDeclaration.setType(imports.addImport(iterator, ast, importContext));

		SingleVariableDeclaration parameter = original.getParameter();
		VariableDeclarationFragment element = ast.newVariableDeclarationFragment();
		element.setName(ast.newSimpleName(model.getElement().variableName()));
		for (Object dimension : parameter.extraDimensions()) {
			element.extraDimensions().add(rewrite.createCopyTarget((Dimension) dimension));
		}
		MethodInvocation next = ast.newMethodInvocation();
		next.setExpression(ast.newSimpleName(iteratorName));
		next.setName(ast.newSimpleName("next")); //$NON-NLS-1$
		element.setInitializer(next);
		VariableDeclarationStatement declaration = ast.newVariableDeclarationStatement(element);
		declaration.setType(parameter.getType().isVar() ? imports.addImport(parameter.resolveBinding().getType(), ast, importContext)
				: (Type) rewrite.createCopyTarget(parameter.getType()));
		for (Object modifier : parameter.modifiers()) {
			declaration.modifiers().add(rewrite.createCopyTarget((ASTNode) modifier));
		}
		WhileStatement loop = ast.newWhileStatement();
		MethodInvocation hasNext = ast.newMethodInvocation();
		hasNext.setExpression(ast.newSimpleName(iteratorName));
		hasNext.setName(ast.newSimpleName("hasNext")); //$NON-NLS-1$
		loop.setExpression(hasNext);
		if (original.getBody() instanceof Block block) {
			rewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY).insertFirst(declaration, group);
			loop.setBody((Statement) rewrite.createCopyTarget(block));
		} else {
			Block body = ast.newBlock();
			body.statements().add(declaration);
			body.statements().add(rewrite.createCopyTarget(original.getBody()));
			loop.setBody(body);
		}
		Statement anchor = original;
		Statement replacement = loop;
		while (anchor.getParent() instanceof LabeledStatement label) {
			LabeledStatement copied = ast.newLabeledStatement();
			copied.setLabel((SimpleName) rewrite.createCopyTarget(label.getLabel()));
			copied.setBody(replacement);
			replacement = copied;
			anchor = label;
		}
		if (anchor.getParent() instanceof Block block) {
			var list = rewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY);
			list.insertBefore(iteratorDeclaration, anchor, group);
			list.replace(anchor, replacement, group);
		} else {
			Block block = ast.newBlock();
			block.statements().add(iteratorDeclaration);
			block.statements().add(replacement);
			rewrite.replace(anchor, block, group);
		}
	}

	private void reserveIteratorName(Statement originalStatement) {
		iteratorName = LoopVariableNames.fresh("it", LoopVariableNames.usedNames(originalStatement)); //$NON-NLS-1$
	}

	/**
	 * Renders the given LoopModel as an iterator-while loop, replacing the original statement.
	 * 
	 * @param model the ULR LoopModel to render
	 * @param originalStatement the original enhanced for-loop to replace
	 * @param originalBody the original loop body (for createCopyTarget comment preservation)
	 * @param group the text edit group
	 */
	public void render(LoopModel model, Statement originalStatement, Statement originalBody, TextEditGroup group) {
		reserveIteratorName(originalStatement);
		String elementType = model.getElement().typeName();
		String elementName = model.getElement().variableName();
		String collectionExpr = model.getSource().expression();

		// Create Iterator<T> it = collection.iterator();
		VariableDeclarationStatement iteratorDecl = createIteratorDeclaration(elementType, collectionExpr);

		// Create while (it.hasNext()) { T item = it.next(); body... }
		WhileStatement whileStmt = createWhileStatement(elementType, elementName, originalBody);

		// Replace the original statement with iterator decl + while
		replaceWithIteratorAndWhile(originalStatement, iteratorDecl, whileStmt, group);
	}

	/**
	 * Renders the given LoopModel as an iterator-while loop using string-based body statements.
	 * Used when the original AST body is not available for createCopyTarget.
	 * 
	 * @param model the ULR LoopModel to render
	 * @param originalStatement the original enhanced for-loop to replace
	 * @param group the text edit group
	 */
	public void renderFromModel(LoopModel model, Statement originalStatement, TextEditGroup group) {
		reserveIteratorName(originalStatement);
		String elementType = model.getElement().typeName();
		String elementName = model.getElement().variableName();
		String collectionExpr = model.getSource().expression();

		// Create Iterator<T> it = collection.iterator();
		VariableDeclarationStatement iteratorDecl = createIteratorDeclaration(elementType, collectionExpr);

		// Create while statement with body from model terminal
		WhileStatement whileStmt = createWhileStatementFromModel(elementType, elementName, model);

		// Replace the original statement with iterator decl + while
		replaceWithIteratorAndWhile(originalStatement, iteratorDecl, whileStmt, group);
	}

	/**
	 * Renders the given LoopModel as an iterator-while loop with body from AST statement nodes.
	 * Used when the body is available as AST nodes (e.g., from a lambda body) for createCopyTarget.
	 * 
	 * @param model the ULR LoopModel to render
	 * @param originalStatement the original statement to replace
	 * @param bodyStatements the body statements to copy into the while-loop
	 * @param group the text edit group
	 */
	@SuppressWarnings("unchecked")
	public void renderWithBodyStatements(LoopModel model, Statement originalStatement,
			java.util.List<Statement> bodyStatements, TextEditGroup group) {
		reserveIteratorName(originalStatement);
		String elementType = model.getElement().typeName();
		String elementName = model.getElement().variableName();
		String collectionExpr = model.getSource().expression();

		// Create Iterator<T> it = collection.iterator();
		VariableDeclarationStatement iteratorDecl = createIteratorDeclaration(elementType, collectionExpr);

		// Create while statement with body from provided statements
		MethodInvocation hasNextCall = ast.newMethodInvocation();
		hasNextCall.setExpression(ast.newSimpleName(iteratorName));
		hasNextCall.setName(ast.newSimpleName("hasNext")); //$NON-NLS-1$

		WhileStatement whileStmt = ast.newWhileStatement();
		whileStmt.setExpression(hasNextCall);

		Block whileBody = ast.newBlock();
		whileBody.statements().add(createItemDeclaration(elementType, elementName));

		for (Statement stmt : bodyStatements) {
			if (stmt.getParent() != null) {
				whileBody.statements().add(rewrite.createCopyTarget(stmt));
			} else {
				whileBody.statements().add(stmt);
			}
		}

		whileStmt.setBody(whileBody);

		// Replace the original statement with iterator decl + while
		replaceWithIteratorAndWhile(originalStatement, iteratorDecl, whileStmt, group);
	}

	/**
	 * Replaces the original statement with an iterator declaration followed by a while-loop.
	 * Handles both block-level (inserts before + replaces) and non-block parents (wraps in new block).
	 */
	@SuppressWarnings("unchecked")
	private void replaceWithIteratorAndWhile(Statement originalStatement,
			VariableDeclarationStatement iteratorDecl, WhileStatement whileStmt, TextEditGroup group) {
		ASTNode parent = originalStatement.getParent();
		if (parent instanceof Block parentBlock) {
			ListRewrite listRewrite = rewrite.getListRewrite(parentBlock, Block.STATEMENTS_PROPERTY);
			listRewrite.insertBefore(iteratorDecl, originalStatement, group);
			listRewrite.replace(originalStatement, whileStmt, group);
		} else {
			Block newBlock = ast.newBlock();
			newBlock.statements().add(iteratorDecl);
			newBlock.statements().add(whileStmt);
			rewrite.replace(originalStatement, newBlock, group);
		}
	}

	@SuppressWarnings("unchecked")
	private VariableDeclarationStatement createIteratorDeclaration(String elementType, String collectionExpr) {
		// Iterator<T>
		ParameterizedType iteratorType = ast.newParameterizedType(ast.newSimpleType(ast.newName("Iterator"))); //$NON-NLS-1$
		iteratorType.typeArguments().add(ast.newSimpleType(ast.newName(elementType)));

		// it = collection.iterator()
		VariableDeclarationFragment fragment = ast.newVariableDeclarationFragment();
		fragment.setName(ast.newSimpleName(iteratorName));

		MethodInvocation iteratorCall = ast.newMethodInvocation();
		iteratorCall.setExpression(createExpression(collectionExpr));
		iteratorCall.setName(ast.newSimpleName("iterator")); //$NON-NLS-1$
		fragment.setInitializer(iteratorCall);

		VariableDeclarationStatement decl = ast.newVariableDeclarationStatement(fragment);
		decl.setType(iteratorType);
		return decl;
	}

	@SuppressWarnings("unchecked")
	private WhileStatement createWhileStatement(String elementType, String elementName, Statement originalBody) {
		// while (it.hasNext())
		MethodInvocation hasNextCall = ast.newMethodInvocation();
		hasNextCall.setExpression(ast.newSimpleName(iteratorName));
		hasNextCall.setName(ast.newSimpleName("hasNext")); //$NON-NLS-1$

		WhileStatement whileStmt = ast.newWhileStatement();
		whileStmt.setExpression(hasNextCall);

		// T item = it.next();
		Block whileBody = ast.newBlock();
		whileBody.statements().add(createItemDeclaration(elementType, elementName));

		// Copy original body statements, preserving comments via createCopyTarget
		if (originalBody instanceof Block origBlock) {
			for (Object stmt : origBlock.statements()) {
				whileBody.statements().add(rewrite.createCopyTarget((Statement) stmt));
			}
		} else {
			whileBody.statements().add(rewrite.createCopyTarget(originalBody));
		}

		whileStmt.setBody(whileBody);
		return whileStmt;
	}

	@SuppressWarnings("unchecked")
	private WhileStatement createWhileStatementFromModel(String elementType, String elementName, LoopModel model) {
		// while (it.hasNext())
		MethodInvocation hasNextCall = ast.newMethodInvocation();
		hasNextCall.setExpression(ast.newSimpleName(iteratorName));
		hasNextCall.setName(ast.newSimpleName("hasNext")); //$NON-NLS-1$

		WhileStatement whileStmt = ast.newWhileStatement();
		whileStmt.setExpression(hasNextCall);

		// T item = it.next();
		Block whileBody = ast.newBlock();
		whileBody.statements().add(createItemDeclaration(elementType, elementName));

		// Add body from model terminal
		if (model.getTerminal() instanceof ForEachTerminal forEachTerminal) {
			List<String> bodyStatements = forEachTerminal.bodyStatements();
			for (String stmtStr : bodyStatements) {
				String withSemicolon = stmtStr.endsWith(";") ? stmtStr : stmtStr + ";"; //$NON-NLS-1$ //$NON-NLS-2$
				ASTNode parsedStmt = rewrite.createStringPlaceholder(withSemicolon, ASTNode.EXPRESSION_STATEMENT);
				whileBody.statements().add(parsedStmt);
			}
		}

		whileStmt.setBody(whileBody);
		return whileStmt;
	}

	private VariableDeclarationStatement createItemDeclaration(String elementType, String elementName) {
		// T item = it.next();
		MethodInvocation nextCall = ast.newMethodInvocation();
		nextCall.setExpression(ast.newSimpleName(iteratorName));
		nextCall.setName(ast.newSimpleName("next")); //$NON-NLS-1$

		VariableDeclarationFragment fragment = ast.newVariableDeclarationFragment();
		fragment.setName(ast.newSimpleName(elementName));
		fragment.setInitializer(nextCall);

		VariableDeclarationStatement decl = ast.newVariableDeclarationStatement(fragment);
		decl.setType(ast.newSimpleType(ast.newName(elementType)));
		return decl;
	}

	private Expression createExpression(String expressionStr) {
		return ExpressionHelper.createExpression(ast, rewrite, expressionStr);
	}
}