/**
 * Optimus, framework for Model Transformation
 *
 * Copyright (C) 2013 Worldline or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */
package net.atos.optimus.m2m.javaxmi.operation.accesses;

import java.util.Iterator;

import net.atos.optimus.m2m.javaxmi.operation.annotations.builders.AnnotationTypeDeclarationBuilder;
import net.atos.optimus.m2m.javaxmi.operation.annotations.builders.UnresolvedAnnotationDeclarationBuilder;
import net.atos.optimus.m2m.javaxmi.operation.classes.UnresolvedClassDeclarationBuilder;
import net.atos.optimus.m2m.javaxmi.operation.elements.Element;
import net.atos.optimus.m2m.javaxmi.operation.interfaces.UnresolvedInterfaceDeclarationBuilder;
import net.atos.optimus.m2m.javaxmi.operation.packages.JavaPackage;
import net.atos.optimus.m2m.javaxmi.operation.packages.PackageHelper;
import net.atos.optimus.m2m.javaxmi.operation.types.ArrayTypeBuilder;
import net.atos.optimus.m2m.javaxmi.operation.types.PrimitiveTypeBuilder;
import net.atos.optimus.m2m.javaxmi.operation.types.UnresolvedTypeBuilder;
import net.atos.optimus.m2m.javaxmi.operation.types.WildCardTypeBuilder;
import net.atos.optimus.m2m.javaxmi.operation.util.ASTElementFinder;

import org.eclipse.emf.common.util.EList;
import org.eclipse.gmt.modisco.java.AbstractTypeDeclaration;
import org.eclipse.gmt.modisco.java.AnnotationTypeDeclaration;
import org.eclipse.gmt.modisco.java.Model;
import org.eclipse.gmt.modisco.java.ParameterizedType;
import org.eclipse.gmt.modisco.java.Type;
import org.eclipse.gmt.modisco.java.TypeAccess;
import org.eclipse.gmt.modisco.java.UnresolvedAnnotationDeclaration;

/**
 * The purpose of such class is to help with the creation of type accesses
 * 
 * @author tnachtergaele <nachtergaele.thomas@gmail.com>
 * 
 *
 */

public class TypeAccessHelper {

	/** String constant : entry character of an array */
	public static final String ARRAY_ENTRY = "[";

	/** String constant : exit character of an array */
	public static final String ARRAY_EXIT = "]";

	/** String constant : entry character of a parameterized type */
	public static final String PARAMETRIZED_ENTRY = "<";

	/** String constant : separator of a parameterized type */
	public static final String PARAMETRIZED_SEPARATOR = ",";

	/** String constant : the wild card character */
	public static final String WILD_CARD = "?";

	/** The Java keyword for class extension */
	public static final String EXTENSION = "extends";

	/** The Java keyword for generalization */
	public static final String GENERALIZATION = "super";

	/** String constant : separator of package chunks */
	public static final String PACKAGE_SEPARATOR = ".";

	/**
	 * Private constructor
	 * 
	 */
	private TypeAccessHelper() {
	}

	/**
	 * Create a type access associated to a class
	 * 
	 * @param className
	 *            the class name which we want a type access.
	 * @return the created type access associated to the specified class name.
	 */
	public static TypeAccess createClassTypeAccess(String className) {
		if (className.contains(TypeAccessHelper.PARAMETRIZED_ENTRY)) {
			return TypeAccessHelper.createParameterizedType(
					TypeAccessHelper.createClassTypeAccess(className.substring(0,
							className.indexOf(TypeAccessHelper.PARAMETRIZED_ENTRY))), className);
		}
		return TypeAccessBuilder.builder()
				.setType(UnresolvedClassDeclarationBuilder.builder().setName(className).build()).build();
	}

