ClosedSourceParameterMigrationPlanner.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.analysis;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.DiagnosticKind;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.PlanningDiagnostic;
import org.sandbox.jdt.container.api.ClosedSourceParameterMigrationPlan.PlanningResult;
import org.sandbox.jdt.container.api.ContainerFlowComponent;
import org.sandbox.jdt.container.api.ContainerFlowComponent.LocatedFlowEdge;
import org.sandbox.jdt.container.api.ContainerFlowGraph.ClosureStatus;
import org.sandbox.jdt.container.api.ContainerFlowGraph.EdgeKind;
import org.sandbox.jdt.container.api.ContainerFlowGraph.FlowNode;
import org.sandbox.jdt.container.api.ContainerFlowGraph.NodeKind;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan;
import org.sandbox.jdt.container.api.ContainerLocalRewritePlan.ArgumentTransfer;
import org.sandbox.jdt.container.api.ContainerMigrationReadiness;
import org.sandbox.jdt.container.api.ContainerParameterRewritePlan;
import org.sandbox.jdt.container.api.ContainerRecommendation;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.PlanningStatus;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.PositionKind;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.SignatureAtomicityGroup;
import org.sandbox.jdt.container.api.ContainerSignatureMigrationPlan.SignatureMember;
import org.sandbox.jdt.container.api.ContainerUsageProfile;

/**
 * Builds the first aggregate closed-source caller/parameter rewrite plan.
 *
 * <p>The accepted topology remains deliberately narrow on the producer side: one
 * local append-array source and one or more direct {@code ARGUMENT_TO_PARAMETER}
 * edges. The parameter side may now be one complete source-resolved atomicity group,
 * including interface declarations and all editable implementations/overrides. Every
 * group member requires its own complete parameter usage profile before any member
 * plan is emitted.</p>
 */
public final class ClosedSourceParameterMigrationPlanner {

	private final ContainerLocalRewritePlanner localPlanner=
			new ContainerLocalRewritePlanner();
	private final ContainerParameterRewritePlanner parameterPlanner=
			new ContainerParameterRewritePlanner();

