ContainerSignatureMigrationPlan.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
 *******************************************************************************/
package org.sandbox.jdt.container.api;

import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Plan describing which method signatures must change atomically for one semantic
 * container migration.
 */
public record ContainerSignatureMigrationPlan(
		TargetContainerContract targetContract,
		List<SignatureAtomicityGroup> groups,
		PlanningStatus status,
		List<SignatureDiagnostic> diagnostics) {

	public ContainerSignatureMigrationPlan {
		Objects.requireNonNull(targetContract, "targetContract"); //$NON-NLS-1$
		groups= List.copyOf(Objects.requireNonNull(groups, "groups")); //$NON-NLS-1$
		Objects.requireNonNull(status, "status"); //$NON-NLS-1$
		diagnostics= List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); //$NON-NLS-1$
		validateUniqueGroups(groups);
		if (status == PlanningStatus.REJECTED && diagnostics.isEmpty()) {
			throw new IllegalArgumentException("A rejected signature plan requires diagnostics"); //$NON-NLS-1$
		}
		if (status == PlanningStatus.CLOSED_SOURCE_AUTOMATIC
				&& (groups.isEmpty() || !diagnostics.isEmpty())) {
			throw new IllegalArgumentException(
					"A closed-source automatic signature plan requires groups without diagnostics"); //$NON-NLS-1$
		}
	}

	/** One parameter or return position that must move as one hierarchy-wide unit. */
	public record SignatureAtomicityGroup(
			String groupId,
			PositionKind positionKind,
			int signatureIndex,
			List<SignatureMember> members,
			BridgeFeasibility bridgeFeasibility,
			String explanation) {

		public SignatureAtomicityGroup {
			groupId= requiredText(groupId, "groupId"); //$NON-NLS-1$
			Objects.requireNonNull(positionKind, "positionKind"); //$NON-NLS-1$
			if (positionKind == PositionKind.PARAMETER && signatureIndex < 0) {
				throw new IllegalArgumentException("A parameter group requires an index"); //$NON-NLS-1$
			}
			if (positionKind == PositionKind.RETURN && signatureIndex != -1) {
				throw new IllegalArgumentException("A return group must use signature index -1"); //$NON-NLS-1$
			}
			members= List.copyOf(Objects.requireNonNull(members, "members")); //$NON-NLS-1$
			if (members.isEmpty()) {
				throw new IllegalArgumentException("A signature group requires members"); //$NON-NLS-1$
			}
			validateUniqueMembers(members);
			Objects.requireNonNull(bridgeFeasibility, "bridgeFeasibility"); //$NON-NLS-1$
			explanation= requiredText(explanation, "explanation"); //$NON-NLS-1$
		}
	}

	/** One exact method declaration in an atomic signature group. */
	public record SignatureMember(
			String javaElementHandle,
			String ownerKey,
			String compilationUnitHandle,
			String flowNodeId) {

		public SignatureMember {
			javaElementHandle= requiredText(javaElementHandle, "javaElementHandle"); //$NON-NLS-1$
			ownerKey= optionalText(ownerKey);
			compilationUnitHandle= requiredText(
					compilationUnitHandle, "compilationUnitHandle"); //$NON-NLS-1$
			flowNodeId= requiredText(flowNodeId, "flowNodeId"); //$NON-NLS-1$
		}
	}

	/** One reason why a complete atomic group could not be established. */
	public record SignatureDiagnostic(
			DiagnosticKind kind,
			String sourceNodeId,
			String javaElementHandle,
			String message) {

		public SignatureDiagnostic {
			Objects.requireNonNull(kind, "kind"); //$NON-NLS-1$
			sourceNodeId= requiredText(sourceNodeId, "sourceNodeId"); //$NON-NLS-1$
			javaElementHandle= optionalText(javaElementHandle);
			message= requiredText(message, "message"); //$NON-NLS-1$
		}
	}

	public enum PositionKind {
		PARAMETER,
		RETURN
	}

	public enum BridgeFeasibility {
		/** The JVM/Java signatures can coexist, but semantic adaptation requires policy. */
		OVERLOAD_POSSIBLE_POLICY_REQUIRED,
		/** Java cannot overload two methods solely by return type. */
		SAME_NAME_RETURN_BRIDGE_IMPOSSIBLE
	}

	public enum PlanningStatus {
		NO_SIGNATURE_CHANGE,
		/** All source declarations and uses are closed and may be changed directly. */
		CLOSED_SOURCE_AUTOMATIC,
		/** Signatures are known, but compatibility or execution policy is still required. */
		REPORT_ONLY,
		REJECTED
	}

	public enum DiagnosticKind {
		FLOW_NOT_CLOSED,
		MISSING_SIGNATURE_NODE,
		AMBIGUOUS_SIGNATURE_NODE,
		MISSING_METHOD_HANDLE,
		UNSUPPORTED_AUTOMATIC_GROUP
	}

	private static void validateUniqueGroups(List<SignatureAtomicityGroup> groups) {
		Set<String> ids= HashSet.newHashSet(groups.size());
		for (SignatureAtomicityGroup group : groups) {
			if (!ids.add(group.groupId())) {
				throw new IllegalArgumentException("Duplicate signature group: " + group.groupId()); //$NON-NLS-1$
			}
		}
	}

	private static void validateUniqueMembers(List<SignatureMember> members) {
		Set<String> handles= HashSet.newHashSet(members.size());
		for (SignatureMember member : members) {
			if (!handles.add(member.javaElementHandle())) {
				throw new IllegalArgumentException(
						"Duplicate signature member: " + member.javaElementHandle()); //$NON-NLS-1$
			}
		}
	}

	private static String requiredText(String value, String fieldName) {
		String text= Objects.requireNonNull(value, fieldName).strip();
		if (text.isEmpty()) {
			throw new IllegalArgumentException(fieldName + " must not be empty"); //$NON-NLS-1$
		}
		return text;
	}

	private static String optionalText(String value) {
		return value == null ? "" : value.strip(); //$NON-NLS-1$
	}
}