ContainerAnalysisView.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
 *******************************************************************************/
package org.sandbox.jdt.ui.helper.views;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.Text;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.texteditor.ITextEditor;

import org.sandbox.jdt.ui.helper.views.ContainerAnalysisService.ContainerAnalysisRow;

/**
 * Read-only projection of the shared semantic container analysis. Each table row
 * is source-backed evidence, so navigation leads to the observation that supports
 * or blocks the recommendation rather than to a synthetic UI-only diagnostic.
 */
public final class ContainerAnalysisView extends ViewPart implements IShowInTarget {

	public static final String VIEW_ID= "org.sandbox.jdt.ui.helper.views.ContainerAnalysisView"; //$NON-NLS-1$

	private final ContainerAnalysisService analysisService= new ContainerAnalysisService();
	private final AtomicLong analysisGeneration= new AtomicLong();
	private TableViewer viewer;
	private Text details;
	private Display display;
	private List<ICompilationUnit> currentUnits= List.of();
	private ISelectionListener workbenchSelectionListener;
	private IPartListener2 editorPartListener;
	private volatile Job analysisJob;

	@Override
	public void createPartControl(Composite parent) {
		display= parent.getDisplay();
		parent.setLayout(new GridLayout(1, false));
		SashForm sash= new SashForm(parent, SWT.VERTICAL);
		sash.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

		viewer= new TableViewer(sash, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL);
		viewer.setContentProvider(ArrayContentProvider.getInstance());
		Table table= viewer.getTable();
		table.setHeaderVisible(true);
		table.setLinesVisible(true);
		addColumn("Candidate", 130, ContainerAnalysisRow::candidate); //$NON-NLS-1$
		addColumn("Current", 90, ContainerAnalysisRow::currentShape); //$NON-NLS-1$
		addColumn("Target contract", 310, ContainerAnalysisRow::targetContract); //$NON-NLS-1$
		addColumn("Confidence", 90, ContainerAnalysisRow::confidence); //$NON-NLS-1$
		addColumn("Status", 110, ContainerAnalysisRow::status); //$NON-NLS-1$
		addColumn("Evidence", 420, row -> row.evidenceKind() + ": " + row.evidenceSummary()); //$NON-NLS-1$ //$NON-NLS-2$

		details= new Text(sash, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL);
		details.setText("Select an evidence row to inspect its semantic explanation."); //$NON-NLS-1$
		sash.setWeights(72, 28);

		viewer.addSelectionChangedListener(event -> updateDetails());
		viewer.addDoubleClickListener(this::navigate);
		getSite().setSelectionProvider(viewer);
		contributeActions();
		installSelectionListener();
		installEditorPartListener();
		updateFromActiveEditor();
	}

	private void addColumn(String title, int width, Function<ContainerAnalysisRow, String> text) {
		TableViewerColumn viewerColumn= new TableViewerColumn(viewer, SWT.NONE);
		TableColumn column= viewerColumn.getColumn();
		column.setText(title);
		column.setWidth(width);
		column.setResizable(true);
		viewerColumn.setLabelProvider(new ColumnLabelProvider() {
			@Override
			public String getText(Object element) {
				return element instanceof ContainerAnalysisRow row ? text.apply(row) : ""; //$NON-NLS-1$
			}
		});
	}

	private void contributeActions() {
		Action refresh= new Action("Refresh container analysis") { //$NON-NLS-1$
			@Override
			public void run() {
				refresh();
			}
		};
		refresh.setToolTipText("Re-run read-only container contract analysis for the current Java selection"); //$NON-NLS-1$
		IToolBarManager toolbar= getViewSite().getActionBars().getToolBarManager();
		toolbar.add(refresh);
	}

	private void installSelectionListener() {
		workbenchSelectionListener= (IWorkbenchPart part, org.eclipse.jface.viewers.ISelection selection) -> {
			if (part == this) {
				return;
			}
			List<ICompilationUnit> units= compilationUnits(selection);
			if (!units.isEmpty()) {
				setInput(units);
			}
		};
		getSite().getWorkbenchWindow().getSelectionService().addSelectionListener(workbenchSelectionListener);
	}

