/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is made available under the terms of the
 * Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.common.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.jboss.tools.common.core.CommonCorePlugin;

public class EclipseJavaUtil {

	public static String getMemberTypeAsString(IMember member) {
		if(member instanceof IField) return getMemberTypeAsString((IField)member);
		if(member instanceof IMethod) return getMemberTypeAsString((IMethod)member);
		return null;
	}

	public static String getMemberTypeAsString(IField f) {
		if(f == null) return null;
		try	{
			String typeName = new String(Signature.toCharArray(f.getTypeSignature().toCharArray()));
			return resolveType(f.getDeclaringType(), typeName);
		} catch (JavaModelException e) {
			CommonCorePlugin.getPluginLog().logError(e);
		}
		return null;
	}

	public static String getMemberTypeAsString(IMethod m) {
		if(m == null) return null;
		try	{
			return resolveTypeAsString(m.getDeclaringType(), m.getReturnType());
		} catch (JavaModelException e) {
			CommonCorePlugin.getPluginLog().logError(e);
		}
		return null;
	}

	public static String resolveTypeAsString(IType type, String typeName) {
		if(type == null || typeName == null) return null;
		typeName = new String(Signature.toCharArray(typeName.toCharArray()));
		int i = typeName.indexOf(Signature.C_GENERIC_START);
		if(i > 0) typeName = typeName.substring(0, i);
		return resolveType(type, typeName);
	}

	public static String resolveType(IType type, String typeName) {
		return TypeResolutionCache.getInstance().resolveType(type, typeName);
	}

	static Map<String, Map<String, IType>> typeCache = new Hashtable<String, Map<String,IType>>();

	/**
	 * Returns IType found in a Java project by its qualified name.
	 * The cache is used that is should be cleared explicitly by 
	 * TypeResolutionCache.getInstance().clear();
	 * Currently, it is done by KBBuilder so that this method works fine for all clients that 
	 * request only projects with KB nature. Otherwise, non-existent object may be 
	 * returned from the cache.
	 * 
	 * Now, it is not clear if the search over package roots, 
	 * fulfilled when IJavaProject.findType(String) fails, makes sense.
	 * There are neither tests nor use-cases that would support the need of it.
	 * 
	 * @param javaProject
	 * @param qualifiedName
	 * @return
	 * @throws JavaModelException
	 */
	public static IType findType(IJavaProject javaProject, String qualifiedName) throws JavaModelException {
		if(qualifiedName == null || qualifiedName.length() == 0 || "void".equals(qualifiedName)) return null;
		Map<String, IType> cache = typeCache.get(javaProject.getElementName());
		if(cache == null) {
			cache = new Hashtable<String, IType>();
			typeCache.put(javaProject.getElementName(), cache);
		} else {
			IType type = cache.get(qualifiedName);
			if(type != null) {
				if(type.exists()) {
					return type;
				} else {
					cache.remove(qualifiedName);
				}
			}
		}
		IType type = javaProject.findType(qualifiedName);
		if(type != null && type.exists()) {
			return register(cache, qualifiedName, type);
		}

		//TODO Either provide use-case that justifies the 
		//     direct search over roots when IJavaProject.findType(String) fails
		//     or remove this obsolete code.
//		int dot = qualifiedName.lastIndexOf('.');
//		String packageName = (dot < 0) ? "" : qualifiedName.substring(0, dot); //$NON-NLS-1$
//		String shortName = qualifiedName.substring(dot + 1);
//		IPackageFragmentRoot[] rs = javaProject.getPackageFragmentRoots();
//		for (int i = 0; i < rs.length; i++) {
//			IPackageFragment f = rs[i].getPackageFragment(packageName);
//			if(f == null || !f.exists()) continue;
//			ICompilationUnit[] us = f.getCompilationUnits();
//			for (int j = 0; j < us.length; j++) {
//				IType t = us[j].getType(shortName);
//				if(t != null && t.exists()) return register(cache, qualifiedName, t);
//			}
//		}
		return null;
	}

	private static IType register(Map<String, IType> cache, String qualifiedName, IType type) {
		cache.put(qualifiedName, type);
		return type;
	}
	
	public static List<IType> getSupperTypes(IType type) throws JavaModelException {
		ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
		IType[] superTypes = typeHierarchy == null ? null : typeHierarchy.getAllSupertypes(type);
		if(superTypes == null) {
			return Collections.emptyList();
		}
		List<IType> suppers = new ArrayList<IType>();
		for (int i = 0; i < superTypes.length; i++) {
			suppers.add(superTypes[i]);
		}
		return suppers;
	}
	
