ProspectiveOperation.java

/*******************************************************************************
 * Copyright (c) 2025 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.Collection;
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.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;

/**
 * Represents a prospective stream operation extracted from a loop body.
 * 
 * <p>
 * This class encapsulates a single operation in a stream pipeline being
 * constructed from an enhanced for-loop. Each operation corresponds to a
 * stream method (filter, map, forEach, reduce, etc.) and maintains information
 * about the expression to transform, variables consumed/produced, and any
 * special handling required.
 * </p>
 * 
 * <p><b>Operation Types:</b></p>
 * <ul>
 * <li><b>FILTER</b>: Conditional filtering ({@code .filter(predicate)})</li>
 * <li><b>MAP</b>: Transformation ({@code .map(function)})</li>
 * <li><b>FOREACH</b>: Terminal action ({@code .forEach(consumer)})</li>
 * <li><b>REDUCE</b>: Aggregation ({@code .reduce(identity, accumulator)})</li>
 * <li><b>ANYMATCH</b>: Short-circuit match ({@code .anyMatch(predicate)})</li>
 * <li><b>NONEMATCH</b>: Short-circuit non-match ({@code .noneMatch(predicate)})</li>
 * <li><b>ALLMATCH</b>: Short-circuit all-match ({@code .allMatch(predicate)})</li>
 * </ul>
 * 
 * <p><b>Variable Tracking:</b></p>
 * <p>
 * The class tracks three types of variables:
 * <ul>
 * <li><b>Consumed variables</b>: Variables read by this operation</li>
 * <li><b>Produced variable</b>: Variable created by MAP operations (e.g., {@code int x = ...})</li>
 * <li><b>Accumulator variable</b>: Variable modified by REDUCE operations (e.g., {@code sum += ...})</li>
 * </ul>
 * This tracking enables proper scoping and validation in the stream pipeline.
 * </p>
 * 
 * <p><b>Reducer Patterns:</b></p>
 * <p>
 * For REDUCE operations, this class supports various reducer types:
 * <ul>
 * <li>INCREMENT/DECREMENT: {@code i++}, {@code i--}</li>
 * <li>SUM: {@code sum += x} → {@code .reduce(sum, Integer::sum)}</li>
 * <li>PRODUCT: {@code product *= x} → {@code .reduce(product, (a,b) -> a*b)}</li>
 * <li>MAX/MIN: {@code max = Math.max(max, x)} → {@code .reduce(max, Integer::max)}</li>
 * <li>STRING_CONCAT: {@code str += s} → {@code .reduce(str, String::concat)} (when null-safe)</li>
 * </ul>
 * </p>
 * 
 * <p><b>Lambda Generation:</b></p>
 * <p>
 * The {@link #getArguments(AST, String)} method generates lambda expressions
 * or method references appropriate for each operation type. It handles:
 * <ul>
 * <li>Parameter naming based on variable tracking</li>
 * <li>Identity element generation for reducers</li>
 * <li>Method reference optimization (e.g., Integer::sum vs explicit lambda)</li>
 * <li>Expression copying and AST node creation</li>
 * </ul>
 * </p>
 * 
 * <p><b>Thread Safety:</b> This class is not thread-safe.</p>
 * 
 * <p>
 * This class is final to prevent subclassing and potential finalizer attacks,
 * since constructors call analysis methods that could potentially throw
 * exceptions.
 * </p>
 * 
 * @see StreamPipelineBuilder
 * @see OperationType
 * @see ReducerType
 * @see StreamConstants
 */
public final class ProspectiveOperation {
	/**
	 * The original expression being analyzed or transformed.
	 * <p>
	 * This is set directly when the {@link ProspectiveOperation} is constructed
	 * with an {@link Expression}. If constructed with a
	 * {@link org.eclipse.jdt.core.dom.Statement}, this is set to the expression
	 * contained within the statement (if applicable, e.g., for
	 * {@link org.eclipse.jdt.core.dom.ExpressionStatement}).
	 */
	private Expression originalExpression;

	/**
	 * The original statement being analyzed or transformed.
	 * <p>
	 * This is set when the {@link ProspectiveOperation} is constructed with a
	 * {@link org.eclipse.jdt.core.dom.Statement}. If the statement is an
	 * {@link org.eclipse.jdt.core.dom.ExpressionStatement}, its expression is also
	 * extracted and stored in {@link #originalExpression}. Otherwise,
	 * {@link #originalExpression} may be null.
	 */
	private org.eclipse.jdt.core.dom.Statement originalStatement;

