LoopBodyParser.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * 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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BreakStatement;
import org.eclipse.jdt.core.dom.ContinueStatement;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.PostfixExpression;
import org.eclipse.jdt.core.dom.PrefixExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

/**
 * Parses enhanced for-loop bodies and extracts stream operations.
 * 
 * <p>This class is responsible for analyzing the statements within an enhanced
 * for-loop and extracting a list of {@link ProspectiveOperation}s that can be
 * transformed into a stream pipeline.</p>
 * 
 * <p><b>Architecture:</b></p>
 * <p>This class uses the Strategy Pattern via {@link StatementHandler} implementations
 * to process different statement types. This eliminates deep if-else-if chains and
 * makes the code more maintainable and extensible.</p>
 * 
 * <p><b>Supported Patterns:</b></p>
 * <ul>
 * <li><b>Variable declarations</b> → MAP operations (via {@link StatementHandlerType#VARIABLE_DECLARATION})</li>
 * <li><b>IF statements</b> → FILTER operations or match patterns (via {@link StatementHandlerType#IF_STATEMENT})</li>
 * <li><b>Continue statements</b> → Negated FILTER operations</li>
 * <li><b>Accumulator patterns</b> → REDUCE operations (via {@link StatementHandlerType#TERMINAL})</li>
 * <li><b>Side-effect statements</b> → FOREACH operations</li>
 * </ul>
 * 
 * <p><b>Thread Safety:</b> This class is not thread-safe.</p>
 * 
 * @see StreamPipelineBuilder
 * @see ProspectiveOperation
 * @see StatementHandlerType
 */
public class LoopBodyParser {

	private final AST ast;
	private final ReducePatternDetector reduceDetector;
	private final CollectPatternDetector collectDetector;
	private final IfStatementAnalyzer ifAnalyzer;
	private final boolean isAnyMatchPattern;
	private final boolean isNoneMatchPattern;
	private final boolean isAllMatchPattern;
	
	// Handler context for processing statements
	private final StatementHandlerContext handlerContext;
	private final SideEffectChecker sideEffectChecker;

	/**
	 * Creates a new LoopBodyParser for the given for-loop.
	 * 
	 * @param forLoop           the enhanced for-loop to parse
	 * @param reduceDetector    detector for reduce patterns
	 * @param collectDetector   detector for collect patterns
	 * @param ifAnalyzer        analyzer for if statements
	 * @param isAnyMatchPattern whether anyMatch pattern is detected
	 * @param isNoneMatchPattern whether noneMatch pattern is detected
	 * @param isAllMatchPattern whether allMatch pattern is detected
	 */
	public LoopBodyParser(EnhancedForStatement forLoop, 
			ReducePatternDetector reduceDetector,
			CollectPatternDetector collectDetector,
			IfStatementAnalyzer ifAnalyzer,
			boolean isAnyMatchPattern, 
			boolean isNoneMatchPattern, 
			boolean isAllMatchPattern) {
		this.ast = forLoop.getAST();
		this.reduceDetector = reduceDetector;
		this.collectDetector = collectDetector;
		this.ifAnalyzer = ifAnalyzer;
		this.isAnyMatchPattern = isAnyMatchPattern;
		this.isNoneMatchPattern = isNoneMatchPattern;
		this.isAllMatchPattern = isAllMatchPattern;
		
		// Initialize handler context
		this.sideEffectChecker = new SideEffectChecker();
		this.handlerContext = new StatementHandlerContext(this, sideEffectChecker);
	}

	/**
	 * Parses the loop body and extracts stream operations.
	 * 
	 * @param body        the loop body statement
	 * @param loopVarName the loop variable name
	 * @return list of prospective operations, empty if parsing fails
	 */
	public List<ProspectiveOperation> parse(Statement body, String loopVarName) {
		List<ProspectiveOperation> ops = new ArrayList<>();
		String currentVarName = loopVarName;

		if (body instanceof Block) {
			return parseBlock((Block) body, loopVarName, currentVarName, ops);
		} else if (body instanceof IfStatement) {
			return parseIfStatement((IfStatement) body, loopVarName, currentVarName, ops);
		} else {
			return parseSingleStatement(body, loopVarName, currentVarName, ops);
		}
	}

