/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * $Header:$
 */
package org.apache.beehive.netui.compiler;

import org.apache.beehive.netui.compiler.typesystem.declaration.*;
import org.apache.beehive.netui.compiler.typesystem.env.CoreAnnotationProcessorEnv;
import org.apache.beehive.netui.compiler.typesystem.env.Filer;
import org.apache.beehive.netui.compiler.typesystem.env.Messager;
import org.apache.beehive.netui.compiler.typesystem.type.ArrayType;
import org.apache.beehive.netui.compiler.typesystem.type.ClassType;
import org.apache.beehive.netui.compiler.typesystem.type.DeclaredType;
import org.apache.beehive.netui.compiler.typesystem.type.InterfaceType;
import org.apache.beehive.netui.compiler.typesystem.type.ReferenceType;
import org.apache.beehive.netui.compiler.typesystem.type.TypeInstance;
import org.apache.beehive.netui.compiler.typesystem.type.TypeVariable;
import org.apache.beehive.netui.compiler.typesystem.util.SourcePosition;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;


public class CompilerUtils
        implements JpfLanguageConstants
{
    private static final String ERROR_STRING = "<error>";
    private static final TypeDeclaration ERROR_TYPE_DECLARATION = new ErrorTypeDeclaration();
    
    public static boolean isJpfAnnotation( AnnotationInstance annotation, String unqualifiedName )
    {
        String annotationName = getDeclaration( annotation.getAnnotationType() ).getQualifiedName();
        return annotationName.equals( ANNOTATION_QUALIFIER + unqualifiedName );
    }
    
    public static AnnotationInstance getAnnotation( ClassDeclaration decl, String unqualifiedName, boolean inherited )
    {
        if ( ! inherited ) return getAnnotation( decl, unqualifiedName );
        
        do
        {
            AnnotationInstance ann = getAnnotation( decl, unqualifiedName );
            if ( ann != null ) return ann;
            ClassType superType = decl.getSuperclass();
            TypeDeclaration superTypeDecl = getDeclaration( superType );
            decl = superTypeDecl instanceof ClassDeclaration ? ( ClassDeclaration ) superTypeDecl : null;
        } while ( decl != null );
        
        return null;
    }
    
    public static AnnotationInstance getAnnotation( Declaration element, String unqualifiedName )
    {
        return getAnnotationFullyQualified( element, ANNOTATION_QUALIFIER + unqualifiedName );
    }
    
    public static AnnotationInstance getAnnotationFullyQualified( Declaration element, String fullyQualifiedName )
    {
        AnnotationInstance[] annotations = element.getAnnotationInstances();
        
        for ( int ii = 0; ii < annotations.length; ii++ )
        {
            AnnotationInstance i = annotations[ii];
            String iName = getDeclaration( i.getAnnotationType() ).getQualifiedName();
            if ( fullyQualifiedName.equals( iName ) ) return i;
        }
        
        return null;
    }
    
    public static AnnotationValue getAnnotationValue( Declaration element, String annotationName, String valueName )
    {
        AnnotationInstance ann = getAnnotation( element, annotationName );
        return ann != null ? getAnnotationValue( ann, valueName, true ) : null;
    }
    
    /**
     * If the given annotation exists, assert that the given member is not null</code>, and return it; otherwise,
     * if the given annotation does not exist, return null</code>.
     */ 
    private static AnnotationValue assertAnnotationValue( Declaration element, String annotationName, String valueName,
                                                          boolean defaultIsNull )
    {
        AnnotationInstance ann = getAnnotation( element, annotationName );
        
        if ( ann == null )
        {
            return null;
        }
        else
        {
            return getAnnotationValue( ann, valueName, defaultIsNull );
        }
    }
    
    public static String getStringValue( Declaration element, String annotationName, String memberName,
                                         boolean defaultIsNull )
    {
        return ( String ) getValue( element, annotationName, memberName, defaultIsNull );
    }
    
    public static AnnotationValue getAnnotationValue( AnnotationInstance annotation, String memberName,
                                                      boolean defaultIsNull )
    {
        Map valuesPresent = annotation.getElementValues();
        
        for ( Iterator ii = valuesPresent.entrySet().iterator(); ii.hasNext(); )  
        {
            Map.Entry i = ( Map.Entry ) ii.next();
            if ( memberName.equals( ( ( AnnotationTypeElementDeclaration ) i.getKey() ).getSimpleName() ) )
            {
                return ( AnnotationValue ) i.getValue();
            }
        }
        
        //
        // We didn't find it.  If necessary, look for the default value.
        //
        if ( defaultIsNull ) return null;
        
        AnnotationTypeDeclaration typeDecl = annotation.getAnnotationType().getAnnotationTypeDeclaration();
        if ( typeDecl == null ) return null;    // type declaration is null in the case of error type
        
        AnnotationTypeElementDeclaration[] members = typeDecl.getAnnotationMembers();
        for ( int i = 0; i < members.length; i++ )
        {
            AnnotationTypeElementDeclaration member = members[i];
            if ( memberName.equals( member.getSimpleName() ) ) return member.getDefaultValue();
            
        }
        
        return null;
    }
    
    public static List getStringArrayValue( Declaration element, String annotationName, String memberName,
                                                      boolean defaultIsNull )
    {
        AnnotationValue value = assertAnnotationValue( element, annotationName, memberName, defaultIsNull );
        if ( value == null ) return null;
        ArrayList ret = new ArrayList();
        getValues( value, ret, false );
        return ret;
    }
    
    public static Boolean getBooleanValue( Declaration element, String annotationName, String memberName,
                                           boolean defaultIsNull )
    {
        AnnotationValue value = assertAnnotationValue( element, annotationName, memberName, defaultIsNull );
        return value != null ? ( Boolean ) value.getValue() : null;
    }
    
    public static String getString( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        return value != null ? ( String ) value.getValue() : ( defaultIsNull ? null : "" );
    }
    
    public static TypeInstance getTypeInstance( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return null;
        Object typeInstance = value.getValue();
        if ( isErrorString( typeInstance ) ) return new ErrorTypeInstance();
        assert typeInstance instanceof TypeInstance 
                : "expected TypeInstance, got " + typeInstance + "(value=" + typeInstance + ")";
        return ( TypeInstance ) typeInstance;
    }
    
    public static String getEnumFieldName( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        return value != null ? getEnumFieldName( value ) : null;
    }
    
    public static List getStringArray( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return null;
        ArrayList ret = new ArrayList();
        getValues( value, ret, false );
        return ret;
    }
    
    public static DeclaredType getDeclaredType( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        return ( DeclaredType ) getReferenceType( annotation, memberName, defaultIsNull );
    }
    
    public static ReferenceType getReferenceType( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        
        // If the type is the string "<error>", it means that there is already an error related to the type itself.
        if ( value != null && isErrorString( value.getValue() ) ) return null;
        
        return value != null ? ( ReferenceType ) value.getValue() : null;
    }
    
    public static Integer getInteger( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return defaultIsNull ? null : new Integer( 0 );
        Object result = value.getValue();
        
        // If the value isn't of type Integer, silently return 0 -- it's a compile error in user code that will
        // be caught by javac.
        return result instanceof Integer ? (Integer) result : new Integer(0);
    }
    
    public static boolean isErrorString( Object str )
    {
        return ERROR_STRING.equals( str );
    }
    
    public static Long getLong( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return defaultIsNull ? null : new Long( 0 );
        Object result = value.getValue();
        
        // If the value isn't of type Long, silently return 0 -- it's a compile error in user code that will
        // be caught by javac.
        return result instanceof Long ? (Long) result : new Long(0);
    }
    
    public static Float getFloat( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return defaultIsNull ? null : new Float( 0 );
        Object result = value.getValue();
        
        // If the value isn't of type Float, silently return 0 -- it's a compile error in user code that will
        // be caught by javac.
        return result instanceof Float ? (Float) result : new Float(0);
    }
    
    public static Double getDouble( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return defaultIsNull ? null : new Double( 0 );
        Object result = value.getValue();
        
        // If the value isn't of type Double, silently return 0 -- it's a compile error in user code that will
        // be caught by javac.
        return result instanceof Double ? (Double) result : new Double(0);
    }
    
    public static Boolean getBoolean( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        if ( value == null ) return defaultIsNull ? null : Boolean.FALSE;
        Object result = value.getValue();
        
        // If the value isn't of type Boolean, silently return false -- it's a compile error in user code that will
        // be caught by javac.
        return result instanceof Boolean ? (Boolean) result : Boolean.FALSE;
    }
    
    public static Object getValue( Declaration element, String annotationName, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = assertAnnotationValue( element, annotationName, memberName, defaultIsNull );
        return value != null ? value.getValue() : null;
    }
    
    public static List getAnnotationArrayValue( Declaration element, String annotationName,
                                                                    String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = assertAnnotationValue( element, annotationName, memberName, defaultIsNull );
        if ( value == null ) return null;
        ArrayList ret = new ArrayList();
        getValues( value, ret, true );
        return ret;
    }
    
    public static List getAnnotationArray( AnnotationInstance annotation, String memberName,
                                                               boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        return getAnnotationArray( value );
    }
    
    public static List getAnnotationArray( AnnotationValue value )
    {
        if ( value == null ) return null;
        ArrayList ret = new ArrayList();
        getValues( value, ret, true );
        return ret;
    }
    
    private static void getValues( AnnotationValue arrayValue, List translatedValues, boolean weedOutErrorType )
    {
        List values  = ( List ) arrayValue.getValue();
        for ( Iterator ii = values.iterator(); ii.hasNext(); )  
        {
            Object i = ii.next();
            Object value = i instanceof AnnotationValue ? ( ( AnnotationValue ) i ).getValue() : i;
            if ( ! weedOutErrorType || ! isErrorString( value ) ) translatedValues.add( value );
        }
    }
    
    public static String getQualifiedName( AnnotationInstance annotation )
    {
        return getDeclaration( annotation.getAnnotationType() ).getQualifiedName();
    }
    
    public static String getSimpleName( AnnotationInstance annotation )
    {
        return getDeclaration( annotation.getAnnotationType() ).getSimpleName();
    }
    
    public static AnnotationInstance getAnnotation( AnnotationInstance annotation, String memberName, boolean defaultIsNull )
    {
        AnnotationValue value = getAnnotationValue( annotation, memberName, defaultIsNull );
        return value != null ? ( AnnotationInstance ) value.getValue() : null;
    }
    
    public static MethodDeclaration getClassMethod( TypeDeclaration jclass, String methodName, String desiredAnnotation )
    {
        return getClassMethod( jclass, methodName, desiredAnnotation, false );
    }
    
    private static MethodDeclaration getClassMethod( TypeDeclaration type, String methodName, String desiredAnnotation,
                                                     boolean onlyPublicOrProtected )
    {
        if ( ! ( type instanceof ClassDeclaration ) ) return null;
        
        ClassDeclaration jclass = ( ClassDeclaration ) type;
        MethodDeclaration[] methods = jclass.getMethods();
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            if ( ! onlyPublicOrProtected || method.hasModifier( Modifier.PROTECTED )
                 || method.hasModifier( Modifier.PUBLIC ) )
            {
                if ( methodName.equals( method.getSimpleName() )
                     && ( desiredAnnotation == null || getAnnotation( method, desiredAnnotation ) != null ) )
                {
                    return method;
                }
            }
        }
        
        ClassType superclass = jclass.getSuperclass();
        
        if ( superclass != null )
        {
            return getClassMethod( getDeclaration( superclass ), methodName, desiredAnnotation, true );
        }
        
        return null;
    }
    
    public static FieldDeclaration getClassField( TypeDeclaration jclass, String fieldName, String desiredAnnotation )
    {
        return getClassField( jclass, fieldName, desiredAnnotation, false );
    }
    
    private static FieldDeclaration getClassField( TypeDeclaration type, String fieldName, String desiredAnnotation,
                                                   boolean onlyPublicOrProtected )
    {
        if ( ! ( type instanceof ClassDeclaration ) ) return null;
        
        ClassDeclaration jclass = ( ClassDeclaration ) type;
        FieldDeclaration[] fields = jclass.getFields();
        
        for ( int i = 0; i < fields.length; i++ )
        {
            FieldDeclaration field = fields[i];
            if ( ! onlyPublicOrProtected || field.hasModifier( Modifier.PROTECTED )
                 || field.hasModifier( Modifier.PUBLIC ) )
            {
                if ( fieldName.equals( field.getSimpleName() )
                     && ( desiredAnnotation == null || getAnnotation( field, desiredAnnotation ) != null ) )
                {
                    return field;
                }
            }
        }
        
        ClassType superclass = jclass.getSuperclass();
        
        if ( superclass != null )
        {
            return getClassField( getDeclaration( superclass ), fieldName, desiredAnnotation, true );
        }
        
        return null;
    }
        
    public static MethodDeclaration[] getClassMethods( TypeDeclaration jclass, String desiredAnnotation )
    {
        Collection results = new ArrayList();
        getClassMethods( jclass, desiredAnnotation, false, results );
        return ( MethodDeclaration[] ) results.toArray( new MethodDeclaration[ results.size() ] );
    }
    
    private static void getClassMethods( TypeDeclaration type, String desiredAnnotation, boolean onlyPublicOrPrivate,
                                         Collection results )
    {
        if ( ! ( type instanceof ClassDeclaration ) ) return;
        
        ClassDeclaration jclass = ( ClassDeclaration ) type;
        MethodDeclaration[] methods = jclass.getMethods();
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            
            if ( ! onlyPublicOrPrivate || method.hasModifier( Modifier.PROTECTED )
                 || method.hasModifier( Modifier.PUBLIC ) )
            {
                if ( desiredAnnotation == null || getAnnotation( method, desiredAnnotation ) != null )
                {
                    boolean isDuplicate = false;
                    
                    //
                    // Make sure we're not adding a duplicate method -- one that was already overridden.
                    //
                    if ( onlyPublicOrPrivate )
                    {
                        ParameterDeclaration[] methodParams = method.getParameters();
                        
                        for ( Iterator j = results.iterator(); j.hasNext() && !isDuplicate; )  
                        {
                            MethodDeclaration existingMethod = ( MethodDeclaration ) j.next();
                            
                            if ( existingMethod.getSimpleName().equals( method.getSimpleName() ) )
                            {
                                ParameterDeclaration[] existingMethodParams = existingMethod.getParameters();
                                
                                if ( existingMethodParams.length == methodParams.length )
                                {
                                    isDuplicate = true;

                                    for ( int k = 0; k < existingMethodParams.length; ++k )
                                    {
                                        ParameterDeclaration existingMethodParam = existingMethodParams[ k ];
                                        ParameterDeclaration methodParam = methodParams[ k ];
                                        
                                        if ( ! existingMethodParam.getType().equals( methodParam.getType() ) )
                                        {
                                            isDuplicate = false;
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                    
                    if ( ! isDuplicate ) results.add( method );
                }
            }
        }
        
        ClassType superclass = jclass.getSuperclass();
        
        if ( superclass != null && ! getDeclaration( superclass ).getQualifiedName().startsWith( "java.lang." ) )
        {
            getClassMethods( getDeclaration( superclass ), desiredAnnotation, true, results );
        }
    }
    
    public static Collection getClassFields( TypeDeclaration jclass )
    {
        Collection results = new ArrayList();
        getClassFields( jclass, false, results );
        return results;
    }
    
    private static void getClassFields( TypeDeclaration type, boolean onlyPublicOrPrivate,
                                        Collection results )
    {
        if ( ! ( type instanceof ClassDeclaration ) ) return;
        
        ClassDeclaration jclass = ( ClassDeclaration ) type;
        FieldDeclaration[] fields = jclass.getFields();
        
        for ( int i = 0; i < fields.length; i++ )
        {
            FieldDeclaration field = fields[i];
            if ( ! onlyPublicOrPrivate || field.hasModifier( Modifier.PROTECTED ) || field.hasModifier( Modifier.PUBLIC ) )
            {
                results.add( field );
            }
        }
        
        ClassType superclass = jclass.getSuperclass();
        if ( superclass != null ) getClassFields( getDeclaration( superclass ), true, results );
    }
    
    public static Collection getClassNestedTypes( TypeDeclaration jclass )
    {
        Collection results = new ArrayList();
        getClassNestedTypes( jclass, false, results );
        return results;
    }
    
    private static void getClassNestedTypes( TypeDeclaration type, boolean onlyPublicOrPrivate,
                                             Collection results )
    {
        if ( ! ( type instanceof ClassDeclaration ) ) return;
        
        ClassDeclaration jclass = ( ClassDeclaration ) type;
        TypeDeclaration[] nestedTypes = jclass.getNestedTypes();
        
        for ( int i = 0; i < nestedTypes.length; i++ )
        {
            TypeDeclaration nestedType = nestedTypes[i];
            if ( ! onlyPublicOrPrivate || nestedType.hasModifier( Modifier.PROTECTED )
                 || nestedType.hasModifier( Modifier.PUBLIC ) )
            {
                results.add( nestedType );
            }
        }
        
        ClassType superclass = jclass.getSuperclass();
        if ( superclass != null ) getClassNestedTypes( getDeclaration( superclass ), true, results );
    }
        
    /**
     * Get a Class.forName-able string for the given type signature.  
     */
    public static String getFormClassName( TypeDeclaration jclass, CoreAnnotationProcessorEnv env )
    {
        if ( isAssignableFrom( STRUTS_FORM_CLASS_NAME, jclass, env ) )
        {
            return getLoadableName( jclass );
        }
        else if ( isAssignableFrom( BEA_XMLOBJECT_CLASS_NAME, jclass, env ) )
        {
            return XML_FORM_CLASS_NAME;
        }
        else if ( isAssignableFrom( APACHE_XMLOBJECT_CLASS_NAME, jclass, env ) )
        {
            return XML_FORM_CLASS_NAME;
        }
        else
        {
            return ANY_FORM_CLASS_NAME;
        }
    }
    
    public static String getFormClassName( DeclaredType jclass, CoreAnnotationProcessorEnv env )
    {
        return getFormClassName( getDeclaration( jclass ), env );
    }
    
    public static boolean isAbsoluteURL( String path )
    {
        try
        {
            return new URI( path ).getScheme() != null;
        }
        catch ( URISyntaxException e )
        {
            return false;
        }
    }  
    
    public static boolean isAssignableFrom( String className, TypeInstance type, CoreAnnotationProcessorEnv env )
    {
        if ( ! ( type instanceof DeclaredType ) ) return false;
        return isAssignableFrom( className, getDeclaration( ( DeclaredType ) type ), env );
    }
    
    public static boolean isAssignableFrom( TypeDeclaration base, TypeDeclaration typeDecl )
    {
        if ( base != null && typeDecl != null )
        {
            if ( typesAreEqual( typeDecl, base ) ) return true;
            
            if ( typeDecl instanceof ClassDeclaration )
            {
                ClassType superclass = ( ( ClassDeclaration ) typeDecl ).getSuperclass();
                if ( superclass != null && isAssignableFrom( base, getDeclaration( superclass ) ) ) return true;
            }
            
            InterfaceType[] superInterfaces = typeDecl.getSuperinterfaces();
            for ( int i = 0; i < superInterfaces.length; i++ )
            {
                InterfaceType superInterface = superInterfaces[i];
                if ( isAssignableFrom( base, getDeclaration( superInterface ) ) ) return true;
            }
        }
        
        return false;
    }
    
    public static boolean isAssignableFrom( TypeInstance base, TypeDeclaration cl )
    {
        if ( ! ( base instanceof DeclaredType ) ) return false;
        return isAssignableFrom( getDeclaration( ( DeclaredType ) base ), cl );
    }
    
    public static boolean isAssignableFrom( TypeDeclaration base, TypeInstance cl )
    {
        if ( ! ( cl instanceof DeclaredType ) ) return false;
        return isAssignableFrom( base, getDeclaration( ( DeclaredType ) cl ) );
    }
    
    public static boolean isAssignableFrom( String className, TypeDeclaration cl, CoreAnnotationProcessorEnv env )
    {
        TypeDeclaration base = env.getTypeDeclaration( className );
        return isAssignableFrom( base, cl );
    }
    
    public static boolean isOfClass( TypeInstance type, String className, CoreAnnotationProcessorEnv env )
    {
        if ( ! ( type instanceof DeclaredType ) ) return false;
        return typesAreEqual( getDeclaration( ( DeclaredType ) type ), env.getTypeDeclaration( className ) );
    }
    
    public static boolean typesAreEqual( TypeDeclaration t1, TypeDeclaration t2 )
    {
        assert t1 != null;
        if ( t2 == null ) return false;
        return t1.getQualifiedName().equals( t2.getQualifiedName() );
    }
    
    public static TypeDeclaration getOuterClass( MemberDeclaration classMember )
    {
        return classMember instanceof ClassDeclaration
                   ? ( ClassDeclaration ) classMember
                   : classMember.getDeclaringType();
    }
    
    public static TypeDeclaration getOutermostClass( MemberDeclaration classMember )
    {
        TypeDeclaration containingClass;
        while ( ( containingClass = classMember.getDeclaringType() ) != null )
        {
            classMember = containingClass;
        }
        
        assert classMember instanceof ClassDeclaration : classMember.getClass().getName();
        return ( ClassDeclaration ) classMember;
    } 
    
    public static boolean hasDefaultConstructor( TypeDeclaration jclass )
    {
        if ( ! ( jclass instanceof ClassDeclaration ) ) return false;
        
        ConstructorDeclaration[] constructors = ( ( ClassDeclaration ) jclass ).getConstructors();
        
        for ( int i = 0; i < constructors.length; i++ )
        {
            ConstructorDeclaration ctor = constructors[i];
            if ( ctor.getParameters().length == 0 ) return true;
        }
        
        return false;
    }
    
    private static Declaration findElement( Collection elements, String elementName )
    {
        for ( Iterator ii = elements.iterator(); ii.hasNext(); )  
        {
            Object element = ii.next();
            Declaration decl = ( Declaration ) element;
            if (  decl.getSimpleName().equals( elementName ) ) return decl;
        }
        
        return null;
    }
    
    public static FieldDeclaration findField( TypeDeclaration jclass, String fieldName )
    {
        return ( FieldDeclaration ) findElement( getClassFields( jclass ), fieldName );
    }
    
    public static ClassDeclaration findInnerClass( TypeDeclaration jclass, String innerClassName )
    {
        return ( ClassDeclaration ) findElement( getClassNestedTypes( jclass ), innerClassName );
    }
    
    public static String getEnumFieldName( AnnotationValue enumMember )
    {
        if ( enumMember == null || enumMember.getValue() == null )
            return "";
        else
            return enumMember.getValue().toString();
    }
    
    /**
     * Get the qualified name of the given class, with '$' used to separate inner classes; the returned string can be
     * used with Class.forName().
     */ 
    public static String getLoadableName( TypeDeclaration jclass )
    {
        TypeDeclaration containingClass = jclass.getDeclaringType();
        
        if ( containingClass == null )
        {
            return jclass.getQualifiedName();
        }
        else
        {
            return getLoadableName( containingClass ) + '$' + jclass.getSimpleName();
        }
    }
    
    public static String getLoadableName( DeclaredType jclass )
    {
        return getLoadableName( getDeclaration( jclass ) );
    }
    
    public static File getSourceFile( TypeDeclaration decl, boolean mustBeNonNull )
    {
        decl = getOutermostClass( decl );
        SourcePosition position = decl.getPosition();
        if ( mustBeNonNull ) assert position != null : "no source file for " + decl.toString();
        return position != null ? position.file() : null;
    }
    
    public static class ExtendedCoreAnnotationProcessorEnv
            implements CoreAnnotationProcessorEnv
    {
        private CoreAnnotationProcessorEnv _env;
        private boolean _useEqualsToCompareAnnotations;
        private HashMap _attributes;

        public ExtendedCoreAnnotationProcessorEnv( CoreAnnotationProcessorEnv env,
                                                   boolean useEqualsToCompareAnnotations )
        {
            _env = env;
            _useEqualsToCompareAnnotations = useEqualsToCompareAnnotations;
        }
        
        public boolean useEqualsToCompareAnnotations()
        {
            return _useEqualsToCompareAnnotations;
        }
        
        public Map getOptions()
        {
            return _env.getOptions();
        }

        public Messager getMessager()
        {
            return _env.getMessager();
        }

        public Filer getFiler()
        {
            return _env.getFiler();
        }

        public TypeDeclaration[] getSpecifiedTypeDeclarations()
        {
            return _env.getSpecifiedTypeDeclarations();
        }

        public TypeDeclaration getTypeDeclaration( String s )
        {
            return _env.getTypeDeclaration( s );
        }

        public Declaration[] getDeclarationsAnnotatedWith( AnnotationTypeDeclaration annotationTypeDeclaration )
        {
            return _env.getDeclarationsAnnotatedWith( annotationTypeDeclaration );
        }
        
        public void setAttribute( String propertyName, Object value )
        {
            if ( _attributes == null ) _attributes = new HashMap();
            _attributes.put( propertyName, value );
        }
    
        public Object getAttribute( String propertyName )
        {
            return _attributes != null ? _attributes.get( propertyName ) : null;
        }
    }
    
    public static boolean annotationsAreEqual( AnnotationInstance a1, AnnotationInstance a2, boolean allowExactDuplicates,
                                               CoreAnnotationProcessorEnv env ) 
    {
        assert a1 != null;
        if ( a2 == null ) return false;
        
        //
        // TODO: This entire method is a workaround for a bug in APT where an annotation may not equal itelf.
        // If this behavior changes, we want to rely on equals(), not this deep comparison, which is more expensive
        // and wrong if the two annotations 'look' exactly the same.
        //
        if ( ! allowExactDuplicates
             && env instanceof ExtendedCoreAnnotationProcessorEnv
             && ( ( ExtendedCoreAnnotationProcessorEnv ) env ).useEqualsToCompareAnnotations() )
        {
            return a1.equals( a2 );
        }
        
        Map vals1 = a1.getElementValues();
        Map vals2 = a2.getElementValues();
        
        if ( vals1.size() != vals2.size() ) return false;
        
        
        Iterator ents1 = vals1.entrySet().iterator();
        Iterator ents2 = vals2.entrySet().iterator();
        while ( ents1.hasNext() )
        {
            Map.Entry entry1 = ( Map.Entry ) ents1.next();
            Map.Entry entry2 = ( Map.Entry ) ents2.next();
            
            if ( ! ( ( AnnotationTypeElementDeclaration ) entry1.getKey() ).getSimpleName().equals( ( ( AnnotationTypeElementDeclaration ) entry2.getKey() ).getSimpleName() ) ) return false;
            Object val1 = ( ( AnnotationValue ) entry1.getValue() ).getValue();
            Object val2 = ( ( AnnotationValue ) entry2.getValue() ).getValue();
            
            if ( val1 instanceof Collection )
            {
                if ( ! ( val2 instanceof Collection ) ) return false;
                Collection list1 = ( Collection ) val1;
                Collection list2 = ( Collection ) val2;
                if ( list1.size() != list2.size() ) return false;
                Iterator j1 = list1.iterator();
                Iterator j2 = list2.iterator();
                
                while ( j1.hasNext() )
                {
                    Object o1 = ( ( AnnotationValue ) j1.next() ).getValue();
                    Object o2 = ( ( AnnotationValue ) j2.next() ).getValue();
                    
                    if ( o1 instanceof AnnotationInstance )
                    {
                        if ( ! ( o2 instanceof AnnotationInstance ) ) return false;
                        if ( ! annotationsAreEqual( ( AnnotationInstance ) o1, ( AnnotationInstance ) o2, allowExactDuplicates, env ) ) return false;
                    }
                    else
                    {
                        if ( ! o1.equals( o2 ) ) return false;
                    }
                }
            }
            else if ( val1 instanceof AnnotationInstance )
            {
                if ( ! ( val2 instanceof AnnotationInstance ) ) return false;
                if ( ! annotationsAreEqual( ( AnnotationInstance ) val1, ( AnnotationInstance ) val2, allowExactDuplicates, env ) ) return false;
            }
            else if ( ! val1.equals( val2 ) )
            {
                return false;
            }
        }
        
        return true;
    }
    
    public static class BeanPropertyDescriptor
    {
        private String _propertyName;
        private String _type;
        
        public BeanPropertyDescriptor( String propertyName, String type )
        {
            _propertyName = propertyName;
            _type = type;
        }
        
        public String getPropertyName()
        {
            return _propertyName;
        }
        
        public String getType()
        {
            return _type;
        }
    }
    
    public static class BeanPropertyDeclaration
            extends BeanPropertyDescriptor
    {
        private MethodDeclaration _getter;
        
        
        public BeanPropertyDeclaration( String propertyName, String type, MethodDeclaration getter )
        {
            super( propertyName, type );
            _getter = getter;
        }
        
        public MethodDeclaration getGetter()
        {
            return _getter;
        }
    }
    
    public static BeanPropertyDeclaration getBeanProperty( MethodDeclaration method )
    {
        if ( method.hasModifier( Modifier.PUBLIC ) && ! method.hasModifier( Modifier.STATIC ) )
        {
            String returnType = method.getReturnType().toString();
            
            if ( ! returnType.equals( "void" ) && method.getParameters().length == 0 )
            {
                String methodName = method.getSimpleName();
                String propertyName = null;
                
                if ( methodName.startsWith( GETTER_PREFIX ) && methodName.length() > GETTER_PREFIX.length() )
                {
                    propertyName = methodName.substring( GETTER_PREFIX.length() );
                }
                else if ( methodName.startsWith( BOOLEAN_GETTER_PREFIX ) && returnType.equals( "boolean" )
                          && methodName.length() > BOOLEAN_GETTER_PREFIX.length() )
                {
                    propertyName = methodName.substring( BOOLEAN_GETTER_PREFIX.length() );
                }
                
                if ( propertyName != null )
                {
                    //
                    // If the first two letters are uppercase, we don't change the first character to lowercase.
                    // This is so that something like getURI has a property name of 'URI' (see JavaBeans spec).
                    //
                    if ( propertyName.length() == 1 )
                    {
                        propertyName = propertyName.toLowerCase();
                    }
                    else if ( ! Character.isUpperCase( propertyName.charAt( 1 ) ) )
                    {
                        propertyName = Character.toLowerCase( propertyName.charAt( 0 ) ) + propertyName.substring( 1 );
                    }
                    
                    return new BeanPropertyDeclaration( propertyName, returnType, method );
                }
            }
        }
        
        return null;
    }
    
    public static Collection getBeanProperties( ClassDeclaration type, boolean getInheritedProperties )
    {
        MethodDeclaration[] methods =  getInheritedProperties ? getClassMethods( type, null ) : type.getMethods();
        ArrayList ret = new ArrayList();
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            
            if ( method.hasModifier( Modifier.PUBLIC ) )
            {
                BeanPropertyDeclaration bpd = getBeanProperty( method );
                if ( bpd != null ) ret.add( bpd );
            }
        }
        
        return ret;
    }
    
    public static boolean isPageFlowClass( ClassDeclaration jclass, CoreAnnotationProcessorEnv env )
    {
        return getAnnotation( jclass, CONTROLLER_TAG_NAME ) != null && isAssignableFrom( JPF_BASE_CLASS, jclass, env );
    }

    public static String removeFileExtension( String uri )
    {
        int lastDot = uri.lastIndexOf( '.' );
        return uri.substring( 0, lastDot );
    }
    
    public static TypeDeclaration inferTypeFromPath( String webappRelativePath, CoreAnnotationProcessorEnv env )
    {
        assert webappRelativePath.startsWith( "/" ) : webappRelativePath;
        String className = removeFileExtension( webappRelativePath.substring( 1 ) );
        return env.getTypeDeclaration( className.replace( '/', '.' ) );
    }

    /**
     * Infers the Struts module path from the given controller class.
     */
    public static String inferModulePathFromType(TypeDeclaration type) {
        PackageDeclaration pkg = type.getPackage();
        return pkg != null ? '/' + pkg.getQualifiedName().replace('.', '/') : "/";
    }
    
    public static TypeDeclaration getDeclaration( DeclaredType type )
    {
        TypeDeclaration decl = type != null ? type.getDeclaration() : null;
        return decl != null ? decl : ERROR_TYPE_DECLARATION;
    }
    
    private static class ErrorTypeInstance
        implements TypeInstance
    {
        public String toString()
        {
            return ERROR_STRING;
        }
    }
    
    private static class ErrorTypeDeclaration
        implements TypeDeclaration
    {
        private static final InterfaceType[] SUPERINTERFACES = new InterfaceType[0];
        private static final FieldDeclaration[] FIELDS = new FieldDeclaration[0];
        private static final MethodDeclaration[] METHODS = new MethodDeclaration[0];
        private static final TypeDeclaration[] NESTED_TYPES = new TypeDeclaration[0];
        private static final AnnotationInstance[] ANNOTATIONS = new AnnotationInstance[0];
        
        public PackageDeclaration getPackage()
        {
            throw new IllegalStateException( "not implemented " );
        }

        public String getQualifiedName()
        {
            return ERROR_STRING;
        }

        /*
        public Collection getFormalTypeParameters()
        {
            return Collections.EMPTY_LIST;
        }
        */

        public InterfaceType[] getSuperinterfaces()
        {
            return SUPERINTERFACES;
        }

        public FieldDeclaration[] getFields()
        {
            return FIELDS;
        }

        public MethodDeclaration[] getMethods()
        {
            return METHODS;
        }

        public TypeDeclaration[] getNestedTypes()
        {
            return NESTED_TYPES;
        }

        public TypeDeclaration getDeclaringType()
        {
            return null;
        }

        public String getDocComment()
        {
            throw new IllegalStateException( "not implemented " );
        }

        public AnnotationInstance[] getAnnotationInstances()
        {
            return ANNOTATIONS;
        }

        /*
        public Annotation getAnnotation( Class s )
        {
            throw new IllegalStateException( "not implemented " );
        }
        */

        public Set getModifiers()
        {
            return Collections.EMPTY_SET;
        }

        public String getSimpleName()
        {
            return getQualifiedName();
        }

        public SourcePosition getPosition()
        {
            throw new IllegalStateException( "not implemented " );
        }
        
        public boolean hasModifier( Modifier modifier )
        {
            return false;
        }

        /*
        public void accept( DeclarationVisitor declarationVisitor )
        {
            throw new IllegalStateException( "not implemented " );
        }
        */
    }
    
    /** 
     * This is the same logic that we have in the runtime, in PageFlowRequestProcessor.  Can't share the code, though.
     */
    public static boolean isAbsoluteURI( String uri )
    {
         //
         // This method needs to be fast, so it can't use java.net.URI.
         //
         if ( uri.length() == 0 || uri.charAt( 0 ) == '/' ) return false;
        
         for ( int i = 0, len = uri.length(); i < len; ++i )
         {
             char c = uri.charAt( i );
            
             if ( c == ':' )
             {
                 return true;
             }
             else if ( c == '/' )
             {
                 return false;
             }
         }
        
         return false;
     }
    
    public static TypeInstance getArrayBaseType( ArrayType arrayType )
    {
        TypeInstance baseType = arrayType;
        
        do
        {
            baseType = ( ( ArrayType ) baseType ).getComponentType();
        } while ( baseType instanceof ArrayType );
        
        return baseType;
    }
    
    public static final class Mutable
    {
        private Object _val = null;
    
        public Mutable()
        {
        }
        
        public Mutable( Object val )
        {
            _val = val;
        }
    
        public void set( Object val )
        {
            _val = val;
        }
    
        public Object get()
        {
            return _val;
        }
    }
    
    public static TypeInstance getGenericBoundsType( TypeInstance type )
    {
        if ( type instanceof TypeVariable )
        {
            ReferenceType[] bounds = ( ( TypeVariable ) type ).getDeclaration().getBounds();
            return bounds.length > 0 ? bounds[0] : type;
        }
        
        return type;
    }
    
    public static File getFileRelativeToSourceFile( TypeDeclaration outerClass, String relativePath,
                                                    CoreAnnotationProcessorEnv env )
            throws FatalCompileTimeException
    {
        assert relativePath.length() > 0;
        
        //
        // If it starts with '/', just find the webapp-relative file.
        //
        if ( relativePath.charAt( 0 ) == '/' ) return getWebappRelativeFile( relativePath, true, env );
        
        //
        // Look for the file relative to the source directory of the given class.
        //
        File sourceFile = getSourceFile( outerClass, false );
        File desiredParentDir = sourceFile.getAbsoluteFile().getParentFile();
        File retVal = new File( desiredParentDir, relativePath );
        
        //
        // If we still haven't found it, construct a webapp-relative path and look for the file relative to the webapp.
        //
        if ( ! retVal.exists() )
        {
            PackageDeclaration jpfPackage = outerClass.getPackage();
            return getWebappRelativeFile( getPathRelativeToPackage( relativePath, jpfPackage ), true, env );
        }
            
        return retVal;
    }
    
    public static String getPathRelativeToPackage( String relativePath, PackageDeclaration packageDecl )
    {
        if ( packageDecl != null )
        {
            String packageName = packageDecl.getQualifiedName();
            if ( packageName.length() > 0 ) return '/' + packageName.replace( '.', '/' ) + '/' + relativePath;
        }
        
        return '/' + relativePath;
    }
    
    public static File getWebappRelativeFile( String webappRelativePath, boolean lookInSourceRoots,
                                              CoreAnnotationProcessorEnv env )
        throws FatalCompileTimeException
    {
        //
        // Look for the file out in the web-addressable portion of the webapp.
        //
        assert webappRelativePath.startsWith( "/" ) : webappRelativePath;
        String[] webContentRoots = getWebContentRoots( env );
        
        for ( int i = 0; i < webContentRoots.length; i++ )
        {
            String webContentRoot = webContentRoots[i];
            File retVal = new File( webContentRoot + webappRelativePath );
            if ( retVal.exists() ) return retVal;
        }
        
        //
        // If appropriate, look for the file under all the source roots.
        //
        if ( lookInSourceRoots )
        {
            String[] webSourceRoots = getWebSourceRoots( env );
            
            for ( int i = 0; i < webSourceRoots.length; i++ )
            {
                String webSourceRoot = webSourceRoots[i];
                File webSourceRelativeFile = new File( webSourceRoot + webappRelativePath );
                if ( webSourceRelativeFile.exists() ) return webSourceRelativeFile;
            }
        }
        
        return null;
    }
    
    public static String[] getWebSourceRoots( CoreAnnotationProcessorEnv env )
        throws FatalCompileTimeException
    {
        String[] legacyOption = getOption( "-Aweb.source.roots", false, env );  // deprecated - TODO: deprecation message
        if ( legacyOption != null ) return legacyOption;
        return getOption( "-sourcepath", true, env );
    }
    
    public static String[] getWebContentRoots( CoreAnnotationProcessorEnv env )
            throws FatalCompileTimeException
    {
        return getOption( "-Aweb.content.root", true, env );
    }

    /**
     * Utility method to get the value of the "phase" option from the annotation processing environment.
     * @param env the annotation processing environment
     * @return the value of the "phase" option, or <code>null</code> if the option is unspecified
     * @throws FatalCompileTimeException if an error occurs getting the value of the option 
     */
    public static String isReconcilePhase(CoreAnnotationProcessorEnv env)
        throws FatalCompileTimeException {
        String[] phase = getOption("-A" + JpfLanguageConstants.ANNOTATION_PROCESSOR_OPTION_PHASE, false, env);
        return phase != null && phase.length > 0 ? phase[0] : null;
    }

    private static String[] getOption( String optionName, boolean required, CoreAnnotationProcessorEnv env )
        throws MissingOptionException
    {
        String[] cached = ( String[] ) env.getAttribute( optionName );
        if ( cached != null ) return cached;

        Map options = env.getOptions();
        String value = ( String ) options.get( optionName );

        if ( value == null )
        {
            // TODO: there appears to be a bug in APT where both the key/value are contained in the key
            String aptOption = optionName + '=';
            for ( Iterator i = options.keySet().iterator(); i.hasNext(); )
            {
                String key = ( String ) i.next();

                if ( key.startsWith( aptOption ) )
                {
                    value = key.substring( aptOption.length() );
                    break;
                }
            }
        }

        if ( value == null )
        {
            if ( required ) throw new MissingOptionException( optionName );
            return null;
        }

        String[] values = value.trim().split( File.pathSeparator );
        for ( int i = 0; i < values.length; i++ )
        {
            values[i] = values[i].trim();
        }

        env.setAttribute( optionName, values );
        return values;
    }
}