	/** Builds one immutable aggregate plan or complete rejection diagnostics. */
	public PlanningResult plan(
			ContainerFlowComponent component,
			ContainerSignatureMigrationPlan signaturePlan,
			ContainerRecommendation recommendation,
			ContainerMigrationReadiness readiness,
			List<ContainerUsageProfile> memberProfiles) {
		Objects.requireNonNull(component, "component"); //$NON-NLS-1$
		Objects.requireNonNull(signaturePlan, "signaturePlan"); //$NON-NLS-1$
		Objects.requireNonNull(recommendation, "recommendation"); //$NON-NLS-1$
		Objects.requireNonNull(readiness, "readiness"); //$NON-NLS-1$
		memberProfiles= List.copyOf(
				Objects.requireNonNull(memberProfiles, "memberProfiles")); //$NON-NLS-1$

		List<PlanningDiagnostic> diagnostics= new ArrayList<>();
		Topology topology= topology(component);
		if (topology == null) {
			diagnostics.add(diagnostic(
					DiagnosticKind.UNSUPPORTED_FLOW_TOPOLOGY,
					"The closed-source parameter rewrite requires one local caller, one or more source parameters, and only direct argument-to-parameter edges from that caller.")); //$NON-NLS-1$
			return PlanningResult.rejected(diagnostics);
		}
		Signature signature= signature(signaturePlan, topology.parameters());
		if (signature == null) {
			diagnostics.add(diagnostic(
					DiagnosticKind.SIGNATURE_PLAN_MISMATCH,
					"The automatic signature plan does not cover the complete source parameter atomicity group.")); //$NON-NLS-1$
			return PlanningResult.rejected(diagnostics);
		}

		ContainerUsageProfile callerProfile= profile(
				memberProfiles, topology.caller().bindingKey());
		if (callerProfile == null) {
			diagnostics.add(diagnostic(
					DiagnosticKind.PROFILE_NOT_FOUND,
					"The local caller profile must be present exactly once.")); //$NON-NLS-1$
			return PlanningResult.rejected(diagnostics);
		}
		if (!recommendation.sourceProfile().equals(callerProfile)
				|| !recommendation.targetContract().equals(signaturePlan.targetContract())) {
			diagnostics.add(diagnostic(
					DiagnosticKind.RECOMMENDATION_MISMATCH,
					"The recommendation does not describe the exact aggregate caller and signature target.")); //$NON-NLS-1$
			return PlanningResult.rejected(diagnostics);
		}

		Map<String, SignatureMember> membersByNodeId= signature.group().members().stream()
				.collect(Collectors.toMap(
						SignatureMember::flowNodeId,
						Function.identity(),
						(left, right) -> left,
						LinkedHashMap::new));
		List<ArgumentTransfer> transfers=
				new ArrayList<>(topology.argumentEdges().size());
		for (LocatedFlowEdge edge : topology.argumentEdges()) {
			SignatureMember member= membersByNodeId.get(edge.targetNodeId());
			if (member == null) {
				diagnostics.add(diagnostic(
						DiagnosticKind.SIGNATURE_PLAN_MISMATCH,
						"An argument edge targets a parameter outside the automatic signature group.")); //$NON-NLS-1$
				continue;
			}
			transfers.add(new ArgumentTransfer(
					member.javaElementHandle(),
					signature.group().signatureIndex(),
					edge.sourceStart(),
					edge.sourceLength()));
		}

		ContainerLocalRewritePlan.PlanningResult callerResult= localPlanner.plan(
				localComponent(topology.caller()),
				recommendation,
				readiness,
				transfers);
		if (!callerResult.ready()) {
			callerResult.diagnostics().forEach(item -> diagnostics.add(diagnostic(
					DiagnosticKind.LOCAL_REWRITE_REJECTED,
					item.kind() + ": " + item.message()))); //$NON-NLS-1$
		}

		List<ContainerParameterRewritePlan> parameterPlans=
				new ArrayList<>(signature.group().members().size());
		for (SignatureMember member : signature.group().members()) {
			FlowNode parameter= component.node(member.flowNodeId()).orElse(null);
			ContainerUsageProfile parameterProfile= parameter == null
					? null : profile(memberProfiles, parameter.bindingKey());
			if (parameter == null || parameterProfile == null) {
				diagnostics.add(diagnostic(
						DiagnosticKind.PROFILE_NOT_FOUND,
						"Every automatic signature member requires one exact parameter profile: "
								+ member.javaElementHandle())); //$NON-NLS-1$
				continue;
			}
			ContainerParameterRewritePlan.PlanningResult parameterResult=
					parameterPlanner.plan(
							component,
							signaturePlan,
							signature.group(),
							member,
							parameterProfile,
							readiness);
			if (!parameterResult.ready()) {
				parameterResult.diagnostics().forEach(item -> diagnostics.add(diagnostic(
						DiagnosticKind.PARAMETER_REWRITE_REJECTED,
						member.javaElementHandle() + ": " //$NON-NLS-1$
								+ item.kind() + ": " + item.message()))); //$NON-NLS-1$
				continue;
			}
			parameterPlans.add(parameterResult.plan().orElseThrow());
		}
		if (!diagnostics.isEmpty()) {
			return PlanningResult.rejected(diagnostics);
		}

		return PlanningResult.accepted(new ClosedSourceParameterMigrationPlan(
				recommendation.targetContract(),
				callerResult.plan().orElseThrow(),
				parameterPlans));
	}

