package lapsePlus;

/*
* Binding2JavaModel.java, version 2.8, 2010
*/

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.IClassFile;
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.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.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;

/**
 * A helper class to convert compiler bindings into corresponding 
 * Java elements.
 */
public class Binding2JavaModel {
    
    private Binding2JavaModel(){}


    public static ICompilationUnit findCompilationUnit(ITypeBinding typeBinding, IJavaProject project) throws JavaModelException {
        if (!typeBinding.isFromSource()) {
            return null;
        }
        while (typeBinding != null && !typeBinding.isTopLevel()) {
            typeBinding= typeBinding.getDeclaringClass();
        }
        if (typeBinding != null) {
            IPackageBinding pack= typeBinding.getPackage();
            String packageName= pack.isUnnamed() ? "" : pack.getName(); //$NON-NLS-1$
            IType type= project.findType(packageName, typeBinding.getName());
            if (type != null) {
                return type.getCompilationUnit();
            }
        }
        return null;
    }


    /**
     * Converts the given <code>IVariableBinding</code> into a <code>IField</code>
     * using the classpath defined by the given Java project. Returns <code>null</code>
     * if the conversion isn't possible.
     */
    public static IField find(IVariableBinding field, IJavaProject in) throws JavaModelException {
        IType declaringClass = find(field.getDeclaringClass(), in);
        if (declaringClass == null)
            return null;
        IField foundField= declaringClass.getField(field.getName());
        if (! foundField.exists())
            return null;
        return foundField;
    }
    
    /**
     * Converts the given <code>ITypeBinding</code> into a <code>IType</code>
     * using the classpath defined by the given Java project. Returns <code>null</code>
     * if the conversion isn't possible.
     */
    public static IType find(ITypeBinding type, IJavaProject scope) throws JavaModelException {
        if (type.isPrimitive())
            return null;
        String[] typeElements= getNameComponents(type);
        IJavaElement element= scope.findElement(getPathToCompilationUnit(type.getPackage(), typeElements[0]));
        IType candidate= null;
        if (element instanceof ICompilationUnit) {
            candidate= ((ICompilationUnit)element).getType(typeElements[0]);
        } else if (element instanceof IClassFile) {
            candidate= ((IClassFile)element).getType();
        } else if (element == null){
            if (type.isMember())
                candidate= findType(scope, getFullyQualifiedImportName(type.getDeclaringClass()));
            else
                candidate= findType(scope, getFullyQualifiedImportName(type));
        }
        
        if (candidate == null || typeElements.length == 1)
            return candidate;
            
        return findTypeInType(typeElements, candidate);
    }

    /** 
     * Finds a type by its qualified type name (dot separated).
     * @param jproject The java project to search in
     * @param str The fully qualified name (type name with enclosing type names and package (all separated by dots))
     * @return The type found, or null if not existing
     */ 
    public static IType findType(IJavaProject jproject, String fullyQualifiedName) throws JavaModelException {
        //workaround for bug 22883
        IType type= jproject.findType(fullyQualifiedName);
        if (type != null)
            return type;
        IPackageFragmentRoot[] roots= jproject.getPackageFragmentRoots();
        for (int i= 0; i < roots.length; i++) {
            IPackageFragmentRoot root= roots[i];
            type= findType(root, fullyQualifiedName);
            if (type != null && type.exists())
                return type;
        }   
        return null;
    }
    
    private static IType findType(IPackageFragmentRoot root, String fullyQualifiedName) throws JavaModelException{
        IJavaElement[] children= root.getChildren();
        for (int i= 0; i < children.length; i++) {
            IJavaElement element= children[i];
            if (element.getElementType() == IJavaElement.PACKAGE_FRAGMENT){
                IPackageFragment pack= (IPackageFragment)element;
                if (! fullyQualifiedName.startsWith(pack.getElementName()))
                    continue;
                IType type= findType(pack, fullyQualifiedName);
                if (type != null && type.exists())
                    return type;
            }
        }       
        return null;
    }
    
    private static IType findType(IPackageFragment pack, String fullyQualifiedName) throws JavaModelException{
        ICompilationUnit[] cus= pack.getCompilationUnits();
        for (int i= 0; i < cus.length; i++) {
            ICompilationUnit unit= cus[i];
            IType type= findType(unit, fullyQualifiedName);
            if (type != null && type.exists())
                return type;
        }
        return null;
    }
    