	/**
	 * Parses a block of statements.
	 */
	private List<ProspectiveOperation> parseBlock(Block block, String loopVarName, 
			String currentVarName, List<ProspectiveOperation> ops) {
		List<Statement> statements = block.statements();

		// Check if the entire block should be treated as a single forEach
		if (shouldTreatAsSimpleForEach(statements, loopVarName)) {
			// For single-statement blocks, unwrap to allow expression lambda
			Statement forEachStatement;
			if (statements.size() == 1) {
				forEachStatement = statements.get(0);
			} else {
				forEachStatement = block;
			}
			ProspectiveOperation forEachOp = new ProspectiveOperation(forEachStatement,
					OperationType.FOREACH, loopVarName);
			ops.add(forEachOp);
			return ops;
		}

		for (int i = 0; i < statements.size(); i++) {
			Statement stmt = statements.get(i);
			boolean isLast = (i == statements.size() - 1);

			ParseResult result = parseStatement(stmt, currentVarName, ops, isLast, statements, i, loopVarName);
			if (result.shouldAbort()) {
				return new ArrayList<>();
			}
			currentVarName = result.getCurrentVarName();
			
			// Handle skip index for wrapped statements
			if (result.getSkipToIndex() >= 0) {
				i = result.getSkipToIndex();
			}
		}

		return ops;
	}

	/**
	 * Parses a single statement and returns the result.
	 * Uses the handler chain to process different statement types.
	 */
	private ParseResult parseStatement(Statement stmt, String currentVarName, 
			List<ProspectiveOperation> ops, boolean isLast, 
			List<Statement> statements, int currentIndex, String loopVarName) {
		
		// Create parsing context
		StatementParsingContext context = new StatementParsingContext(
				loopVarName,
				currentVarName,
				isLast,
				currentIndex,
				statements,
				ast,
				ifAnalyzer,
				reduceDetector,
				collectDetector,
				isAnyMatchPattern,
				isNoneMatchPattern,
				isAllMatchPattern);
		
		// Find and execute the appropriate handler using the enum
		StatementHandlerType handler = StatementHandlerType.findHandler(stmt, context);
		if (handler != null) {
			return handler.handle(stmt, context, ops, handlerContext);
		}
		
		// Fallback: no handler found - should not happen with proper handlers
		return new ParseResult(currentVarName);
	}

	/**
	 * Parses a standalone IF statement (not in a block).
	 */
	private List<ProspectiveOperation> parseIfStatement(IfStatement ifStmt, String loopVarName,
			String currentVarName, List<ProspectiveOperation> ops) {
		
		if (ifStmt.getElseStatement() != null) {
			return ops;
		}

		if (ifAnalyzer.isIfWithLabeledContinue(ifStmt) || ifAnalyzer.isIfWithBreak(ifStmt)) {
			return new ArrayList<>();
		}

		if (ifAnalyzer.isEarlyReturnIf(ifStmt, isAnyMatchPattern, isNoneMatchPattern, isAllMatchPattern)) {
			ProspectiveOperation matchOp = ifAnalyzer.createMatchOperation(ifStmt);
			ops.add(matchOp);
		} else {
			ProspectiveOperation filterOp = new ProspectiveOperation(ifStmt.getExpression(),
					OperationType.FILTER);
			ops.add(filterOp);

			List<ProspectiveOperation> nestedOps = parse(ifStmt.getThenStatement(), currentVarName);
			ops.addAll(nestedOps);
		}
		return ops;
	}

