LoopModel.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
 *
 * Contributors:
 *     Carsten Hammer
 *******************************************************************************/
package org.sandbox.functional.core.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.sandbox.functional.core.operation.Operation;
import org.sandbox.functional.core.terminal.TerminalOperation;

/**
 * Unified Loop Representation (ULR) model.
 * 
 * <p>This class provides an AST-independent representation of loop structures
 * that can be transformed into functional/stream-based equivalents.</p>
 * 
 * <p>The model consists of three main components:</p>
 * <ul>
 * <li>{@link SourceDescriptor} - describes what is being iterated over</li>
 * <li>{@link ElementDescriptor} - describes the loop variable</li>
 * <li>{@link LoopMetadata} - describes loop characteristics and constraints</li>
 * </ul>
 * 
 * @see <a href="https://github.com/carstenartur/sandbox/issues/450">Issue #450</a>
 * @since 1.0.0
 */
public class LoopModel {
	
	private SourceDescriptor source;
	private ElementDescriptor element;
	private LoopMetadata metadata;
	private final List<Operation> operations = new ArrayList<>();
	private TerminalOperation terminal;
	
	/**
	 * Default constructor.
	 */
	public LoopModel() {
	}
	
	/**
	 * Creates a new LoopModel.
	 * 
	 * @param source the source descriptor
	 * @param element the element descriptor
	 * @param metadata the loop metadata
	 */
	public LoopModel(SourceDescriptor source, ElementDescriptor element, LoopMetadata metadata) {
		this.source = source;
		this.element = element;
		this.metadata = metadata;
	}
	
	/**
	 * Gets the source descriptor.
	 * 
	 * @return the source descriptor
	 */
	public SourceDescriptor getSource() {
		return source;
	}
	
	/**
	 * Gets the element descriptor.
	 * 
	 * @return the element descriptor
	 */
	public ElementDescriptor getElement() {
		return element;
	}
	
	/**
	 * Gets the loop metadata.
	 * 
	 * @return the loop metadata
	 */
	public LoopMetadata getMetadata() {
		return metadata;
	}
	
	/**
	 * Gets an unmodifiable view of the operations list.
	 * 
	 * @return an unmodifiable list of operations
	 */
	public List<Operation> getOperations() { 
		return Collections.unmodifiableList(operations); 
	}
	
	/**
	 * Gets the terminal operation.
	 * 
	 * @return the terminal operation
	 */
	public TerminalOperation getTerminal() { 
		return terminal; 
	}
	
	/**
	 * Sets the terminal operation.
	 * 
	 * @param terminal the terminal operation
	 */
	public void setTerminal(TerminalOperation terminal) {
		this.terminal = terminal;
	}
	
	// Package-private setters for internal model construction and testing.
	// Note: LoopModelBuilder in org.sandbox.functional.core.builder uses the public constructor instead.
	LoopModel setSource(SourceDescriptor source) {
		this.source = source;
		return this;
	}
	
	LoopModel setElement(ElementDescriptor element) {
		this.element = element;
		return this;
	}
	
	LoopModel setMetadata(LoopMetadata metadata) {
		this.metadata = metadata;
		return this;
	}
	
	/**
	 * Adds an operation to the pipeline.
	 * 
	 * @param op the operation to add, must not be {@code null}
	 * @return this LoopModel for fluent API
	 * @throws NullPointerException if op is {@code null}
	 */
	public LoopModel addOperation(Operation op) {
		java.util.Objects.requireNonNull(op, "operation must not be null");
		this.operations.add(op);
		return this;
	}
	
	/**
	 * Sets the terminal operation (fluent API).
	 * 
	 * @param terminal the terminal operation, must not be {@code null}
	 * @return this LoopModel for fluent API
	 * @throws NullPointerException if terminal is {@code null}
	 */
	public LoopModel withTerminal(TerminalOperation terminal) {
		java.util.Objects.requireNonNull(terminal, "terminal operation must not be null");
		this.terminal = terminal;
		return this;
	}
	
	/**
	 * Checks if this model can be converted to a stream.
	 */
	public boolean isConvertible() {
		if (metadata == null) return true;
		return metadata.isConvertible();
	}
	
	/**
	 * Returns a string representation of this loop model.
	 * 
	 * @return string representation
	 */
	@Override
	public String toString() {
		return "LoopModel[source=" + source + ", element=" + element 
				+ ", metadata=" + metadata + ", operations=" + operations
				+ ", terminal=" + terminal + "]";
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null || getClass() != obj.getClass()) {
			return false;
		}
		LoopModel other = (LoopModel) obj;
		return java.util.Objects.equals(source, other.source)
				&& java.util.Objects.equals(element, other.element)
				&& java.util.Objects.equals(metadata, other.metadata)
				&& java.util.Objects.equals(operations, other.operations)
				&& java.util.Objects.equals(terminal, other.terminal);
	}
	
	@Override
	public int hashCode() {
		return java.util.Objects.hash(source, element, metadata, operations, terminal);
	}
}