	private static Topology topology(ContainerFlowComponent component) {
		if (component.closureStatus() != ClosureStatus.LOCAL_CLOSED
				|| !component.diagnostics().isEmpty()) {
			return null;
		}
		List<FlowNode> callers= component.nodes().stream()
				.filter(node -> node.kind() == NodeKind.LOCAL_VARIABLE)
				.toList();
		List<FlowNode> parameters= component.nodes().stream()
				.filter(node -> node.kind() == NodeKind.PARAMETER)
				.toList();
		if (callers.size() != 1 || parameters.isEmpty()
				|| component.nodes().size() != callers.size() + parameters.size()) {
			return null;
		}
		FlowNode caller= callers.get(0);
		if (!caller.sourceResolved() || caller.bindingKey().isBlank()
				|| caller.compilationUnitHandle().isBlank()) {
			return null;
		}
		if (parameters.stream().anyMatch(parameter ->
				!parameter.sourceResolved()
						|| parameter.bindingKey().isBlank()
						|| parameter.javaElementHandle().isBlank()
						|| parameter.compilationUnitHandle().isBlank()
						|| parameter.signatureIndex() < 0)) {
			return null;
		}
		Set<String> parameterIds= parameters.stream()
				.map(FlowNode::stableId)
				.collect(Collectors.toSet());
		List<LocatedFlowEdge> argumentEdges= component.edges();
		if (argumentEdges.isEmpty()) {
			return null;
		}
		for (LocatedFlowEdge edge : argumentEdges) {
			if (edge.kind() != EdgeKind.ARGUMENT_TO_PARAMETER
					|| !edge.sourceNodeId().equals(caller.stableId())
					|| !parameterIds.contains(edge.targetNodeId())
					|| !edge.compilationUnitHandle().equals(
							caller.compilationUnitHandle())) {
				return null;
			}
		}
		return new Topology(caller, parameters, argumentEdges);
	}

	private static Signature signature(
			ContainerSignatureMigrationPlan plan,
			List<FlowNode> parameters) {
		if (plan.status() != PlanningStatus.CLOSED_SOURCE_AUTOMATIC
				|| plan.groups().size() != 1) {
			return null;
		}
		SignatureAtomicityGroup group= plan.groups().get(0);
		if (group.positionKind() != PositionKind.PARAMETER
				|| group.members().size() != parameters.size()) {
			return null;
		}
		Map<String, FlowNode> parametersById= parameters.stream()
				.collect(Collectors.toMap(
						FlowNode::stableId,
						Function.identity(),
						(left, right) -> left,
						LinkedHashMap::new));
		for (SignatureMember member : group.members()) {
			FlowNode parameter= parametersById.get(member.flowNodeId());
			if (parameter == null
					|| parameter.signatureIndex() != group.signatureIndex()
					|| !member.compilationUnitHandle().equals(
							parameter.compilationUnitHandle())
					|| !member.javaElementHandle().equals(
							parameter.javaElementHandle())) {
				return null;
			}
		}
		return new Signature(group);
	}

	private static ContainerUsageProfile profile(
			Collection<ContainerUsageProfile> profiles,
			String bindingKey) {
		List<ContainerUsageProfile> matches= profiles.stream()
				.filter(profile -> profile.identity().bindingKey().equals(bindingKey))
				.toList();
		return matches.size() == 1 ? matches.get(0) : null;
	}

	private static ContainerFlowComponent localComponent(FlowNode local) {
		return new ContainerFlowComponent(
				local.stableId(),
				List.of(local),
				List.of(),
				ClosureStatus.LOCAL_CLOSED,
				List.of());
	}

	private static PlanningDiagnostic diagnostic(
			DiagnosticKind kind,
			String message) {
		return new PlanningDiagnostic(kind, message);
	}

	private record Topology(
			FlowNode caller,
			List<FlowNode> parameters,
			List<LocatedFlowEdge> argumentEdges) {

		private Topology {
			parameters= List.copyOf(parameters);
			argumentEdges= List.copyOf(argumentEdges);
		}
	}

	private record Signature(SignatureAtomicityGroup group) {
	}
}