	private OperationType operationType;
	private Set<String> neededVariables = new HashSet<>();
	/**
	 * The name of the loop variable associated with this operation, if applicable.
	 * <p>
	 * This is set when the {@link ProspectiveOperation} is constructed with a
	 * statement and a loop variable name. It is used to track the variable iterated
	 * over in enhanced for-loops or similar constructs.
	 */
	private String loopVariableName;

	/**
	 * The name of the variable produced by this operation (for MAP operations).
	 * This is used to track variable names through the stream pipeline.
	 */
	private String producedVariableName;

	/**
	 * The name of the accumulator variable for REDUCE operations. Used to track
	 * which variable is being accumulated (e.g., "i" in "i++").
	 */
	private String accumulatorVariableName;

	/**
	 * The reducer type for REDUCE operations (INCREMENT, DECREMENT, SUM, etc.).
	 */
	private ReducerType reducerType;

	/**
	 * The type of the accumulator variable for REDUCE operations (e.g., "int", "double", "long").
	 * Used to generate the correct method reference (Integer::sum vs Double::sum).
	 */
	private String accumulatorType;

	/**
	 * Indicates if this operation is null-safe (e.g., variables are annotated with @NotNull).
	 * When true for STRING_CONCAT, String::concat method reference can be used safely.
	 */
	private boolean isNullSafe = false;

	/**
	 * The collector type for COLLECT operations (TO_LIST, TO_SET, etc.).
	 */
	private CollectorType collectorType;

	/**
	 * The name of the target collection variable for COLLECT operations (e.g., "result" in "result.add(item)").
	 */
	private String targetVariable;

	/**
	 * Set of variables consumed by this operation. Used for tracking variable scope
	 * and preventing leaks.
	 */
	private Set<String> consumedVariables = new HashSet<>();

	/**
	 * Collection of variable names already in use in the scope. Used to generate
	 * unique lambda parameter names that don't clash with existing variables.
	 */
	private Collection<String> usedVariableNames = new HashSet<>();

