TypeInfo.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.ast.api.info;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * Immutable record representing a Java type.
 * Provides fluent query methods for type checking and comparisons.
 */
public record TypeInfo(
	String qualifiedName,
	String simpleName,
	List<TypeInfo> typeArguments,
	boolean isPrimitive,
	boolean isArray,
	int arrayDimensions
) {
	
	private static final Set<String> COLLECTION_TYPES = Set.of(
		"java.util.Collection",
		"java.util.List",
		"java.util.Set",
		"java.util.Queue",
		"java.util.Deque",
		"java.util.ArrayList",
		"java.util.LinkedList",
		"java.util.HashSet",
		"java.util.TreeSet"
	);
	
	private static final Set<String> LIST_TYPES = Set.of(
		"java.util.List",
		"java.util.ArrayList",
		"java.util.LinkedList"
	);
	
	private static final Set<String> STREAM_TYPES = Set.of(
		"java.util.stream.Stream",
		"java.util.stream.IntStream",
		"java.util.stream.LongStream",
		"java.util.stream.DoubleStream"
	);
	
	private static final Set<String> OPTIONAL_TYPES = Set.of(
		"java.util.Optional",
		"java.util.OptionalInt",
		"java.util.OptionalLong",
		"java.util.OptionalDouble"
	);
	
	/**
	 * Creates a TypeInfo record.
	 * 
	 * @param qualifiedName fully qualified name (e.g., "java.util.List")
	 * @param simpleName simple name (e.g., "List")
	 * @param typeArguments type arguments for generics
	 * @param isPrimitive true if primitive type
	 * @param isArray true if array type
	 * @param arrayDimensions number of array dimensions
	 */
	public TypeInfo {
		if (qualifiedName == null) {
			throw new IllegalArgumentException("Qualified name cannot be null");
		}
		if (qualifiedName.isEmpty()) {
			throw new IllegalArgumentException("Qualified name cannot be empty");
		}
		if (simpleName == null) {
			throw new IllegalArgumentException("Simple name cannot be null");
		}
		if (simpleName.isEmpty()) {
			throw new IllegalArgumentException("Simple name cannot be empty");
		}
		typeArguments = typeArguments == null ? List.of() : List.copyOf(typeArguments);
		if (arrayDimensions < 0) {
			throw new IllegalArgumentException("Array dimensions cannot be negative");
		}
	}
	
	/**
	 * Checks if this type matches the given class.
	 * 
	 * @param clazz class to compare
	 * @return true if qualified names match
	 */
	public boolean is(Class<?> clazz) {
		return qualifiedName.equals(clazz.getName());
	}
	
	/**
	 * Checks if this type matches the given qualified name.
	 * 
	 * @param qualifiedName qualified name to compare
	 * @return true if names match
	 */
	public boolean is(String qualifiedName) {
		return this.qualifiedName.equals(qualifiedName);
	}
	
	/**
	 * Checks if this type is a collection type (List, Set, Collection, etc.).
	 * 
	 * @return true if collection type
	 */
	public boolean isCollection() {
		return COLLECTION_TYPES.contains(qualifiedName);
	}
	
	/**
	 * Checks if this type is a List.
	 * 
	 * @return true if List type
	 */
	public boolean isList() {
		return LIST_TYPES.contains(qualifiedName);
	}
	
	/**
	 * Checks if this type is a Stream.
	 * 
	 * @return true if Stream type
	 */
	public boolean isStream() {
		return STREAM_TYPES.contains(qualifiedName);
	}
	
	/**
	 * Checks if this type is Optional.
	 * 
	 * @return true if Optional type
	 */
	public boolean isOptional() {
		return OPTIONAL_TYPES.contains(qualifiedName);
	}
	
	/**
	 * Checks if this type is numeric (int, Integer, double, Double, etc.).
	 * 
	 * @return true if numeric type
	 */
	public boolean isNumeric() {
		return isPrimitive && (
			qualifiedName.equals("int") ||
			qualifiedName.equals("long") ||
			qualifiedName.equals("double") ||
			qualifiedName.equals("float") ||
			qualifiedName.equals("short") ||
			qualifiedName.equals("byte")
		) || (
			qualifiedName.equals("java.lang.Integer") ||
			qualifiedName.equals("java.lang.Long") ||
			qualifiedName.equals("java.lang.Double") ||
			qualifiedName.equals("java.lang.Float") ||
			qualifiedName.equals("java.lang.Short") ||
			qualifiedName.equals("java.lang.Byte") ||
			qualifiedName.equals("java.math.BigInteger") ||
			qualifiedName.equals("java.math.BigDecimal")
		);
	}
	
	/**
	 * Returns the boxed version of a primitive type.
	 * 
	 * @return Optional containing boxed type, or empty if not primitive
	 */
	public Optional<TypeInfo> boxed() {
		if (!isPrimitive) {
			return Optional.empty();
		}
		return switch (qualifiedName) {
			case "int" -> Optional.of(Builder.of("java.lang.Integer").build());
			case "long" -> Optional.of(Builder.of("java.lang.Long").build());
			case "double" -> Optional.of(Builder.of("java.lang.Double").build());
			case "float" -> Optional.of(Builder.of("java.lang.Float").build());
			case "boolean" -> Optional.of(Builder.of("java.lang.Boolean").build());
			case "byte" -> Optional.of(Builder.of("java.lang.Byte").build());
			case "short" -> Optional.of(Builder.of("java.lang.Short").build());
			case "char" -> Optional.of(Builder.of("java.lang.Character").build());
			default -> Optional.empty();
		};
	}
	
	/**
	 * Checks if this type has type arguments (is generic).
	 * 
	 * @return true if has type arguments
	 */
	public boolean hasTypeArguments() {
		return !typeArguments.isEmpty();
	}
	
	/**
	 * Gets the first type argument if present.
	 * 
	 * @return Optional containing first type argument
	 */
	public Optional<TypeInfo> firstTypeArgument() {
		return typeArguments.isEmpty() ? Optional.empty() : Optional.of(typeArguments.get(0));
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj) return true;
		if (!(obj instanceof TypeInfo other)) return false;
		return Objects.equals(qualifiedName, other.qualifiedName) &&
			   isPrimitive == other.isPrimitive &&
			   isArray == other.isArray &&
			   arrayDimensions == other.arrayDimensions &&
			   Objects.equals(typeArguments, other.typeArguments);
	}
	
	@Override
	public int hashCode() {
		return Objects.hash(qualifiedName, isPrimitive, isArray, arrayDimensions, typeArguments);
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder(qualifiedName);
		if (!typeArguments.isEmpty()) {
			sb.append("<");
			for (int i = 0; i < typeArguments.size(); i++) {
				if (i > 0) sb.append(", ");
				sb.append(typeArguments.get(i).simpleName);
			}
			sb.append(">");
		}
		if (isArray) {
			sb.append("[]".repeat(arrayDimensions));
		}
		return sb.toString();
	}
	
	/**
	 * Builder for creating TypeInfo instances.
	 */
	public static class Builder {
		private String qualifiedName;
		private String simpleName;
		private List<TypeInfo> typeArguments = new java.util.ArrayList<>();
		private boolean isPrimitive;
		private boolean isArray;
		private int arrayDimensions;
		
		private Builder(String qualifiedName) {
			this.qualifiedName = qualifiedName;
			// Extract simple name from qualified name
			int lastDot = qualifiedName.lastIndexOf('.');
			this.simpleName = lastDot >= 0 ? qualifiedName.substring(lastDot + 1) : qualifiedName;
		}
		
		/**
		 * Creates a builder for the given qualified name.
		 * 
		 * @param qualifiedName fully qualified type name
		 * @return new Builder
		 */
		public static Builder of(String qualifiedName) {
			return new Builder(qualifiedName);
		}
		
		/**
		 * Creates a builder for a class.
		 * 
		 * @param clazz the class
		 * @return new Builder
		 */
		public static Builder of(Class<?> clazz) {
			return new Builder(clazz.getName());
		}
		
		/**
		 * Sets the simple name explicitly.
		 * 
		 * @param simpleName simple name
		 * @return this builder
		 */
		public Builder simpleName(String simpleName) {
			this.simpleName = simpleName;
			return this;
		}
		
		/**
		 * Sets type arguments for generics.
		 * 
		 * @param typeArguments type arguments
		 * @return this builder
		 */
		public Builder typeArguments(List<TypeInfo> typeArguments) {
			this.typeArguments = typeArguments;
			return this;
		}
		
		/**
		 * Adds a single type argument.
		 * 
		 * @param typeArgument type argument to add
		 * @return this builder
		 */
		public Builder addTypeArgument(TypeInfo typeArgument) {
			typeArguments.add(typeArgument);
			return this;
		}
		
		/**
		 * Marks this type as primitive.
		 * 
		 * @return this builder
		 */
		public Builder primitive() {
			this.isPrimitive = true;
			return this;
		}
		
		/**
		 * Marks this type as an array.
		 * 
		 * @param dimensions number of array dimensions
		 * @return this builder
		 */
		public Builder array(int dimensions) {
			this.isArray = true;
			this.arrayDimensions = dimensions;
			return this;
		}
		
		/**
		 * Marks this type as a 1-dimensional array.
		 * 
		 * @return this builder
		 */
		public Builder array() {
			return array(1);
		}
		
		/**
		 * Builds the TypeInfo instance.
		 * 
		 * @return new TypeInfo
		 */
		public TypeInfo build() {
			return new TypeInfo(qualifiedName, simpleName, typeArguments, 
							   isPrimitive, isArray, arrayDimensions);
		}
	}
}