    private static IType findType(ICompilationUnit cu, String fullyQualifiedName) throws JavaModelException{
        IType[] types= cu.getAllTypes();
        for (int i= 0; i < types.length; i++) {
            IType type= types[i];
            if (getFullyQualifiedName(type).equals(fullyQualifiedName))
                return type;
        }
        return null;
    }
    
    /**
     * Returns the fully qualified name of the given type using '.' as separators.
     * This is a replace for IType.getFullyQualifiedTypeName
     * which uses '$' as separators. As '$' is also a valid character in an id
     * this is ambiguous. JavaCore PR: 1GCFUNT
     */
    public static String getFullyQualifiedName(IType type) {
        return type.getFullyQualifiedName('.');
    }
    

    /**
     * Finds the given <code>IMethodBinding</code> in the given <code>IType</code>. Returns
     * <code>null</code> if the type doesn't contain a corresponding method.
     */
    public static IMethod find(IMethodBinding method, IType type) throws JavaModelException {
        IMethod[] candidates= type.getMethods();
        for (int i= 0; i < candidates.length; i++) {
            IMethod candidate= candidates[i];
            if (candidate.getElementName().equals(method.getName()) && sameParameters(method, candidate)) {
                return candidate;
            }
        }
        return null;
    }

    public static IMethod findIncludingSupertypes(IMethodBinding method, IType type, IProgressMonitor pm) throws JavaModelException {
        IMethod inThisType= find(method, type);
        if (inThisType != null)
            return inThisType;
        IType[] superTypes= getAllSuperTypes(type, pm);
        for (int i= 0; i < superTypes.length; i++) {
            IMethod m= find(method, superTypes[i]);
            if (m != null)
                return m;
        }
        return null;
    }

    public static IMethod find(IMethodBinding method, IJavaProject scope) throws JavaModelException {
        IType type= find(method.getDeclaringClass(), scope);
        if (type == null)
            return null;
        return find(method, type);  
    }
    
    //---- Helper methods to convert a type --------------------------------------------
    
    private static IPath getPathToCompilationUnit(IPackageBinding packageBinding, String topLevelTypeName) {
        IPath result= new Path(""); //$NON-NLS-1$
        String[] packageNames= packageBinding.getNameComponents();
        for (int i= 0; i < packageNames.length; i++) {
            result= result.append(packageNames[i]);
        }
        return result.append(topLevelTypeName + ".java"); //$NON-NLS-1$
    }
    
    private static IType findTypeInType(String[] typeElements, IType jmType) {
        IType result= jmType;
        for (int i= 1; i < typeElements.length; i++) {
            result= result.getType(typeElements[i]);
            if (!result.exists())
                return null;
        }
        return result == jmType ? null : result;
    }
    
    //---- Helper methods to convert a method ---------------------------------------------
    
    private static boolean sameParameters(IMethodBinding method, IMethod candidate) throws JavaModelException {
        ITypeBinding[] methodParamters= method.getParameterTypes();
        String[] candidateParameters= candidate.getParameterTypes();
        if (methodParamters.length != candidateParameters.length)
            return false;
        IType scope= candidate.getDeclaringType();
        for (int i= 0; i < methodParamters.length; i++) {
            ITypeBinding methodParameter= methodParamters[i];
            String candidateParameter= candidateParameters[i];
            if (!sameParameter(methodParameter, candidateParameter, scope))
                return false;
        }
        return true;
    }
    
    private static boolean sameParameter(ITypeBinding type, String candidate, IType scope) throws JavaModelException {
        if (type.getDimensions() != Signature.getArrayCount(candidate))
            return false;
            
        // Normalizes types
        if (type.isArray())
            type= type.getElementType();
        candidate= Signature.getElementType(candidate);
        
        if (isPrimitiveType(candidate) || type.isPrimitive()) {
            return type.getName().equals(Signature.toString(candidate));
        } else {
            if (isResolvedType(candidate)) {
                return Signature.toString(candidate).equals(getFullyQualifiedName(type));
            } else {
                String[][] qualifiedCandidates= scope.resolveType(Signature.toString(candidate));
                if (qualifiedCandidates == null || qualifiedCandidates.length == 0)
                    return false;
                String packageName= type.getPackage().isUnnamed() ? "" : type.getPackage().getName(); //$NON-NLS-1$
                String typeName= getTypeQualifiedName(type);
                for (int i= 0; i < qualifiedCandidates.length; i++) {
                    String[] qualifiedCandidate= qualifiedCandidates[i];
                    if (qualifiedCandidate[0].equals(packageName) &&
                            qualifiedCandidate[1].equals(typeName)) {
                        return true;
                            }
                }
            }
        }
        return false;
    }
    
