IntToEnumHelper.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.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.lang.model.SourceVersion;

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.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.IfStatement;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SwitchStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperationWithSourceRange;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.common.ReferenceHolder;
import org.sandbox.jdt.internal.corext.fix.IntToEnumFixCore;

/**
 * Converts an enum-like group of private integer constants used by a private
 * method parameter in an if/else chain to a nested enum.
 *
 * <p>The detector deliberately accepts only candidates whose complete data flow
 * can be proven inside the current compilation unit. In particular, constants
 * and the migrated method must be private, every use of the state parameter must
 * be one of the recognised equality comparisons, and every call site must pass
 * one of the recognised constants. This keeps the ordinary single-file cleanup
 * safe while leaving project-wide API migrations to a future multi-file
 * refactoring.</p>
 */
public class IntToEnumHelper extends AbstractTool<ReferenceHolder<Integer, IntToEnumHelper.IntConstantHolder>> {

	/** Data required to perform one int-to-enum migration. */
	public static class IntConstantHolder {
		/** Root if-statement of the recognised chain. */
		public IfStatement ifStatement;
		/** Switch statement used by the switch-specific helper. */
		public SwitchStatement switchStatement;
		/** Map of constant names to their field declarations. */
		public Map<String, FieldDeclaration> constantFields = new LinkedHashMap<>();
		/** Constant names in declaration order. */
		public List<String> constantNames = new ArrayList<>();
		/** Name of the migrated state parameter. */
		public String comparedVariable;
		/** Set of nodes already processed by the cleanup. */
		public Set<ASTNode> nodesProcessed;
		/** Private method containing the state parameter. */
		public MethodDeclaration method;
		/** Parameter whose type is changed from int to the generated enum. */
		public SingleVariableDeclaration parameter;
		/** Type into which the nested enum is inserted. */
		public TypeDeclaration enclosingType;
		/** Constant expressions in conditions and call sites to replace. */
		public Map<Expression, String> constantReferences = new LinkedHashMap<>();
	}

	private static final class ConstantInfo {
		private final String name;
		private final FieldDeclaration field;
		private final int value;

		private ConstantInfo(String name, FieldDeclaration field, int value) {
			this.name = name;
			this.field = field;
			this.value = value;
		}
	}

	private static final class Comparison {
		private final IVariableBinding stateBinding;
		private final Expression stateExpression;
		private final ConstantInfo constant;
		private final Expression constantExpression;

		private Comparison(IVariableBinding stateBinding, Expression stateExpression, ConstantInfo constant,
				Expression constantExpression) {
			this.stateBinding = stateBinding;
			this.stateExpression = stateExpression;
			this.constant = constant;
			this.constantExpression = constantExpression;
		}
	}

	private static final class Candidate {
		private final IntConstantHolder holder = new IntConstantHolder();
		private final List<IfStatement> chain = new ArrayList<>();
		private final Set<Expression> stateReferences = new LinkedHashSet<>();
		private IVariableBinding stateBinding;
		private IMethodBinding methodBinding;
		private int parameterIndex;
	}

