/*
 * 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.grammar;

import org.apache.beehive.netui.compiler.AnnotationMemberType;
import org.apache.beehive.netui.compiler.CompilerUtils;
import org.apache.beehive.netui.compiler.Diagnostics;
import org.apache.beehive.netui.compiler.FlowControllerInfo;
import org.apache.beehive.netui.compiler.JpfLanguageConstants;
import org.apache.beehive.netui.compiler.RuntimeVersionChecker;
import org.apache.beehive.netui.compiler.FatalCompileTimeException;
import org.apache.beehive.netui.compiler.typesystem.declaration.*;
import org.apache.beehive.netui.compiler.typesystem.env.CoreAnnotationProcessorEnv;
import org.apache.beehive.netui.compiler.typesystem.type.DeclaredType;
import org.apache.beehive.netui.compiler.typesystem.type.TypeInstance;


public class ActionGrammar
        extends BaseFlowControllerGrammar
        implements JpfLanguageConstants
{
    public ActionGrammar( CoreAnnotationProcessorEnv env, Diagnostics diags,
                          RuntimeVersionChecker rvc, FlowControllerInfo fcInfo )
    {
        super( env, diags, null, rvc, fcInfo );
        
        addMemberType( LOGIN_REQUIRED_ATTR, new AnnotationMemberType( null, this ) );
        addMemberType( ROLES_ALLOWED_ATTR, new RolesAllowedType( this ) );
        addMemberType( READONLY_ATTR, new AnnotationMemberType( VERSION_8_SP2_STRING, this ) );
        addMemberType( USE_FORM_BEAN_ATTR, new UseFormBeanType() );
        addMemberType( PREVENT_DOUBLE_SUBMIT_ATTR, new PreventDoubleSubmitType() );
        addMemberType( DO_VALIDATION_ATTR, new DoValidateType() );
        
        addMemberArrayGrammar( FORWARDS_ATTR, new ForwardGrammar( env, diags, null, rvc, fcInfo ) );
        addMemberArrayGrammar( CATCHES_ATTR, new CatchGrammar( env, diags, null, rvc, ACTION_TAG_NAME, fcInfo ) );
        addMemberArrayGrammar( VALIDATABLE_PROPERTIES_ATTR, new ValidatablePropertyGrammar( env, diags, rvc ) );
        addMemberGrammar( VALIDATION_ERROR_FORWARD_ATTR, new ActionForwardGrammar() );
    }

    public String[][] getMutuallyExclusiveAttrs()
    {
        return null;
    }

    public String[][] getRequiredAttrs()
    {
        return null;
    }

    protected boolean onBeginCheck( AnnotationInstance annotation, AnnotationInstance[] parentAnnotations, 
                                    MemberDeclaration classMember )
            throws FatalCompileTimeException
    {
        //
        // First check the form bean type.
        //
        TypeInstance argType = getFormBeanType( annotation, classMember );
        TypeDeclaration argTypeDecl = null;
        
        if ( ! ( argType instanceof DeclaredType ) )
        {
            if ( argType != null )
            {
                getDiagnostics().addError( annotation, "error.action-invalid-form-bean-type", argType.toString() );
                argType = null;
            }
        }
        else
        {
            argTypeDecl = CompilerUtils.getDeclaration( ( DeclaredType ) argType );
            boolean isClass = argTypeDecl instanceof ClassDeclaration;
            
            if ( isClass && ! CompilerUtils.hasDefaultConstructor( argTypeDecl ) )
            {
                getDiagnostics().addError( annotation, "error.action-form-bean-no-default-constructor",
                                           argTypeDecl.getQualifiedName() );
            }
            
            if ( ! argTypeDecl.hasModifier( Modifier.PUBLIC ) )
            {
                getDiagnostics().addError( annotation, "error.action-form-bean-not-public",
                                           argTypeDecl.getQualifiedName() );
            }
            
            if ( isClass && argTypeDecl.getDeclaringType() != null && ! argTypeDecl.hasModifier( Modifier.STATIC ) )
            {
                getDiagnostics().addError( annotation, "error.action-form-bean-not-static",
                                           argTypeDecl.getQualifiedName() );
            }
            
            //
            // Give a warning if there is no validationErrorForward annotation and doValidation isn't set to false.
            //
            if ( CompilerUtils.getAnnotationValue( annotation, VALIDATION_ERROR_FORWARD_ATTR, true ) == null
                 && hasValidationAnnotations( argTypeDecl ) )
            {
                Boolean doValidation = CompilerUtils.getBoolean( annotation, DO_VALIDATION_ATTR, true );
                
                if ( doValidation == null || doValidation.booleanValue() )
                {
                    getDiagnostics().addWarning(
                            annotation, "warning.validatable-formbean-no-forward",
                            ANNOTATION_INTERFACE_PREFIX + annotation.getAnnotationType().getDeclaration().getSimpleName(),
                            VALIDATION_ERROR_FORWARD_ATTR, argTypeDecl.getQualifiedName() );
                }
            }
        }
        
        //
        // Add this action to the FlowControllerInfo.
        //
        getFlowControllerInfo().addAction( getActionName( annotation, classMember ),
                                           argTypeDecl != null ? argTypeDecl.getQualifiedName() : null );

        //
        // Check to make sure the 'useFormBean' attribute (reference to a member variable) matches the form declared as
        // an argument to the action method.
        //
        TypeInstance useFormBeanType = getUseFormBeanType( annotation, classMember );
        
        if ( useFormBeanType != null && useFormBeanType instanceof DeclaredType )
        {
            if ( argType == null )
            {
                String memberFormTypeName = CompilerUtils.getDeclaration( ( DeclaredType ) useFormBeanType ).getQualifiedName();
                getDiagnostics().addError( annotation, "error.action-mismatched-form", USE_FORM_BEAN_ATTR,
                                           memberFormTypeName );
            }
            else if ( ! CompilerUtils.isAssignableFrom( argTypeDecl, useFormBeanType ))
            {
                String memberFormTypeName = CompilerUtils.getDeclaration( ( DeclaredType ) useFormBeanType ).getQualifiedName();
                getDiagnostics().addError( annotation, "error.action-mismatched-form", USE_FORM_BEAN_ATTR,
                                           memberFormTypeName );
            }
        }
        
        return true;
    }

    protected String getActionName( AnnotationInstance annotation, MemberDeclaration classMember )
    {
        assert classMember instanceof MethodDeclaration : classMember.getClass().getName();
        return classMember.getSimpleName();
    }
    
    private static boolean hasValidationAnnotations( TypeDeclaration type )
    {
        // Could cache this if it's a performance problem.
        
        MethodDeclaration[] methods = type.getMethods();
        
        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            AnnotationInstance[] annotations = method.getAnnotationInstances();
            
            for ( int j = 0; j < annotations.length; j++ )
            {
                AnnotationInstance ann = annotations[j];
                String annotationName = CompilerUtils.getDeclaration( ann.getAnnotationType() ).getQualifiedName();
                int pos = annotationName.indexOf( ANNOTATION_QUALIFIER );
                
                if ( pos != -1 )
                {
                    if ( annotationName.substring( pos + ANNOTATION_QUALIFIER.length() ).startsWith( "Validat" ) )
                    {
                        return true;
                    }
                }
            }
        }
        
        return false;
    }
    
    protected static TypeInstance getUseFormBeanType( AnnotationInstance annotation, MemberDeclaration classMember )
    {
        String formBeanFieldName = CompilerUtils.getString( annotation, USE_FORM_BEAN_ATTR, true );
        
        if ( formBeanFieldName != null )
        {
            FieldDeclaration formBeanField =
                    CompilerUtils.findField( CompilerUtils.getOutermostClass( classMember ), formBeanFieldName );
            
            if ( formBeanField != null )
            {
                return CompilerUtils.getGenericBoundsType( formBeanField.getType() );
            }
        }
        
        return null;
    }
    
    protected TypeInstance getFormBeanType( AnnotationInstance annotation, MemberDeclaration classMember )
    {
        assert classMember instanceof MethodDeclaration : classMember.getClass().getName();
        MethodDeclaration method = ( MethodDeclaration ) classMember;
        ParameterDeclaration[] parameters = method.getParameters();
        int nParameters = parameters.length;
        
        if ( nParameters > 1 ) getDiagnostics().addError( method, "error.action-method-wrong-arg" );
        if ( nParameters > 0 ) return CompilerUtils.getGenericBoundsType( parameters[0].getType() );
        
        return null;
    }
    
    private class DoValidateType
        extends AnnotationMemberType
    {
        public DoValidateType()
        {
            super( null, ActionGrammar.this );
        }
        
        
        public Object onCheck( AnnotationTypeElementDeclaration valueDecl, AnnotationValue member,
                               AnnotationInstance[] parentAnnotations, MemberDeclaration classMember,
                               int annotationArrayIndex )
        {
            //
            // If this value is set to true, there must be a value for validationErrorForward.
            //
            if ( ( ( Boolean ) member.getValue() ).booleanValue() )
            {
                AnnotationInstance parentAnnotation = parentAnnotations[ parentAnnotations.length - 1 ];
                
                if ( CompilerUtils.getAnnotation( parentAnnotation, VALIDATION_ERROR_FORWARD_ATTR, true ) == null )
                {
                    addError( member, "error.validate-with-no-validation-error-forward", DO_VALIDATION_ATTR,
                              VALIDATION_ERROR_FORWARD_ATTR );
                }
            }
            
            return null;
        }
    }
    
    private class ActionForwardGrammar
        extends ForwardGrammar
    {
        public ActionForwardGrammar()
        {
            super( ActionGrammar.this.getEnv(), ActionGrammar.this.getDiagnostics(), null,
                   ActionGrammar.this.getRuntimeVersionChecker(), ActionGrammar.this.getFlowControllerInfo() );
            ExternalPathOrActionType baseForwardType =
                    new ExternalPathOrActionType( false, null, this, ActionGrammar.this.getFlowControllerInfo() );
            addMemberType( PATH_ATTR, new ForwardToExternalPathType( baseForwardType, null, ActionGrammar.this ) );
        }
    }
    
    private class UseFormBeanType
        extends WritableFieldType
    {
        public UseFormBeanType()
        {
            super( null, USE_FORM_BEAN_ATTR, null, ActionGrammar.this );
        }

        
        public Object onCheck( AnnotationTypeElementDeclaration valueDecl, AnnotationValue value,
                               AnnotationInstance[] parentAnnotations, MemberDeclaration classMember,
                               int annotationArrayIndex )
        {
            FieldDeclaration memberField =
                    ( FieldDeclaration ) super.onCheck( valueDecl, value, parentAnnotations, classMember,
                                                        annotationArrayIndex );
            
            if ( memberField != null )
            {
                //
                // If this action is marked 'readOnly', print a warning about the 'useFormBean' attribute implicitly
                // modifying member data.
                //
                AnnotationInstance parentAnnotation = parentAnnotations[parentAnnotations.length - 1];
                if ( CompilerUtils.getBoolean( parentAnnotation, READONLY_ATTR, false ).booleanValue() )
                {
                    addWarning( value, "warning.use-form-bean-on-readonly-action", READONLY_ATTR, USE_FORM_BEAN_ATTR,
                                memberField.getSimpleName() );
                }
            }
            
            return memberField;
        }
    }
    
    private class PreventDoubleSubmitType
        extends AnnotationMemberType
    {
        public PreventDoubleSubmitType()
        {
            super(null, ActionGrammar.this);
        }

        public Object onCheck(AnnotationTypeElementDeclaration valueDecl, AnnotationValue member,
                              AnnotationInstance[] parentAnnotations, MemberDeclaration classMember,
                              int annotationArrayIndex) throws FatalCompileTimeException {

            // We don't allow the preventDoubleSubmit attribute on the begin action, since this
            // would make the page flow inaccessible through its URI.
            AnnotationInstance parentAnn = parentAnnotations[parentAnnotations.length - 1];
            if (BEGIN_ACTION_NAME.equals(getActionName(parentAnn, classMember))) {
                addError(member, "error.prevent-double-submit-on-begin", PREVENT_DOUBLE_SUBMIT_ATTR, BEGIN_ACTION_NAME);
            }
            return null;
        }
    }
}