	public static IAnnotation findAnnotation(IType sourceType, IAnnotatable member, String qulifiedAnnotationName) throws JavaModelException{
		IAnnotation[] annotations = member.getAnnotations();
		String simpleAnnotationTypeName = qulifiedAnnotationName;
		int lastDot = qulifiedAnnotationName.lastIndexOf('.');
		if(lastDot>-1) {
			simpleAnnotationTypeName = simpleAnnotationTypeName.substring(lastDot + 1);
		}
		for (IAnnotation annotation : annotations) {
			if(qulifiedAnnotationName.equals(annotation.getElementName())) {
				return annotation;
			}
			if(simpleAnnotationTypeName.equals(annotation.getElementName())) {
				String fullAnnotationclassName = EclipseJavaUtil.resolveType(sourceType, simpleAnnotationTypeName);
				if(fullAnnotationclassName!=null) {
					IType annotationType = sourceType.getJavaProject().findType(fullAnnotationclassName);
					if(annotationType!=null && annotationType.getFullyQualifiedName().equals(qulifiedAnnotationName)) {
						return annotation;
					}
				}
			}
		}
		return null;
	}

	/**
	 * Finds field declared in the given type or its super types.
	 * 
	 * @param type
	 * @param name
	 * @return
	 * @throws CoreException
	 */
	public static IField findField(IType type, String name) throws CoreException {
		return findField(type, name, new HashSet<IType>());
	}
	private static IField findField(IType type, String name, Set<IType> processed) throws CoreException {
		if(!type.exists() || processed.contains(type)) {
			return null;
		}
		processed.add(type);
		if(type.getField(name).exists()) {
			return type.getField(name);
		}
		IField f = findField(type, type.getSuperclassName(), name, processed);
		String[] is = type.getSuperInterfaceNames();
		for (int i = 0; f == null && i < is.length; i++) {
			f = findField(type, is[i], name, processed);
		}
		if(f == null) {
			IType d = type.getDeclaringType();
			if(d != null && d != type && d.exists()) {
				f = findField(d, name);
			}
		}
		
		return f;
	}
	private static IField findField(IType context, String typeName, String fieldName, Set<IType> processed) throws CoreException {
		typeName = resolveType(context, typeName);
		if(typeName != null) {
			IType s = findType(context.getJavaProject(), typeName);
			return (s != null) ? findField(s, fieldName, processed) : null;
		}
		return null;
	}

	/**
	 * Returns true if the given annotation has the given full name
	 * @param annotation
	 * @param fullName
	 * @return
	 * @throws JavaModelException
	 */
	public static boolean checkAnnotationByFulltName(IAnnotation annotation, String fullName) throws JavaModelException {
		if(annotation.getElementName().equals(fullName)) {
			return true;
		}
		boolean result = true;
		IType sourceType = null;
		IJavaElement parent = annotation.getParent();
		if(parent instanceof IMember) {
			if(parent instanceof IType) {
				sourceType = (IType)parent;
			} else {
				sourceType = ((IMember)parent).getDeclaringType();
			}
			String fullAnnotationName = EclipseJavaUtil.resolveType(sourceType, annotation.getElementName());
			if(fullAnnotationName!=null) {
				IType annotationType = sourceType.getJavaProject().findType(fullAnnotationName);
				result = annotationType!=null && annotationType.getFullyQualifiedName().equals(fullName);
			} else {
				result = false;
			}
		}
		return result;
	}

	/**
	 * Returns annotation by the short name declared for the given java member and its parents.
	 * Returns null if no annotation found.
	 * @param member
	 * @param name
	 * @param checkParents
	 * @return
	 * @throws JavaModelException
	 */
	public static Set<IAnnotation> findAnnotationsByShortName(IJavaElement element, String name, boolean checkParents) throws JavaModelException {
		return findAnnotationsByShortName(null, element, name, checkParents);
	}

	private static Set<IAnnotation> findAnnotationsByShortName(Set<IAnnotation> result, IJavaElement element, String name, boolean checkParents) throws JavaModelException {
		if(element instanceof IAnnotatable) {
			IAnnotation[] annotations = ((IAnnotatable)element).getAnnotations();
			for (IAnnotation annotation : annotations) {
				String aName = annotation.getElementName();
				int i = aName.lastIndexOf('.');
				if(i>-1) {
					aName = aName.substring(i+1);
				}
				if(aName.equals(name)) {
					if(result==null) {
						result = new HashSet<IAnnotation>();
					}
					result.add(annotation);
					break;
				}
			}
		}
		if(checkParents) {
			IJavaElement parent = element.getParent();
			if(parent instanceof IAnnotatable) {
				return findAnnotationsByShortName(result, parent, name, true);
			}
		}
		return result;
	}
}