	/**
	 * Parses a single statement that is not a block or IF.
	 */
	private List<ProspectiveOperation> parseSingleStatement(Statement body, String loopVarName,
			String currentVarName, List<ProspectiveOperation> ops) {
		
		// Check for COLLECT pattern first
		ProspectiveOperation collectOp = collectDetector.detectCollectOperation(body);
		if (collectOp != null) {
			// For COLLECT, we might need a MAP operation before it if the added expression is not identity
			Expression addedExpr = collectDetector.extractCollectExpression(body);
			if (addedExpr != null && !isIdentityMapping(addedExpr, currentVarName)) {
				// Create a MAP operation for the transformation
				ProspectiveOperation mapOp = new ProspectiveOperation(addedExpr, OperationType.MAP, currentVarName);
				ops.add(mapOp);
			}
			ops.add(collectOp);
			return ops;
		}
		
		// Check for REDUCE pattern
		ProspectiveOperation reduceOp = reduceDetector.detectReduceOperation(body);
		if (reduceOp != null) {
			reduceDetector.addMapBeforeReduce(ops, reduceOp, body, currentVarName, ast);
			ops.add(reduceOp);
		} else if (body instanceof ReturnStatement || body instanceof ContinueStatement 
				|| body instanceof ThrowStatement) {
			return new ArrayList<>();
		} else if (!sideEffectChecker.isSafeSideEffect(body, currentVarName, ops)) {
			return new ArrayList<>();
		} else {
			ProspectiveOperation forEachOp = new ProspectiveOperation(body,
					OperationType.FOREACH, currentVarName);
			ops.add(forEachOp);
		}
		return ops;
	}
	
	/**
	 * Checks if an expression is an identity mapping (just returns the variable unchanged).
	 */
	private boolean isIdentityMapping(Expression expr, String varName) {
		if (expr instanceof SimpleName) {
			return varName.equals(((SimpleName) expr).getIdentifier());
		}
		return false;
	}

	/**
	 * Determines if the loop body should be treated as a single forEach.
	 */
	private boolean shouldTreatAsSimpleForEach(List<Statement> statements, String loopVarName) {
		if (statements.isEmpty()) {
			return false;
		}

		if (statements.size() >= 1 && statements.get(0) instanceof IfStatement) {
			return false;
		}

		Set<String> declaredVariables = new HashSet<>();
		IfStatement ifStatementAfterVarDecls = null;
		boolean allSafeSideEffects = true;

		for (Statement stmt : statements) {
			if (stmt instanceof VariableDeclarationStatement) {
				VariableDeclarationStatement varDecl = (VariableDeclarationStatement) stmt;
				for (Object frag : varDecl.fragments()) {
					if (frag instanceof VariableDeclarationFragment) {
						declaredVariables.add(((VariableDeclarationFragment) frag).getName().getIdentifier());
					}
				}
				// Variable declarations are not simple side-effects
				allSafeSideEffects = false;
			} else if (stmt instanceof IfStatement) {
				IfStatement ifStmt = (IfStatement) stmt;
				if (ifAnalyzer.isEarlyReturnIf(ifStmt, isAnyMatchPattern, isNoneMatchPattern, isAllMatchPattern)
						|| ifAnalyzer.isIfWithContinue(ifStmt)) {
					return false;
				}
				if (!declaredVariables.isEmpty()) {
					ifStatementAfterVarDecls = ifStmt;
				}
				// IF statements are not simple side-effects
				allSafeSideEffects = false;
			} else if (stmt instanceof ReturnStatement || stmt instanceof ContinueStatement 
					|| stmt instanceof BreakStatement) {
				return false;
			} else if (stmt instanceof ExpressionStatement) {
				// Check if this is a method call or other safe side-effect
				ExpressionStatement exprStmt = (ExpressionStatement) stmt;
				Expression expr = exprStmt.getExpression();
				
				if (expr instanceof MethodInvocation) {
					MethodInvocation methodInv = (MethodInvocation) expr;
					// Check if this is a COLLECT pattern (collection.add())
					// If so, don't treat as simple forEach - let COLLECT handling take over
					if ("add".equals(methodInv.getName().getIdentifier())) {
						ProspectiveOperation collectOp = collectDetector.detectCollectOperation(stmt);
						if (collectOp != null) {
							return false;  // NOT a simple forEach - it's a COLLECT pattern
						}
					}
					// Other method invocations are safe side-effects
				} else {
					// Non-method-call expressions might be reduce operations or assignments
					ProspectiveOperation reduceOp = reduceDetector.detectReduceOperation(stmt);
					if (reduceOp != null) {
						return false;
					}
					allSafeSideEffects = false;
				}
			} else {
				// Other statement types are not simple side-effects
				allSafeSideEffects = false;
			}

			ProspectiveOperation reduceOp = reduceDetector.detectReduceOperation(stmt);
			if (reduceOp != null) {
				return false;
			}
		}

		if (ifStatementAfterVarDecls != null && !declaredVariables.isEmpty()) {
			if (ifModifiesDeclaredVariables(ifStatementAfterVarDecls, declaredVariables)) {
				return true;
			}
			return false;
		}

		// If all statements are safe side-effects (method calls), treat as simple forEach
		return allSafeSideEffects;
	}