	/**
	 * Create a type access associated to an interface
	 * 
	 * @param interfaceName
	 *            the interface name which we want a type access.
	 * @return the created type access associated to the specified interface
	 *         name.
	 */
	public static TypeAccess createInterfaceTypeAccess(String interfaceName) {
		if (interfaceName.contains(TypeAccessHelper.PARAMETRIZED_ENTRY)) {
			return TypeAccessHelper.createParameterizedType(
					TypeAccessHelper.createInterfaceTypeAccess(interfaceName.substring(0,
							interfaceName.indexOf(TypeAccessHelper.PARAMETRIZED_ENTRY))), interfaceName);
		}
		return TypeAccessBuilder.builder()
				.setType(UnresolvedInterfaceDeclarationBuilder.builder().setName(interfaceName).build()).build();
	}

	/**
	 * Create a type access associated to an exception
	 * 
	 * @param exceptionName
	 *            the exception name which we want a type access.
	 * @return the created type access associated to the specified exception
	 *         name.
	 */
	public static TypeAccess createExceptionTypeAccess(String exceptionName) {
		if (exceptionName.contains(TypeAccessHelper.PARAMETRIZED_ENTRY)) {
			return TypeAccessHelper.createParameterizedType(
					TypeAccessHelper.createExceptionTypeAccess(exceptionName.substring(0,
							exceptionName.indexOf(TypeAccessHelper.PARAMETRIZED_ENTRY))), exceptionName);
		}
		return TypeAccessBuilder.builder().setType(PrimitiveTypeBuilder.builder().setName(exceptionName).build())
				.build();
	}

	/**
	 * Create a type access associated to an annotation
	 * 
	 * @param element
	 *            the element which we associate the annotation.
	 * @param packageName
	 *            the name of the package of the element.
	 * @param annotationName
	 *            the name of the annotation.
	 * @return the created type access associated to the specified annotation
	 *         name.
	 */
	public static TypeAccess createAnnotationTypeAccess(Element<?> element, String packageName, String annotationName) {
		Model model = ASTElementFinder.findModel(element.getDelegate());
		JavaPackage javaPackage = PackageHelper.createPackage(model, packageName);
		AnnotationTypeDeclaration generatedAnnotation = null;
		Iterator<AbstractTypeDeclaration> declarationIterator = javaPackage.getDelegate().getOwnedElements().iterator();

		while (declarationIterator.hasNext() && generatedAnnotation == null) {
			AbstractTypeDeclaration next = declarationIterator.next();
			if (next instanceof AnnotationTypeDeclaration && annotationName.equals(next.getName())) {
				generatedAnnotation = (AnnotationTypeDeclaration) next;
			}
		}

		if (generatedAnnotation == null) {
			return TypeAccessBuilder
					.builder()
					.setType(
							AnnotationTypeDeclarationBuilder.builder().setName(annotationName)
									.setPackage(javaPackage.getDelegate()).setProxy(true).build()).build();
		}

		return TypeAccessBuilder.builder().setType(generatedAnnotation).build();
	}

	/**
	 * Create a type access associated to an orphan annotation
	 * 
	 * @param element
	 *            the element which we associate the orphan annotation.
	 * @param fullyQualifiedName
	 *            the full qualified name of the orphan annotation.
	 * @return the created type access associated to the specified orphan
	 *         annotation name.
	 */
	public static TypeAccess createOrphanAnnotationTypeAccess(Element<?> element, String fullyQualifiedName) {
		Model model = element == null ? null : ASTElementFinder.findModel(element.getDelegate());
		UnresolvedAnnotationDeclaration annotation = null;
		if (model != null) {
			Iterator<Type> iterator = model.getOrphanTypes().iterator();
			while (iterator.hasNext() && annotation == null) {
				Type orphanType = iterator.next();
				if (orphanType instanceof UnresolvedAnnotationDeclaration
						&& fullyQualifiedName.equals(orphanType.getName())) {
					annotation = (UnresolvedAnnotationDeclaration) orphanType;
				}
			}
		}

		if (annotation == null) {
			annotation = UnresolvedAnnotationDeclarationBuilder.builder().setName(fullyQualifiedName).build();
			if (model != null) {
				model.getOrphanTypes().add(annotation);
			}
		}

		return TypeAccessBuilder.builder().setType(annotation).build();
	}