	private void installEditorPartListener() {
		editorPartListener= new IPartListener2() {
			@Override
			public void partActivated(IWorkbenchPartReference partRef) {
				updateFromEditorPart(partRef);
			}

			@Override
			public void partBroughtToTop(IWorkbenchPartReference partRef) {
				updateFromEditorPart(partRef);
			}

			@Override
			public void partOpened(IWorkbenchPartReference partRef) {
				updateFromEditorPart(partRef);
			}

			@Override
			public void partInputChanged(IWorkbenchPartReference partRef) {
				updateFromEditorPart(partRef);
			}

			@Override
			public void partClosed(IWorkbenchPartReference partRef) {
				// Nothing to do; another activated editor will provide the next input.
			}

			@Override
			public void partDeactivated(IWorkbenchPartReference partRef) {
				// Activation of the next editor is the useful lifecycle event.
			}

			@Override
			public void partHidden(IWorkbenchPartReference partRef) {
				// View input remains useful while an editor is hidden.
			}

			@Override
			public void partVisible(IWorkbenchPartReference partRef) {
				// Visibility alone does not change the active source input.
			}
		};
		getSite().getPage().addPartListener(editorPartListener);
	}

	private void updateFromEditorPart(IWorkbenchPartReference partRef) {
		if (partRef.getPart(false) instanceof IEditorPart) {
			updateFromActiveEditor();
		}
	}

	private void updateFromActiveEditor() {
		IEditorPart editor= getSite().getPage().getActiveEditor();
		if (editor == null) {
			return;
		}
		ICompilationUnit unit= compilationUnit(editor.getEditorInput());
		if (unit != null) {
			setInput(List.of(unit));
		}
	}

	private void setInput(List<ICompilationUnit> units) {
		Map<String, ICompilationUnit> unique= new LinkedHashMap<>();
		for (ICompilationUnit unit : units) {
			if (unit != null && unit.exists()) {
				ICompilationUnit primary= unit.getPrimary();
				unique.put(primary.getHandleIdentifier(), primary);
			}
		}
		List<ICompilationUnit> normalized= unique.values().stream()
				.sorted(Comparator.comparing(ICompilationUnit::getHandleIdentifier))
				.toList();
		if (sameInput(currentUnits, normalized)) {
			return;
		}
		currentUnits= normalized;
		refresh();
	}

	private static boolean sameInput(List<ICompilationUnit> left, List<ICompilationUnit> right) {
		if (left.size() != right.size()) {
			return false;
		}
		for (int index= 0; index < left.size(); index++) {
			if (!left.get(index).getHandleIdentifier().equals(right.get(index).getHandleIdentifier())) {
				return false;
			}
		}
		return true;
	}