	/**
	 * Checks if an IF statement modifies any declared variables.
	 */
	private boolean ifModifiesDeclaredVariables(IfStatement ifStmt, Set<String> declaredVariables) {
		Statement thenStmt = ifStmt.getThenStatement();
		return statementModifiesVariables(thenStmt, declaredVariables);
	}

	/**
	 * Recursively checks if a statement modifies any variables.
	 */
	private boolean statementModifiesVariables(Statement stmt, Set<String> variables) {
		if (stmt == null || variables.isEmpty()) {
			return false;
		}

		if (stmt instanceof Block) {
			Block block = (Block) stmt;
			for (Object s : block.statements()) {
				if (statementModifiesVariables((Statement) s, variables)) {
					return true;
				}
			}
		} else if (stmt instanceof ExpressionStatement) {
			ExpressionStatement exprStmt = (ExpressionStatement) stmt;
			return expressionModifiesVariables(exprStmt.getExpression(), variables);
		} else if (stmt instanceof IfStatement) {
			IfStatement ifStmt = (IfStatement) stmt;
			if (statementModifiesVariables(ifStmt.getThenStatement(), variables)) {
				return true;
			}
			if (ifStmt.getElseStatement() != null 
					&& statementModifiesVariables(ifStmt.getElseStatement(), variables)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Checks if an expression modifies any variables.
	 */
	private boolean expressionModifiesVariables(Expression expr, Set<String> variables) {
		if (expr == null) {
			return false;
		}

		if (expr instanceof Assignment) {
			Assignment assignment = (Assignment) expr;
			Expression lhs = assignment.getLeftHandSide();
			if (lhs instanceof SimpleName) {
				return variables.contains(((SimpleName) lhs).getIdentifier());
			}
		}

		if (expr instanceof PostfixExpression) {
			PostfixExpression postfix = (PostfixExpression) expr;
			if (postfix.getOperand() instanceof SimpleName) {
				return variables.contains(((SimpleName) postfix.getOperand()).getIdentifier());
			}
		}

		if (expr instanceof PrefixExpression) {
			PrefixExpression prefix = (PrefixExpression) expr;
			PrefixExpression.Operator op = prefix.getOperator();
			if (op == PrefixExpression.Operator.INCREMENT || op == PrefixExpression.Operator.DECREMENT) {
				if (prefix.getOperand() instanceof SimpleName) {
					return variables.contains(((SimpleName) prefix.getOperand()).getIdentifier());
				}
			}
		}

		return false;
	}

	/**
	 * Result of parsing a single statement.
	 */
	public static class ParseResult {
		private final String currentVarName;
		private final boolean abort;
		private final int skipToIndex;

		public ParseResult(String currentVarName) {
			this(currentVarName, false, -1);
		}

		public ParseResult(String currentVarName, int skipToIndex) {
			this(currentVarName, false, skipToIndex);
		}

		private ParseResult(String currentVarName, boolean abort, int skipToIndex) {
			this.currentVarName = currentVarName;
			this.abort = abort;
			this.skipToIndex = skipToIndex;
		}

		public static ParseResult abort() {
			return new ParseResult(null, true, -1);
		}

		public String getCurrentVarName() {
			return currentVarName;
		}

		public boolean shouldAbort() {
			return abort;
		}

		public int getSkipToIndex() {
			return skipToIndex;
		}
	}
}