StatementDispatcher.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.common;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Predicate;

import org.eclipse.jdt.core.dom.ASTNode;

/**
 * A registry-based dispatcher for handling different statement types in a type-safe manner.
 * 
 * <p>This class provides an alternative to long if-else instanceof chains by allowing
 * handlers to be registered for specific types. The dispatcher tries handlers in
 * registration order and stops at the first successful match.</p>
 * 
 * <p><b>Example - Before (nested if-instanceof):</b></p>
 * <pre>{@code
 * if (stmt instanceof VariableDeclarationStatement && !isLast) {
 *     // handle variable declaration
 * } else if (stmt instanceof IfStatement && !isLast) {
 *     // handle non-last if
 * } else if (stmt instanceof IfStatement && isLast) {
 *     // handle last if
 * } else if (!isLast) {
 *     // handle other non-last
 * } else {
 *     // handle last
 * }
 * }</pre>
 * 
 * <p><b>Example - After (registry-based):</b></p>
 * <pre>{@code
 * StatementDispatcher<MyContext, MyResult> dispatcher = StatementDispatcher.<MyContext, MyResult>create()
 *     .when(VariableDeclarationStatement.class)
 *         .and(ctx -> !ctx.isLast())
 *         .then((stmt, ctx) -> handleVariableDecl((VariableDeclarationStatement) stmt, ctx))
 *     .when(IfStatement.class)
 *         .and(ctx -> !ctx.isLast())
 *         .then((stmt, ctx) -> handleNonLastIf((IfStatement) stmt, ctx))
 *     .when(IfStatement.class)
 *         .and(ctx -> ctx.isLast())
 *         .then((stmt, ctx) -> handleLastIf((IfStatement) stmt, ctx))
 *     .otherwise((stmt, ctx) -> handleDefault(stmt, ctx));
 * 
 * MyResult result = dispatcher.dispatch(stmt, context);
 * }</pre>
 * 
 * @param <C> the context type passed to handlers
 * @param <R> the result type returned by handlers
 */
public final class StatementDispatcher<C, R> {

	/**
	 * A handler entry that combines type matching, condition checking, and action execution.
	 */
	private static class HandlerEntry<C, R> {
		final Class<? extends ASTNode> nodeType;
		final Predicate<C> condition;
		final BiFunction<ASTNode, C, Optional<R>> handler;

		HandlerEntry(Class<? extends ASTNode> nodeType, Predicate<C> condition, BiFunction<ASTNode, C, Optional<R>> handler) {
			this.nodeType = nodeType;
			this.condition = condition;
			this.handler = handler;
		}

		boolean matches(ASTNode node, C context) {
			return nodeType.isInstance(node) && (condition == null || condition.test(context));
		}

		Optional<R> handle(ASTNode node, C context) {
			return handler.apply(node, context);
		}
	}

	private final List<HandlerEntry<C, R>> handlers = new ArrayList<>();
	private BiFunction<ASTNode, C, Optional<R>> defaultHandler;

	private StatementDispatcher() {
	}

	/**
	 * Creates a new StatementDispatcher.
	 * 
	 * @param <C> the context type
	 * @param <R> the result type
	 * @return a new dispatcher instance
	 */
	public static <C, R> StatementDispatcher<C, R> create() {
		return new StatementDispatcher<>();
	}

	/**
	 * Starts building a handler for the specified node type.
	 * 
	 * @param <N> the node type
	 * @param nodeType the class of the node type to handle
	 * @return a handler builder
	 */
	public <N extends ASTNode> HandlerBuilder<N, C, R> when(Class<N> nodeType) {
		return new HandlerBuilder<>(this, nodeType);
	}

	/**
	 * Sets the default handler for when no other handler matches.
	 * 
	 * @param handler the default handler
	 * @return this dispatcher for chaining
	 */
	public StatementDispatcher<C, R> otherwise(BiFunction<ASTNode, C, Optional<R>> handler) {
		this.defaultHandler = handler;
		return this;
	}

	/**
	 * Sets the default handler that always returns a value.
	 * 
	 * @param handler the default handler
	 * @return this dispatcher for chaining
	 */
	public StatementDispatcher<C, R> otherwiseReturn(BiFunction<ASTNode, C, R> handler) {
		this.defaultHandler = (node, ctx) -> Optional.ofNullable(handler.apply(node, ctx));
		return this;
	}