	private void refresh() {
		if (viewer == null || viewer.getControl().isDisposed()) {
			return;
		}
		long generation= analysisGeneration.incrementAndGet();
		Job previous= analysisJob;
		if (previous != null) {
			previous.cancel();
		}
		List<ICompilationUnit> units= currentUnits;
		if (units.isEmpty()) {
			analysisJob= null;
			applyAnalysisResult(generation, List.of(), null);
			return;
		}

		details.setText("Analyzing container usage..."); //$NON-NLS-1$
		Job job= new Job("Analyze semantic container contracts") { //$NON-NLS-1$
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				List<ContainerAnalysisRow> rows= new ArrayList<>();
				RuntimeException failure= null;
				try {
					for (ICompilationUnit unit : units) {
						if (monitor.isCanceled() || generation != analysisGeneration.get()) {
							return Status.CANCEL_STATUS;
						}
						rows.addAll(analysisService.analyze(unit));
					}
				} catch (RuntimeException e) {
					failure= e;
					logAnalysisFailure(e);
				}
				if (monitor.isCanceled() || generation != analysisGeneration.get()) {
					return Status.CANCEL_STATUS;
				}
				List<ContainerAnalysisRow> result= List.copyOf(rows);
				RuntimeException resultFailure= failure;
				Display uiDisplay= display;
				if (uiDisplay != null && !uiDisplay.isDisposed()) {
					uiDisplay.asyncExec(() -> applyAnalysisResult(generation, result, resultFailure));
				}
				return Status.OK_STATUS;
			}
		};
		job.setSystem(true);
		analysisJob= job;
		job.schedule();
	}

	private void applyAnalysisResult(long generation, List<ContainerAnalysisRow> rows,
			RuntimeException failure) {
		if (generation != analysisGeneration.get()
				|| viewer == null || viewer.getControl().isDisposed()
				|| details == null || details.isDisposed()) {
			return;
		}
		if (failure != null) {
			viewer.setInput(List.of());
			details.setText("Container analysis failed: " + failureSummary(failure)); //$NON-NLS-1$
			return;
		}
		viewer.setInput(rows);
		if (rows.isEmpty()) {
			details.setText(currentUnits.isEmpty()
					? "Select a Java source element to analyze container usage." //$NON-NLS-1$
					: "No supported or rejected semantic container candidates were found in the selected source."); //$NON-NLS-1$
		} else {
			viewer.setSelection(new org.eclipse.jface.viewers.StructuredSelection(rows.get(0)), true);
			updateDetails();
		}
	}

	private static String failureSummary(RuntimeException failure) {
		String message= failure.getMessage();
		String type= failure.getClass().getSimpleName();
		return message == null || message.isBlank() ? type : type + ": " + message; //$NON-NLS-1$
	}

	private static void logAnalysisFailure(RuntimeException failure) {
		UsageViewPlugin plugin= UsageViewPlugin.getDefault();
		if (plugin != null) {
			plugin.getLog().log(new Status(IStatus.ERROR,
					UsageViewPlugin.PLUGIN_ID, "Container analysis failed", failure)); //$NON-NLS-1$
		}
	}

	private void updateDetails() {
		if (details == null || details.isDisposed()) {
			return;
		}
		Object selected= viewer.getStructuredSelection().getFirstElement();
		details.setText(selected instanceof ContainerAnalysisRow row
				? row.details()
				: "Select an evidence row to inspect its semantic explanation."); //$NON-NLS-1$
	}

	private void navigate(DoubleClickEvent event) {
		Object selected= ((IStructuredSelection) event.getSelection()).getFirstElement();
		if (!(selected instanceof ContainerAnalysisRow row)) {
			return;
		}
		IJavaElement element= JavaCore.create(row.compilationUnitHandle());
		if (!(element instanceof ICompilationUnit unit) || !unit.exists()) {
			return;
		}
		try {
			IEditorPart editor= JavaUI.openInEditor(unit, true, true);
			if (editor instanceof ITextEditor textEditor) {
				textEditor.selectAndReveal(row.sourceStart(), row.sourceLength());
			}
		} catch (org.eclipse.ui.PartInitException | org.eclipse.jdt.core.JavaModelException e) {
			UsageViewPlugin plugin= UsageViewPlugin.getDefault();
			if (plugin != null) {
				plugin.getLog().log(new Status(IStatus.ERROR,
						UsageViewPlugin.PLUGIN_ID, "Could not navigate to container evidence", e)); //$NON-NLS-1$
			}
		}
	}

	@Override
	public boolean show(ShowInContext context) {
		List<ICompilationUnit> units= compilationUnits(context == null ? null : context.getSelection());
		if (units.isEmpty()) {
			return false;
		}
		setInput(units);
		return true;
	}

	private static List<ICompilationUnit> compilationUnits(org.eclipse.jface.viewers.ISelection selection) {
		if (!(selection instanceof IStructuredSelection structured)) {
			return List.of();
		}
		List<ICompilationUnit> result= new ArrayList<>();
		for (Object element : structured.toList()) {
			ICompilationUnit unit= compilationUnit(element);
			if (unit != null) {
				result.add(unit);
			}
		}
		return result;
	}

	private static ICompilationUnit compilationUnit(Object element) {
		if (element instanceof ICompilationUnit unit) {
			return unit;
		}
		if (element instanceof IJavaElement javaElement) {
			IJavaElement ancestor= javaElement.getAncestor(IJavaElement.COMPILATION_UNIT);
			return ancestor instanceof ICompilationUnit unit ? unit : null;
		}
		if (element instanceof IFile file) {
			IJavaElement javaElement= JavaCore.create(file);
			return javaElement instanceof ICompilationUnit unit ? unit : null;
		}
		return null;
	}

	private static ICompilationUnit compilationUnit(IEditorInput input) {
		if (input instanceof IFileEditorInput fileInput) {
			return compilationUnit(fileInput.getFile());
		}
		IJavaElement javaElement= input == null ? null : input.getAdapter(IJavaElement.class);
		return compilationUnit(javaElement);
	}

	@Override
	public void setFocus() {
		if (viewer != null && !viewer.getControl().isDisposed()) {
			viewer.getControl().setFocus();
		}
	}

	@Override
	public void dispose() {
		analysisGeneration.incrementAndGet();
		Job job= analysisJob;
		if (job != null) {
			job.cancel();
		}
		if (workbenchSelectionListener != null && getSite() != null) {
			getSite().getWorkbenchWindow().getSelectionService().removeSelectionListener(workbenchSelectionListener);
		}
		if (editorPartListener != null && getSite() != null) {
			getSite().getPage().removePartListener(editorPartListener);
		}
		display= null;
		super.dispose();
	}
}