Match.java

/*******************************************************************************
 * Copyright (c) 2025 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 - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

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

/**
 * Represents a successful match of a pattern against Java code.
 * 
 * <p>A match contains:</p>
 * <ul>
 *   <li>The matched AST node</li>
 *   <li>A map of placeholder bindings (e.g., {@code "$x" -> InfixExpression})</li>
 *   <li>A map of multi-placeholder bindings (e.g., {@code "$args$" -> List<Expression>})</li>
 *   <li>Source location (offset and length)</li>
 * </ul>
 * 
 * @since 1.2.2
 */
public final class Match {
	private final ASTNode matchedNode;
	private final Map<String, Object> bindings;  // Changed to Object to support both ASTNode and List<ASTNode>
	private final int offset;
	private final int length;
	
	/**
	 * Creates a new match.
	 * 
	 * @param matchedNode the AST node that matched the pattern
	 * @param bindings map of placeholder names to their bound AST nodes or lists of AST nodes
	 * @param offset the character offset of the match in the source
	 * @param length the character length of the match in the source
	 */
	public Match(ASTNode matchedNode, Map<String, Object> bindings, int offset, int length) {
		this.matchedNode = Objects.requireNonNull(matchedNode, "Matched node cannot be null"); //$NON-NLS-1$
		this.bindings = bindings != null ? Collections.unmodifiableMap(bindings) : Collections.emptyMap();
		this.offset = offset;
		this.length = length;
	}
	
	/**
	 * Returns the matched AST node.
	 * 
	 * @return the matched node
	 */
	public ASTNode getMatchedNode() {
		return matchedNode;
	}
	
	/**
	 * Returns the placeholder bindings.
	 * 
	 * @return an unmodifiable map of placeholder names to AST nodes or lists of AST nodes
	 */
	public Map<String, Object> getBindings() {
		return bindings;
	}
	
	/**
	 * Gets a single-placeholder binding as an AST node.
	 * 
	 * @param placeholderName the placeholder name including $ marker (e.g., "$x")
	 * @return the bound AST node, or null if not found or if it's a multi-placeholder binding
	 */
	public ASTNode getBinding(String placeholderName) {
		Object binding = bindings.get(placeholderName);
		if (binding instanceof ASTNode) {
			return (ASTNode) binding;
		}
		return null;
	}
	
	/**
	 * Gets a multi-placeholder binding as a list of nodes.
	 * 
	 * @param placeholderName the placeholder name including $ markers (e.g., "$args$")
	 * @return the list of matched nodes, or empty list if not found or if it's a single-placeholder binding
	 */
	@SuppressWarnings("unchecked")
	public List<ASTNode> getListBinding(String placeholderName) {
		Object binding = bindings.get(placeholderName);
		if (binding instanceof List<?>) {
			return (List<ASTNode>) binding;
		}
		return Collections.emptyList();
	}
	
	/**
	 * Returns the character offset of the match in the source.
	 * 
	 * @return the offset
	 */
	public int getOffset() {
		return offset;
	}
	
	/**
	 * Returns the character length of the match in the source.
	 * 
	 * @return the length
	 */
	public int getLength() {
		return length;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null || getClass() != obj.getClass()) {
			return false;
		}
		Match other = (Match) obj;
		return offset == other.offset 
				&& length == other.length
				&& Objects.equals(matchedNode, other.matchedNode);
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(matchedNode, offset, length);
	}
	
	@Override
	public String toString() {
		return "Match[offset=" + offset + ", length=" + length + ", bindings=" + bindings.keySet() + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	}
}