    public static String getTypeQualifiedName(ITypeBinding type) {
        StringBuffer buffer= new StringBuffer();
        createName(type, false, buffer);
        return buffer.toString();
    }
    
    private static boolean isPrimitiveType(String s) {
        char c= s.charAt(0);
        return c != Signature.C_RESOLVED && c != Signature.C_UNRESOLVED;
    }
    
    private static boolean isResolvedType(String s) {
        int arrayCount= Signature.getArrayCount(s);
        return s.charAt(arrayCount) == Signature.C_RESOLVED;
    }
    
    public static String[] getNameComponents(ITypeBinding type) {
        List<String> result= new ArrayList<String>(5);
        createName(type, false, result);
        return (String[]) result.toArray(new String[result.size()]);
    }
    
    private static void createName(ITypeBinding type, boolean includePackage, StringBuffer buffer) {
        ITypeBinding baseType= type;
        if (type.isArray()) {
            baseType= type.getElementType();
        }
        if (!baseType.isPrimitive() && !baseType.isNullType()) {
            ITypeBinding declaringType= baseType.getDeclaringClass();
            if (declaringType != null) {
                createName(declaringType, includePackage, buffer);
                buffer.append('.');
            } else if (includePackage && !baseType.getPackage().isUnnamed()) {
                buffer.append(baseType.getPackage().getName());
                buffer.append('.');
            }
        }
        if (!baseType.isAnonymous()) {
            buffer.append(type.getName());
        } else {
            buffer.append("$local$"); //$NON-NLS-1$
        }
    }
    
    private static void createName(ITypeBinding type, boolean includePackage, List<String> list) {
        ITypeBinding baseType= type;
        if (type.isArray()) {
            baseType= type.getElementType();
        }
        if (!baseType.isPrimitive() && !baseType.isNullType()) {
            ITypeBinding declaringType= baseType.getDeclaringClass();
            if (declaringType != null) {
                createName(declaringType, includePackage, list);
            } else if (includePackage && !baseType.getPackage().isUnnamed()) {
                String[] components= baseType.getPackage().getNameComponents();
                for (int i= 0; i < components.length; i++) {
                    list.add(components[i]);
                }
            }
        }
        if (!baseType.isAnonymous()) {
            list.add(type.getName());
        } else {
            list.add("$local$"); //$NON-NLS-1$
        }       
    }   
    
    
    public static String getFullyQualifiedImportName(ITypeBinding type) {
        if (type.isArray()) {
            return getFullyQualifiedName(type.getElementType());
        } else if (type.isAnonymous()) {
            return getFullyQualifiedImportName(type.getSuperclass());
        } else {
            return getFullyQualifiedName(type);
        }
    }
    
    public static String getFullyQualifiedName(ITypeBinding type) {
        StringBuffer buffer= new StringBuffer();
        createName(type, true, buffer);
        return buffer.toString();       
    }   

    public static IType[] getAllSuperTypes(IType type, IProgressMonitor pm) throws JavaModelException {
        //workaround for bugs 23644 and 23656
        try{
            pm.beginTask("", 3); //$NON-NLS-1$
            ITypeHierarchy hierarchy= type.newSupertypeHierarchy(new SubProgressMonitor(pm, 1));
            
            IProgressMonitor subPm= new SubProgressMonitor(pm, 2);
            List<IType> typeList= Arrays.asList(hierarchy.getAllSupertypes(type));
            subPm.beginTask("", typeList.size()); //$NON-NLS-1$
            Set<IType> types= new HashSet<IType>(typeList);
            for (Iterator iter= typeList.iterator(); iter.hasNext();) {
                IType superType= (IType)iter.next();
                IType[] superTypes= getAllSuperTypes(superType, new SubProgressMonitor(subPm, 1));
                types.addAll(Arrays.asList(superTypes));
            }
            types.add(type.getJavaProject().findType("java.lang.Object"));//$NON-NLS-1$
            subPm.done();
            return (IType[]) types.toArray(new IType[types.size()]);
        } finally {
            pm.done();
        }   
    }
    

}