	@Override
	public void find(IntToEnumFixCore fixcore, CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperationWithSourceRange> operations, Set<ASTNode> nodesprocessed) {
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(IfStatement node) {
				if (nodesprocessed.contains(node) || isElseIf(node)) {
					return true;
				}

				Candidate candidate = createCandidate(compilationUnit, node);
				if (candidate == null) {
					return true;
				}

				candidate.holder.nodesProcessed = nodesprocessed;
				ReferenceHolder<Integer, IntConstantHolder> dataholder = new ReferenceHolder<>();
				dataholder.put(0, candidate.holder);
				operations.add(fixcore.rewrite(dataholder));
				nodesprocessed.addAll(candidate.chain);
				return false;
			}
		});
	}

	private static Candidate createCandidate(CompilationUnit compilationUnit, IfStatement root) {
		MethodDeclaration method = findEnclosingMethod(root);
		TypeDeclaration enclosingType = findEnclosingType(root);
		if (method == null || enclosingType == null || !Modifier.isPrivate(method.getModifiers())) {
			return null;
		}

		Map<String, ConstantInfo> constantsByBinding = collectPrivateIntConstants(enclosingType);
		if (constantsByBinding.size() < 2) {
			return null;
		}

		Candidate candidate = new Candidate();
		Set<String> usedNames = new LinkedHashSet<>();
		IfStatement current = root;
		while (current != null) {
			Comparison comparison = parseComparison(current.getExpression(), constantsByBinding);
			if (comparison == null) {
				return null;
			}
			if (candidate.stateBinding == null) {
				candidate.stateBinding = comparison.stateBinding;
			} else if (!sameVariable(candidate.stateBinding, comparison.stateBinding)) {
				return null;
			}
			if (!usedNames.add(comparison.constant.name)) {
				return null;
			}

			candidate.chain.add(current);
			candidate.stateReferences.add(comparison.stateExpression);
			candidate.holder.constantReferences.put(comparison.constantExpression, comparison.constant.name);
			current = current.getElseStatement() instanceof IfStatement next ? next : null;
		}

		if (usedNames.size() < 2) {
			return null;
		}

		SingleVariableDeclaration parameter = findParameter(method, candidate.stateBinding);
		if (parameter == null || !isPlainInt(parameter)) {
			return null;
		}

		IMethodBinding methodBinding = method.resolveBinding();
		if (methodBinding == null) {
			return null;
		}
		candidate.methodBinding = methodBinding.getMethodDeclaration();
		candidate.parameterIndex = findParameterIndex(method, parameter);
		if (candidate.parameterIndex < 0) {
			return null;
		}

		String prefix = SwitchIntToEnumHelper.findCommonPrefix(new ArrayList<>(usedNames));
		if (prefix == null) {
			return null;
		}
		String enumName = SwitchIntToEnumHelper.prefixToEnumName(prefix);
		if (!isValidIdentifier(enumName) || hasNestedTypeNamed(enclosingType, enumName)) {
			return null;
		}

		Map<String, ConstantInfo> enumConstants = constantsForPrefix(constantsByBinding, prefix);
		Set<String> enumConstantNames = new LinkedHashSet<>();
		for (ConstantInfo info : enumConstants.values()) {
			enumConstantNames.add(info.name);
			String enumConstantName = info.name.substring(prefix.length());
			if (!isValidIdentifier(enumConstantName)) {
				return null;
			}
		}
		if (enumConstants.size() < 2 || !enumConstantNames.containsAll(usedNames)
				|| !hasDistinctValues(enumConstants.values())) {
			return null;
		}

		candidate.holder.ifStatement = root;
		candidate.holder.method = method;
		candidate.holder.parameter = parameter;
		candidate.holder.enclosingType = enclosingType;
		candidate.holder.comparedVariable = parameter.getName().getIdentifier();
		for (ConstantInfo info : enumConstants.values()) {
			candidate.holder.constantNames.add(info.name);
			candidate.holder.constantFields.put(info.name, info.field);
		}

		if (!collectCallSiteReplacements(compilationUnit, candidate, enumConstants)
				|| !validateAllReferences(compilationUnit, candidate, enumConstants)) {
			return null;
		}
		return candidate;
	}

	private static boolean collectCallSiteReplacements(CompilationUnit compilationUnit, Candidate candidate,
			Map<String, ConstantInfo> enumConstants) {
		AtomicBoolean valid = new AtomicBoolean(true);
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(MethodInvocation node) {
				if (!valid.get()) {
					return false;
				}
				IMethodBinding binding = node.resolveMethodBinding();
				if (binding != null) {
					if (!candidate.methodBinding.isEqualTo(binding.getMethodDeclaration())) {
						return true;
					}
				} else if (!isSafelyRecoverableLocalInvocation(node, candidate)) {
					return true;
				}
				if (candidate.parameterIndex >= node.arguments().size()) {
					valid.set(false);
					return false;
				}
				Expression argument = (Expression) node.arguments().get(candidate.parameterIndex);
				ConstantInfo constant = resolveConstant(argument, enumConstants);
				if (constant == null) {
					constant = recoverUnresolvedConstant(argument, candidate, enumConstants);
				}
				if (constant == null) {
					valid.set(false);
					return false;
				}
				candidate.holder.constantReferences.put(unparenthesize(argument), constant.name);
				return true;
			}
		});
		return valid.get();
	}

	/**
	 * Returns whether an invocation whose JDT binding is temporarily unavailable can
	 * still be identified without ambiguity as the private local method being
	 * migrated. The recovery is intentionally limited to the one-parameter form
	 * from a method or constructor declared directly in the same type. More complex
	 * signatures, nested/anonymous types and overloads stay fail-closed.
	 */
	private static boolean isSafelyRecoverableLocalInvocation(MethodInvocation invocation, Candidate candidate) {
		if (candidate.holder.method.parameters().size() != 1
				|| invocation.getExpression() != null
				|| !invocation.getName().getIdentifier().equals(candidate.holder.method.getName().getIdentifier())
				|| invocation.arguments().size() != 1
				|| findEnclosingType(invocation) != candidate.holder.enclosingType) {
			return false;
		}
		MethodDeclaration caller = findEnclosingMethod(invocation);
		if (caller == null || caller.getParent() != candidate.holder.enclosingType) {
			return false;
		}
		String methodName = candidate.holder.method.getName().getIdentifier();
		for (MethodDeclaration method : candidate.holder.enclosingType.getMethods()) {
			if (method != candidate.holder.method && method.getName().getIdentifier().equals(methodName)
					&& method.parameters().size() == 1) {
				return false;
			}
		}
		return true;
	}

	private static ConstantInfo recoverUnresolvedConstant(Expression argument, Candidate candidate,
			Map<String, ConstantInfo> enumConstants) {
		Expression unwrapped = unparenthesize(argument);
		if (!(unwrapped instanceof SimpleName simpleName)) {
			return null;
		}
		ConstantInfo match = null;
		for (ConstantInfo constant : enumConstants.values()) {
			if (constant.name.equals(simpleName.getIdentifier())) {
				match = constant;
				break;
			}
		}
		if (match == null) {
			return null;
		}
		MethodDeclaration enclosingMethod = findEnclosingMethod(simpleName);
		if (enclosingMethod == null || hasLocalDeclarationNamed(enclosingMethod, match.name)) {
			return null;
		}
		return match;
	}

	private static boolean hasLocalDeclarationNamed(MethodDeclaration method, String identifier) {
		AtomicBoolean found = new AtomicBoolean(false);
		method.accept(new ASTVisitor() {
			@Override
			public boolean visit(SingleVariableDeclaration node) {
				if (node.getName().getIdentifier().equals(identifier)) {
					found.set(true);
					return false;
				}
				return !found.get();
			}

			@Override
			public boolean visit(VariableDeclarationFragment node) {
				if (node.getName().getIdentifier().equals(identifier)) {
					found.set(true);
					return false;
				}
				return !found.get();
			}
		});
		return found.get();
	}

	private static boolean validateAllReferences(CompilationUnit compilationUnit, Candidate candidate,
			Map<String, ConstantInfo> enumConstants) {
		AtomicBoolean valid = new AtomicBoolean(true);
		Set<String> enumConstantNames = new HashSet<>();
		for (ConstantInfo constant : enumConstants.values()) {
			enumConstantNames.add(constant.name);
		}
		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(SimpleName node) {
				if (!valid.get()) {
					return false;
				}
				IBinding binding = node.resolveBinding();
				if (binding instanceof IVariableBinding variableBinding) {
					ConstantInfo constant = enumConstants.get(bindingKey(variableBinding));
					if (constant != null) {
						if (isDeclarationName(node)) {
							return true;
						}
						Expression reference = containingExpression(node);
						if (!candidate.holder.constantReferences.containsKey(reference)) {
							valid.set(false);
							return false;
						}
					} else if (sameVariable(candidate.stateBinding, variableBinding)
							&& node != candidate.holder.parameter.getName()
							&& !candidate.stateReferences.contains(containingExpression(node))) {
						valid.set(false);
						return false;
					}
				} else if (binding instanceof IMethodBinding methodBinding
						&& candidate.methodBinding.isEqualTo(methodBinding.getMethodDeclaration())
						&& node != candidate.holder.method.getName()
						&& !(node.getParent() instanceof MethodInvocation invocation && invocation.getName() == node)) {
					valid.set(false);
					return false;
				} else if (binding == null && !validateUnresolvedReference(node, candidate, enumConstantNames)) {
					valid.set(false);
					return false;
				}
				return true;
			}
		});
		return valid.get();
	}

	private static boolean validateUnresolvedReference(SimpleName node, Candidate candidate,
			Set<String> enumConstantNames) {
		String identifier = node.getIdentifier();
		if (enumConstantNames.contains(identifier)) {
			if (isCandidateConstantDeclaration(node, candidate)) {
				return true;
			}
			return candidate.holder.constantReferences.containsKey(containingExpression(node));
		}
		if (identifier.equals(candidate.holder.parameter.getName().getIdentifier())
				&& findEnclosingMethod(node) == candidate.holder.method
				&& node != candidate.holder.parameter.getName()) {
			return candidate.stateReferences.contains(containingExpression(node));
		}
		if (identifier.equals(candidate.holder.method.getName().getIdentifier())
				&& node != candidate.holder.method.getName()) {
			return node.getParent() instanceof MethodInvocation invocation && invocation.getName() == node
					&& isSafelyRecoverableLocalInvocation(invocation, candidate);
		}
		return true;
	}

	private static boolean isCandidateConstantDeclaration(SimpleName name, Candidate candidate) {
		if (!(name.getParent() instanceof VariableDeclarationFragment fragment) || fragment.getName() != name
				|| !(fragment.getParent() instanceof FieldDeclaration field)) {
			return false;
		}
		return candidate.holder.constantFields.get(name.getIdentifier()) == field;
	}

	private static Map<String, ConstantInfo> collectPrivateIntConstants(TypeDeclaration typeDeclaration) {
		Map<String, ConstantInfo> result = new LinkedHashMap<>();
		for (FieldDeclaration field : typeDeclaration.getFields()) {
			int modifiers = field.getModifiers();
			if (!Modifier.isPrivate(modifiers) || !Modifier.isStatic(modifiers) || !Modifier.isFinal(modifiers)
					|| !isInt(field.getType())) {
				continue;
			}
			for (Object fragmentObject : field.fragments()) {
				VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObject;
				IVariableBinding binding = fragment.resolveBinding();
				Object constantValue = binding == null ? null : binding.getConstantValue();
				String key = bindingKey(binding);
				if (key != null && constantValue instanceof Integer value) {
					result.put(key, new ConstantInfo(fragment.getName().getIdentifier(), field, value.intValue()));
				}
			}
		}
		return result;
	}

	private static Map<String, ConstantInfo> constantsForPrefix(Map<String, ConstantInfo> constantsByBinding,
			String prefix) {
		Map<String, ConstantInfo> result = new LinkedHashMap<>();
		for (Map.Entry<String, ConstantInfo> entry : constantsByBinding.entrySet()) {
			if (entry.getValue().name.startsWith(prefix)) {
				result.put(entry.getKey(), entry.getValue());
			}
		}
		return result;
	}

	private static Comparison parseComparison(Expression expression, Map<String, ConstantInfo> constantsByBinding) {
		Expression unwrapped = unparenthesize(expression);
		if (!(unwrapped instanceof InfixExpression infix)
				|| infix.getOperator() != InfixExpression.Operator.EQUALS
				|| !infix.extendedOperands().isEmpty()) {
			return null;
		}

		Expression left = unparenthesize(infix.getLeftOperand());
		Expression right = unparenthesize(infix.getRightOperand());
		ConstantInfo leftConstant = resolveConstant(left, constantsByBinding);
		ConstantInfo rightConstant = resolveConstant(right, constantsByBinding);
		IVariableBinding leftVariable = resolveVariable(left);
		IVariableBinding rightVariable = resolveVariable(right);

		if (leftConstant != null && rightVariable != null && rightConstant == null) {
			return new Comparison(rightVariable, right, leftConstant, left);
		}
		if (rightConstant != null && leftVariable != null && leftConstant == null) {
			return new Comparison(leftVariable, left, rightConstant, right);
		}
		return null;
	}

	private static ConstantInfo resolveConstant(Expression expression, Map<String, ConstantInfo> constantsByBinding) {
		IVariableBinding binding = resolveVariable(unparenthesize(expression));
		return binding == null ? null : constantsByBinding.get(bindingKey(binding));
	}

	private static IVariableBinding resolveVariable(Expression expression) {
		Expression unwrapped = unparenthesize(expression);
		IBinding binding = null;
		if (unwrapped instanceof Name name) {
			binding = name.resolveBinding();
		} else if (unwrapped instanceof FieldAccess fieldAccess) {
			binding = fieldAccess.resolveFieldBinding();
		}
		return binding instanceof IVariableBinding variableBinding ? variableBinding.getVariableDeclaration() : null;
	}

	private static Expression unparenthesize(Expression expression) {
		Expression current = expression;
		while (current instanceof ParenthesizedExpression parenthesized) {
			current = parenthesized.getExpression();
		}
		return current;
	}

	private static Expression containingExpression(SimpleName name) {
		ASTNode parent = name.getParent();
		if (parent instanceof QualifiedName qualifiedName && qualifiedName.getName() == name) {
			return qualifiedName;
		}
		if (parent instanceof FieldAccess fieldAccess && fieldAccess.getName() == name) {
			return fieldAccess;
		}
		return name;
	}

	private static boolean isDeclarationName(SimpleName name) {
		return name.getParent() instanceof VariableDeclarationFragment fragment && fragment.getName() == name;
	}

	private static SingleVariableDeclaration findParameter(MethodDeclaration method, IVariableBinding binding) {
		for (Object parameterObject : method.parameters()) {
			SingleVariableDeclaration parameter = (SingleVariableDeclaration) parameterObject;
			IVariableBinding parameterBinding = parameter.resolveBinding();
			if (sameVariable(binding, parameterBinding)) {
				return parameter;
			}
		}
		return null;
	}

	private static int findParameterIndex(MethodDeclaration method, SingleVariableDeclaration parameter) {
		for (int i = 0; i < method.parameters().size(); i++) {
			if (method.parameters().get(i) == parameter) {
				return i;
			}
		}
		return -1;
	}

	private static boolean isPlainInt(SingleVariableDeclaration parameter) {
		return !parameter.isVarargs() && parameter.extraDimensions().isEmpty() && isInt(parameter.getType());
	}

	private static boolean isInt(Type type) {
		return type.isPrimitiveType()
				&& ((PrimitiveType) type).getPrimitiveTypeCode() == PrimitiveType.INT;
	}

	private static boolean hasDistinctValues(Iterable<ConstantInfo> constants) {
		Set<Integer> values = new HashSet<>();
		for (ConstantInfo constant : constants) {
			if (!values.add(Integer.valueOf(constant.value))) {
				return false;
			}
		}
		return true;
	}

	private static boolean hasNestedTypeNamed(TypeDeclaration enclosingType, String name) {
		if (enclosingType.getName().getIdentifier().equals(name)) {
			return true;
		}
		for (Object declaration : enclosingType.bodyDeclarations()) {
			if (declaration instanceof AbstractTypeDeclaration typeDeclaration
					&& typeDeclaration.getName().getIdentifier().equals(name)) {
				return true;
			}
		}
		return false;
	}

	private static boolean isValidIdentifier(String name) {
		return SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name);
	}

	private static boolean sameVariable(IVariableBinding first, IVariableBinding second) {
		return first != null && second != null
				&& first.getVariableDeclaration().isEqualTo(second.getVariableDeclaration());
	}

	private static String bindingKey(IVariableBinding binding) {
		return binding == null ? null : binding.getVariableDeclaration().getKey();
	}

	private static boolean isElseIf(IfStatement statement) {
		return statement.getParent() instanceof IfStatement parent && parent.getElseStatement() == statement;
	}

	private static MethodDeclaration findEnclosingMethod(ASTNode node) {
		ASTNode current = node.getParent();
		while (current != null) {
			if (current instanceof MethodDeclaration methodDeclaration) {
				return methodDeclaration;
			}
			current = current.getParent();
		}
		return null;
	}

	private static TypeDeclaration findEnclosingType(ASTNode node) {
		ASTNode current = node.getParent();
		while (current != null) {
			if (current instanceof TypeDeclaration typeDeclaration) {
				return typeDeclaration;
			}
			current = current.getParent();
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	@Override
	public void rewrite(IntToEnumFixCore fixCore, ReferenceHolder<Integer, IntConstantHolder> holder,
			CompilationUnitRewrite cuRewrite, TextEditGroup group) {
		for (IntConstantHolder data : holder.values()) {
			if (data.ifStatement == null || data.parameter == null || data.enclosingType == null
					|| data.constantNames.size() < 2) {
				continue;
			}

			String prefix = SwitchIntToEnumHelper.findCommonPrefix(data.constantNames);
			if (prefix == null) {
				continue;
			}
			String enumName = SwitchIntToEnumHelper.prefixToEnumName(prefix);
			AST ast = cuRewrite.getRoot().getAST();
			ASTRewrite rewrite = cuRewrite.getASTRewrite();

			EnumDeclaration enumDeclaration = ast.newEnumDeclaration();
			enumDeclaration.setName(ast.newSimpleName(enumName));
			enumDeclaration.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PRIVATE_KEYWORD));
			for (String constantName : data.constantNames) {
				EnumConstantDeclaration enumConstant = ast.newEnumConstantDeclaration();
				enumConstant.setName(ast.newSimpleName(constantName.substring(prefix.length())));
				enumDeclaration.enumConstants().add(enumConstant);
			}

			ListRewrite bodyRewrite = rewrite.getListRewrite(data.enclosingType,
					TypeDeclaration.BODY_DECLARATIONS_PROPERTY);
			FieldDeclaration firstField = data.constantFields.values().iterator().next();
			bodyRewrite.insertBefore(enumDeclaration, firstField, group);
			removeConstantFields(rewrite, bodyRewrite, data, group);

			Type enumType = ast.newSimpleType(ast.newSimpleName(enumName));
			rewrite.replace(data.parameter.getType(), enumType, group);
			for (Map.Entry<Expression, String> reference : data.constantReferences.entrySet()) {
				String enumConstantName = reference.getValue().substring(prefix.length());
				Name replacement = ast.newName(enumName + "." + enumConstantName); //$NON-NLS-1$
				rewrite.replace(reference.getKey(), replacement, group);
			}
		}
	}

	private static void removeConstantFields(ASTRewrite rewrite, ListRewrite bodyRewrite, IntConstantHolder data,
			TextEditGroup group) {
		Set<FieldDeclaration> fieldsToRemove = new LinkedHashSet<>(data.constantFields.values());
		for (FieldDeclaration field : fieldsToRemove) {
			boolean removeWholeField = true;
			for (Object fragmentObject : field.fragments()) {
				VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObject;
				if (!data.constantFields.containsKey(fragment.getName().getIdentifier())) {
					removeWholeField = false;
					break;
				}
			}
			if (removeWholeField) {
				bodyRewrite.remove(field, group);
			} else {
				ListRewrite fragmentRewrite = rewrite.getListRewrite(field, FieldDeclaration.FRAGMENTS_PROPERTY);
				for (Object fragmentObject : field.fragments()) {
					VariableDeclarationFragment fragment = (VariableDeclarationFragment) fragmentObject;
					if (data.constantFields.containsKey(fragment.getName().getIdentifier())) {
						fragmentRewrite.remove(fragment, group);
					}
				}
			}
		}
	}

	@Override
	public String getPreview(boolean afterRefactoring) {
		if (!afterRefactoring) {
			return """
					private static final int STATUS_PENDING = 0;
					private static final int STATUS_APPROVED = 1;

					private void process(int status) {
					    if (status == STATUS_PENDING) {
					        handlePending();
					    } else if (status == STATUS_APPROVED) {
					        handleApproved();
					    }
					}
					"""; //$NON-NLS-1$
		}
		return """
				private enum Status {
				    PENDING, APPROVED
				}

				private void process(Status status) {
				    if (status == Status.PENDING) {
				        handlePending();
				    } else if (status == Status.APPROVED) {
				        handleApproved();
				    }
				}
				"""; //$NON-NLS-1$
	}
}