	// Collects all used/referenced variables
	private void collectNeededVariables(Expression expression) {
		if (expression == null)
			return;
		expression.accept(new ASTVisitor() {
			@Override
			public boolean visit(SimpleName node) {
				// Only collect SimpleName nodes that are actual variable references,
				// not part of qualified names (e.g., System.out) or method/field names
				ASTNode parent = node.getParent();
				
				// Skip if this is any part of a qualified name (e.g., "System" or "out" in "System.out")
				if (parent instanceof org.eclipse.jdt.core.dom.QualifiedName) {
					// Skip both qualifier and name parts of qualified names
					return super.visit(node);
				}
				
				// Skip if this is any part of a field access (e.g., explicit field accesses)
				if (parent instanceof org.eclipse.jdt.core.dom.FieldAccess) {
					// Skip both the expression (qualifier) and the name (field name)
					return super.visit(node);
				}
				
				// Skip if this is the name part of a method invocation (e.g., "println" in "out.println()")
				if (parent instanceof MethodInvocation) {
					MethodInvocation mi = (MethodInvocation) parent;
					if (mi.getName() == node) {
						return super.visit(node); // Skip method name
					}
					// Skip class names used as method invocation receivers (e.g., "Math" in "Math.max()")
					// but NOT variable references (e.g., "list" in "list.forEach()")
					if (mi.getExpression() == node) {
						IBinding binding = node.resolveBinding();
						// If it's a type binding (class name), skip it
						// If it's a variable binding, we want to collect it
						if (binding != null && !(binding instanceof IVariableBinding)) {
							return super.visit(node); // Skip class name
						}
						// Fallback: If binding resolution failed, check if the name starts with uppercase
						// (convention for class names in Java) and has no local variable conflict
						if (binding == null) {
							String name = node.getIdentifier();
							if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) {
								// Likely a class name like Math, System, Integer, etc.
								return super.visit(node);
							}
						}
					}
				}
				
				// Skip if this is part of a type reference (e.g., class names)
				if (parent instanceof org.eclipse.jdt.core.dom.Type) {
					return super.visit(node);
				}
				
				// Skip if this is the type name in a constructor invocation (e.g., "MyClass" in "new MyClass()")
				if (parent instanceof org.eclipse.jdt.core.dom.ClassInstanceCreation) {
					return super.visit(node);
				}
				
				// Skip if this is the name of a type declaration (e.g., class or interface name)
				if (parent instanceof org.eclipse.jdt.core.dom.TypeDeclaration) {
					org.eclipse.jdt.core.dom.TypeDeclaration typeDecl = (org.eclipse.jdt.core.dom.TypeDeclaration) parent;
					if (typeDecl.getName() == node) {
						return super.visit(node);
					}
				}
				
				// Otherwise, this is a variable reference - collect it
				neededVariables.add(node.getIdentifier());
				return super.visit(node);
			}
		});
	}

	/**
	 * Constructor for operations with an expression.
	 * 
	 * @param expression    the expression to process (must not be null)
	 * @param operationType the type of operation (must not be null)
	 */
	public ProspectiveOperation(Expression expression, OperationType operationType) {
		assert expression != null : "expression cannot be null";
		assert operationType != null : "operationType cannot be null";

		this.originalExpression = expression;
		this.operationType = operationType;
		collectNeededVariables(expression);
		updateConsumedVariables();
	}

	/**
	 * Constructor for operations with a statement.
	 * 
	 * @param statement     the statement to process (must not be null)
	 * @param operationType the type of operation (MAP, FOREACH, etc.) (must not be
	 *                      null)
	 * @param loopVarName   the loop variable name; for side-effect MAP operations,
	 *                      this represents the variable to be returned in the
	 *                      lambda body (may be the current variable name in the
	 *                      pipeline, not necessarily the original loop variable)
	 */
	public ProspectiveOperation(org.eclipse.jdt.core.dom.Statement statement, OperationType operationType,
			String loopVarName) {
		assert statement != null : "statement cannot be null";
		assert operationType != null : "operationType cannot be null";

		this.originalStatement = statement;
		this.operationType = operationType;
		this.loopVariableName = loopVarName;
		if (statement instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
			this.originalExpression = ((org.eclipse.jdt.core.dom.ExpressionStatement) statement).getExpression();
			collectNeededVariables(this.originalExpression);
		}
		updateConsumedVariables();
	}

	/**
	 * Constructor for MAP operations with a produced variable name. Used when a
	 * variable declaration creates a new variable in the stream pipeline.
	 * 
	 * @param expression      the expression that produces the new variable (must
	 *                        not be null)
	 * @param operationType   the type of operation (should be MAP) (must not be
	 *                        null)
	 * @param producedVarName the name of the variable produced by this operation
	 */
	public ProspectiveOperation(Expression expression, OperationType operationType, String producedVarName) {
		assert expression != null : "expression cannot be null";
		assert operationType != null : "operationType cannot be null";

		this.originalExpression = expression;
		this.operationType = operationType;
		this.producedVariableName = producedVarName;
		collectNeededVariables(expression);
		updateConsumedVariables();
	}

	/**
	 * Constructor for REDUCE operations with accumulator variable and reducer type.
	 * Used when a reducer pattern (i++, sum += x, etc.) is detected.
	 * 
	 * @param statement          the statement containing the reducer (must not be
	 *                           null)
	 * @param accumulatorVarName the name of the accumulator variable (e.g., "i",
	 *                           "sum") (must not be null)
	 * @param reducerType        the type of reducer (INCREMENT, SUM, etc.) (must
	 *                           not be null)
	 */
	public ProspectiveOperation(org.eclipse.jdt.core.dom.Statement statement, String accumulatorVarName,
			ReducerType reducerType) {
		assert statement != null : "statement cannot be null";
		assert accumulatorVarName != null : "accumulatorVarName cannot be null";
		assert reducerType != null : "reducerType cannot be null";

		this.originalStatement = statement;
		this.operationType = OperationType.REDUCE;
		this.accumulatorVariableName = accumulatorVarName;
		this.reducerType = reducerType;
		if (statement instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
			this.originalExpression = ((org.eclipse.jdt.core.dom.ExpressionStatement) statement).getExpression();
			collectNeededVariables(this.originalExpression);
		}
		updateConsumedVariables();
	}

	/** Returns the operation type. */
	public OperationType getOperationType() {
		return this.operationType;
	}

	/**
	 * Generate the lambda arguments for this operation Based on NetBeans
	 * ProspectiveOperation.getArguments()
	 * 
	 * @param ast       the AST to create nodes in (must not be null)
	 * @param paramName the parameter name to use for the lambda (may be null,
	 *                  defaults to "item")
	 * @return a list of expressions to use as arguments for the stream method
	 * @throws IllegalArgumentException if ast is null
	 */
	public List<Expression> getArguments(AST ast, String paramName) {
		if (ast == null) {
			throw new IllegalArgumentException("ast cannot be null");
		}

		List<Expression> args = new ArrayList<>();

		// REDUCE and COLLECT have special argument handling
		if (operationType.hasSpecialArgumentHandling()) {
			if (operationType == OperationType.REDUCE) {
				return getArgumentsForReducer(ast);
			} else if (operationType == OperationType.COLLECT) {
				return getArgumentsForCollector(ast);
			}
		}

		// Create lambda expression for MAP, FILTER, FOREACH, ANYMATCH, NONEMATCH, ALLMATCH
		LambdaExpression lambda = ast.newLambdaExpression();

		// Create parameter with defensive null check
		// Use the provided paramName, or generate a unique default name
		VariableDeclarationFragment param = ast.newVariableDeclarationFragment();
		String effectiveParamName;
		if (paramName != null && !paramName.isEmpty()) {
			effectiveParamName = paramName;
		} else {
			// Generate a unique default name to avoid clashes
			effectiveParamName = generateUniqueVariableName("item");
		}
		param.setName(ast.newSimpleName(effectiveParamName));
		lambda.parameters().add(param);
		
		// For single parameter without type annotation, don't use parentheses
		lambda.setParentheses(false);

		// Create lambda body using OperationType delegation
		OperationType.LambdaBodyContext context = new OperationType.LambdaBodyContext(
				originalExpression, originalStatement, loopVariableName);
		ASTNode lambdaBody = operationType.createLambdaBody(ast, context);
		
		if (lambdaBody != null) {
			lambda.setBody(lambdaBody);
		} else if (originalExpression != null) {
			// Fallback: use expression as body
			lambda.setBody(ASTNode.copySubtree(ast, originalExpression));
		} else {
			// Defensive: neither originalExpression nor originalStatement is available
			throw new IllegalStateException(
					"Cannot create lambda: both originalExpression and originalStatement are null for operationType "
							+ operationType);
		}

		args.add(lambda);
		return args;
	}

	/**
	 * Returns the variable name produced by this operation (for MAP operations).
	 * This is used to track variable names through the stream pipeline.
	 */
	public String getProducedVariableName() {
		return producedVariableName;
	}

	/**
	 * Returns the accumulator variable name for REDUCE operations.
	 * 
	 * @return the accumulator variable name, or null if not a REDUCE operation
	 */
	public String getAccumulatorVariableName() {
		return accumulatorVariableName;
	}

	/**
	 * Returns the reducer type for REDUCE operations.
	 * 
	 * @return the reducer type, or null if not a REDUCE operation
	 */
	public ReducerType getReducerType() {
		return reducerType;
	}

	/**
	 * Sets whether this operation is null-safe.
	 * 
	 * @param isNullSafe true if the operation is null-safe
	 */
	public void setNullSafe(boolean isNullSafe) {
		this.isNullSafe = isNullSafe;
	}

	/**
	 * Returns whether this operation is null-safe.
	 * 
	 * @return true if the operation is null-safe
	 */
	public boolean isNullSafe() {
		return isNullSafe;
	}

	/**
	 * Sets the collector type for COLLECT operations.
	 * 
	 * @param collectorType the collector type (TO_LIST, TO_SET, etc.)
	 */
	public void setCollectorType(CollectorType collectorType) {
		this.collectorType = collectorType;
	}

	/**
	 * Returns the collector type for COLLECT operations.
	 * 
	 * @return the collector type, or null if not a COLLECT operation
	 */
	public CollectorType getCollectorType() {
		return collectorType;
	}

	/**
	 * Sets the target collection variable for COLLECT operations.
	 * 
	 * @param targetVariable the target variable name (e.g., "result")
	 */
	public void setTargetVariable(String targetVariable) {
		this.targetVariable = targetVariable;
	}

	/**
	 * Returns the target collection variable for COLLECT operations.
	 * 
	 * @return the target variable name, or null if not a COLLECT operation
	 */
	public String getTargetVariable() {
		return targetVariable;
	}

	/**
	 * Sets the accumulator type for REDUCE operations.
	 * This is used to generate the correct method reference (e.g., Integer::sum vs Double::sum).
	 * 
	 * @param accumulatorType the type of the accumulator variable (e.g., "int", "double", "long")
	 */
	public void setAccumulatorType(String accumulatorType) {
		this.accumulatorType = accumulatorType;
	}

	/**
	 * Returns the accumulator type for REDUCE operations.
	 * 
	 * @return the accumulator type (e.g., "int", "double", "long"), or null if not set
	 */
	public String getAccumulatorType() {
		return accumulatorType;
	}

	/**
	 * Sets the collection of variable names already in use in the current scope.
	 * This is used to generate unique lambda parameter names that don't clash
	 * with existing variables.
	 * 
	 * @param usedNames the collection of variable names in use (may be null)
	 */
	public void setUsedVariableNames(Collection<String> usedNames) {
		if (usedNames != null) {
			this.usedVariableNames = usedNames;
		}
	}

	/**
	 * Generates a unique variable name that doesn't collide with existing variables in scope.
	 * 
	 * <p>This method ensures the generated lambda parameter name doesn't conflict with other
	 * variables visible at the transformation point. If the base name is already in use,
	 * a numeric suffix is appended (e.g., "a2", "a3", etc.).</p>
	 * 
	 * @param baseName the base name to use (e.g., "a", "_item", "accumulator")
	 * @return a unique variable name that doesn't exist in the current scope
	 */
	private String generateUniqueVariableName(String baseName) {
		// Combine neededVariables (from expression analysis) with usedVariableNames (from scope)
		Set<String> allUsedNames = new HashSet<>(neededVariables);
		allUsedNames.addAll(usedVariableNames);
		
		// If base name is not used, return it
		if (!allUsedNames.contains(baseName)) {
			return baseName;
		}
		
		// Otherwise, append a number until we find an unused name
		int counter = 2;
		String candidate = baseName + counter;
		while (allUsedNames.contains(candidate)) {
			counter++;
			candidate = baseName + counter;
		}
		return candidate;
	}

	/**
	 * Returns the set of variables consumed by this operation. This includes all
	 * SimpleName references in the operation's expression.
	 * 
	 * @return the set of consumed variable names
	 */
	public Set<String> getConsumedVariables() {
		return consumedVariables;
	}

	/**
	 * Updates the consumed variables set by collecting all SimpleName references.
	 * This is called during operation construction to track variable usage.
	 */
	private void updateConsumedVariables() {
		consumedVariables.addAll(neededVariables);
	}

	/** Determines the arguments for reduce() operation. */
	private List<Expression> getArgumentsForReducer(AST ast) {
		List<Expression> arguments = new ArrayList<>();
		if (operationType == OperationType.REDUCE) {
			// First argument: identity element (the accumulator variable reference)
			if (accumulatorVariableName != null) {
				arguments.add(ast.newSimpleName(accumulatorVariableName));
			} else {
				// Fallback to default identity
				Expression identity = getIdentityElement(ast);
				if (identity != null)
					arguments.add(identity);
			}

			// Second argument: accumulator function (method reference or lambda)
			Expression accumulator = createAccumulatorExpression(ast);
			if (accumulator != null)
				arguments.add(accumulator);
		}
		return arguments;
	}

	/**
	 * Determines the arguments for collect() operation.
	 * Returns a single argument: the Collectors method invocation (e.g., Collectors.toList()).
	 */
	private List<Expression> getArgumentsForCollector(AST ast) {
		List<Expression> arguments = new ArrayList<>();
		if (operationType == OperationType.COLLECT && collectorType != null) {
			// Single argument: Collectors.toList() or Collectors.toSet()
			Expression collector = collectorType.createCollectorExpression(ast);
			arguments.add(collector);
		}
		return arguments;
	}

	/**
	 * Creates the accumulator expression for REDUCE operations. Returns a method
	 * reference (e.g., Integer::sum, Long::sum, Double::sum) when possible, or a lambda otherwise.
	 * The method reference type is determined by the accumulator variable type.
	 */
	private Expression createAccumulatorExpression(AST ast) {
		LambdaGenerator generator = new LambdaGenerator(ast);
		generator.setNeededVariables(neededVariables);
		generator.setUsedVariableNames(usedVariableNames);
		return generator.createAccumulatorExpression(reducerType, accumulatorType, isNullSafe);
	}

	/** Determines the identity element (0, 1) for reduce() operation. */
	private Expression getIdentityElement(AST ast) {
		if (operationType == OperationType.REDUCE && originalExpression instanceof Assignment) {
			Assignment assignment = (Assignment) originalExpression;
			if (assignment.getOperator() == Assignment.Operator.PLUS_ASSIGN) {
				return ast.newNumberLiteral("0");
			} else if (assignment.getOperator() == Assignment.Operator.TIMES_ASSIGN) {
				return ast.newNumberLiteral("1");
			}
		}
		return null;
	}

	@Override
	public String toString() {
		return "ProspectiveOperation{" + "expression=" + originalExpression + ", operationType=" + operationType
				+ ", neededVariables=" + neededVariables + '}';
	}
}