	/**
	 * Create a type associated to a class or an interface
	 * 
	 * @param typeName
	 *            the type name which we want a type access.
	 * @return the created type access associated to the specified type name.
	 */
	public static TypeAccess createTypeAccess(String typeName) {
		typeName = typeName.trim();
		if (typeName.contains(TypeAccessHelper.ARRAY_ENTRY)) {
			return TypeAccessHelper.createArrayType(
					TypeAccessHelper.createTypeAccess(typeName.substring(0,
							typeName.indexOf(TypeAccessHelper.ARRAY_ENTRY))), typeName);
		}
		if (typeName.contains(TypeAccessHelper.PARAMETRIZED_ENTRY)) {
			return TypeAccessHelper.createParameterizedType(
					TypeAccessHelper.createTypeAccess(typeName.substring(0,
							typeName.indexOf(TypeAccessHelper.PARAMETRIZED_ENTRY))), typeName);
		}
		if (Character.isLowerCase(typeName.charAt(0)) && !typeName.contains(TypeAccessHelper.PACKAGE_SEPARATOR)) {
			return TypeAccessBuilder.builder().setType(PrimitiveTypeBuilder.builder().setName(typeName).build())
					.build();
		}
		return TypeAccessBuilder.builder().setType(UnresolvedTypeBuilder.builder().setName(typeName).build()).build();
	}

	/**
	 * Create an array type
	 * 
	 * @param mainType
	 *            the main type of the array type.
	 * @param name
	 *            the name of the array type.
	 * @return the created type access associated to the specified array type.
	 */
	protected static TypeAccess createArrayType(TypeAccess mainType, String name) {
		int dimensions = 0;
		int index = 0;
		while ((index = name.indexOf(TypeAccessHelper.ARRAY_EXIT, index + 1)) != -1) {
			dimensions++;
		}
		return TypeAccessBuilder.builder()
				.setType(ArrayTypeBuilder.builder().setDimensions(dimensions).setElementType(mainType).build()).build();
	}

	/**
	 * Create a parameterized type
	 * 
	 * @param mainType
	 *            the main type of the parameterized type.
	 * @param name
	 *            the name of the parameterized type.
	 * @return the created type access associated to the specified parameterized
	 *         type.
	 */
	protected static TypeAccess createParameterizedType(TypeAccess mainType, String name) {
		String[] arguments = name.substring(name.indexOf(TypeAccessHelper.PARAMETRIZED_ENTRY) + 1, name.length() - 1)
				.split(TypeAccessHelper.PARAMETRIZED_SEPARATOR);
		ParameterizedType parameterizedType = ParameterizedTypeBuilder.builder().setType(mainType).setName(name)
				.build();
		EList<TypeAccess> argumentsList = parameterizedType.getTypeArguments();
		for (String argument : arguments) {
			argumentsList.add(TypeAccessHelper.createTypeParametrizedArgument(argument));
		}
		return TypeAccessBuilder.builder().setType(parameterizedType).build();
	}

	/**
	 * Create type for an argument of a parameterized type
	 * 
	 * @param argument
	 *            the argument of the parameterized type in one string.
	 * @return the created type for the specified argument of a parameterized
	 *         type.
	 */
	protected static TypeAccess createTypeParametrizedArgument(String argument) {
		String[] argumentParts = argument.trim().split("\\s+");
		if (argumentParts.length >= 3) {
			if (TypeAccessHelper.WILD_CARD.equals(argumentParts[0])) {
				return TypeAccessBuilder
						.builder()
						.setType(
								WildCardTypeBuilder.builder()
										.setUpperBound(TypeAccessHelper.EXTENSION.equals(argumentParts[1]))
										.setBound(TypeAccessHelper.createClassTypeAccess(argumentParts[2])).build())
						.build();
			}
			return TypeAccessHelper.createClassTypeAccess(argumentParts[0]);
		}
		if (TypeAccessHelper.WILD_CARD.equals(argumentParts[0])) {
			TypeAccessBuilder.builder().setType(WildCardTypeBuilder.builder().build()).build();
		}
		return TypeAccessHelper.createClassTypeAccess(argumentParts[0]);
	}
}