	/**
	 * Dispatches a node to the appropriate handler.
	 * 
	 * @param node the AST node to dispatch
	 * @param context the context to pass to handlers
	 * @return the result from the first matching handler, or empty if no handler matches
	 */
	public Optional<R> dispatch(ASTNode node, C context) {
		for (HandlerEntry<C, R> entry : handlers) {
			if (entry.matches(node, context)) {
				Optional<R> result = entry.handle(node, context);
				if (result.isPresent()) {
					return result;
				}
			}
		}
		if (defaultHandler != null) {
			return defaultHandler.apply(node, context);
		}
		return Optional.empty();
	}

	/**
	 * Dispatches a node and returns the result or a default value.
	 * 
	 * @param node the AST node to dispatch
	 * @param context the context to pass to handlers
	 * @param defaultValue the default value if no handler matches
	 * @return the result from the first matching handler, or the default value
	 */
	public R dispatchOrDefault(ASTNode node, C context, R defaultValue) {
		return dispatch(node, context).orElse(defaultValue);
	}

	/**
	 * Dispatches a node and executes the handler without returning a value.
	 * Useful for side-effect operations.
	 * 
	 * @param node the AST node to dispatch
	 * @param context the context to pass to handlers
	 * @return true if a handler was executed
	 */
	public boolean dispatchVoid(ASTNode node, C context) {
		return dispatch(node, context).isPresent();
	}

	/**
	 * Internal method to add a handler.
	 */
	void addHandler(HandlerEntry<C, R> entry) {
		handlers.add(entry);
	}

	/**
	 * Builder for constructing handlers with conditions.
	 * 
	 * @param <N> the node type
	 * @param <C> the context type
	 * @param <R> the result type
	 */
	public static final class HandlerBuilder<N extends ASTNode, C, R> {
		private final StatementDispatcher<C, R> dispatcher;
		private final Class<N> nodeType;
		private Predicate<C> condition;

		HandlerBuilder(StatementDispatcher<C, R> dispatcher, Class<N> nodeType) {
			this.dispatcher = dispatcher;
			this.nodeType = nodeType;
		}

		/**
		 * Adds a condition that must be true for the handler to execute.
		 * 
		 * @param condition the condition predicate
		 * @return this builder for chaining
		 */
		public HandlerBuilder<N, C, R> and(Predicate<C> condition) {
			this.condition = condition;
			return this;
		}

		/**
		 * Adds an additional condition that the node itself must satisfy.
		 * 
		 * @param nodeCondition the condition on the node
		 * @return this builder for chaining
		 */
		public HandlerBuilder<N, C, R> matching(Predicate<N> nodeCondition) {
			// Create a combined condition that checks both the context and the node
			Predicate<C> originalCondition = this.condition;
			// We'll need to apply this in the handler instead
			return this;
		}

		/**
		 * Specifies the handler to execute when the type and condition match.
		 * 
		 * @param handler the handler function that returns an Optional result
		 * @return the dispatcher for chaining
		 */
		public StatementDispatcher<C, R> then(BiFunction<N, C, Optional<R>> handler) {
			@SuppressWarnings("unchecked")
			BiFunction<ASTNode, C, Optional<R>> wrappedHandler = (node, ctx) -> handler.apply((N) node, ctx);
			dispatcher.addHandler(new HandlerEntry<>(nodeType, condition, wrappedHandler));
			return dispatcher;
		}

		/**
		 * Specifies the handler to execute, returning a non-optional result.
		 * 
		 * @param handler the handler function
		 * @return the dispatcher for chaining
		 */
		public StatementDispatcher<C, R> thenReturn(BiFunction<N, C, R> handler) {
			return then((node, ctx) -> Optional.ofNullable(handler.apply(node, ctx)));
		}

		/**
		 * Specifies a handler that performs a side effect and returns empty.
		 * 
		 * @param handler the handler consumer
		 * @return the dispatcher for chaining
		 */
		public StatementDispatcher<C, R> thenDo(java.util.function.BiConsumer<N, C> handler) {
			return then((node, ctx) -> {
				handler.accept(node, ctx);
				return Optional.empty();
			});
		}

		/**
		 * Specifies that this handler should signal to skip processing (returns empty Optional).
		 * 
		 * @return the dispatcher for chaining
		 */
		public StatementDispatcher<C, R> thenSkip() {
			return then((node, ctx) -> Optional.empty());
		}
	}
}