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

import org.apache.beehive.netui.compiler.CompilerUtils;
import org.apache.beehive.netui.compiler.FatalCompileTimeException;
import org.apache.beehive.netui.compiler.JpfLanguageConstants;
import org.apache.beehive.netui.compiler.MergedControllerAnnotation;
import org.apache.beehive.netui.compiler.model.FormBeanModel;
import org.apache.beehive.netui.compiler.model.XmlModelWriterException;
import org.apache.beehive.netui.compiler.model.validation.ValidationModel;
import org.apache.beehive.netui.compiler.model.validation.ValidatorConstants;
import org.apache.beehive.netui.compiler.model.validation.ValidatorRule;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationInstance;
import org.apache.beehive.netui.compiler.typesystem.declaration.AnnotationValue;
import org.apache.beehive.netui.compiler.typesystem.declaration.ClassDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.MethodDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.Modifier;
import org.apache.beehive.netui.compiler.typesystem.declaration.ParameterDeclaration;
import org.apache.beehive.netui.compiler.typesystem.declaration.TypeDeclaration;
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.ClassType;
import org.apache.beehive.netui.compiler.typesystem.type.TypeInstance;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class GenValidationModel
        extends ValidationModel
        implements JpfLanguageConstants, ValidatorConstants
{
    private static final String STRUTS_VALIDATION_PREFIX = "validation";
    private static final ValidatorRuleFactory VALIDATOR_RULE_FACTORY = new DefaultValidatorRuleFactory();

    private GenStrutsApp _strutsApp;
    private File _mergeFile;
    private CoreAnnotationProcessorEnv _env;
    private HashSet processedFormBeanClasses = new HashSet();


    public GenValidationModel( ClassDeclaration jclass, GenStrutsApp strutsApp, CoreAnnotationProcessorEnv env )
            throws FatalCompileTimeException
    {
        MergedControllerAnnotation mca = strutsApp.getFlowControllerInfo().getMergedControllerAnnotation();
        _strutsApp = strutsApp;
        setValidatorVersion( mca.getValidatorVersion() );
        addRulesFromBeans( jclass );
        addRulesFromActions( jclass, mca );
        addRulesFromClass( mca );
        String mergeFileName = mca.getValidatorMerge();
        _mergeFile = strutsApp.getMergeFile( mergeFileName );
        _env = env;
    }

    private void addRulesFromBeans( ClassDeclaration jclass )
    {
        //
        // Read validation rules from public static inner classes (beans).
        //
        Collection innerTypes = CompilerUtils.getClassNestedTypes( jclass );

        for ( Iterator ii = innerTypes.iterator(); ii.hasNext(); )
        {
            TypeDeclaration innerType = ( TypeDeclaration ) ii.next();
            if ( innerType instanceof ClassDeclaration
                 && innerType.hasModifier( Modifier.PUBLIC )
                 && innerType.hasModifier( Modifier.STATIC ) )
            {
                addRulesFromBeanClass( ( ClassDeclaration ) innerType );
            }
        }
    }

    private void addRulesFromBeanClass( ClassDeclaration beanClass )
    {
        String className = beanClass.getQualifiedName();
        if (processedFormBeanClasses.contains(className)) {
            return;
        }
        else {
            processedFormBeanClasses.add(className);
        }

        Collection properties = CompilerUtils.getBeanProperties( beanClass, true );

        List formNames = null;
        if (properties != null && !properties.isEmpty()) {
            formNames = getFormBeanNames(beanClass);
        }

        for ( Iterator ii = properties.iterator(); ii.hasNext(); )
        {
            CompilerUtils.BeanPropertyDeclaration property = ( CompilerUtils.BeanPropertyDeclaration ) ii.next();
            MethodDeclaration getter = property.getGetter();
            String propertyName = property.getPropertyName();

            if ( getter != null )
            {
                //
                // Parse validation annotations on each getter.
                //
                AnnotationInstance[] annotations = getter.getAnnotationInstances();

                if ( annotations != null )
                {
                    for ( Iterator j = formNames.iterator(); j.hasNext(); )
                    {
                        String formName = ( String ) j.next();

                        for ( int i = 0; i < annotations.length; i++ )
                        {
                            AnnotationInstance ann = annotations[i];

                            if ( CompilerUtils.isJpfAnnotation( ann, VALIDATABLE_PROPERTY_TAG_NAME ) )
                            {
                                // Add field rules from the Jpf.ValidationLocaleRules annotation.
                                addRulesFromAnnotation( ann, formName, propertyName );
                            }
                        }
                    }
                }
            }
        }
    }


    private void addRulesFromAnnotation( AnnotationInstance validationFieldAnn, String entityName, String propertyName )
    {
        //
        // Add rules from the FieldValidationRules annotations in the "localeRules" member.
        //
        Collection localeRulesAnnotations =
                CompilerUtils.getAnnotationArray( validationFieldAnn, LOCALE_RULES_ATTR, false );
        String displayName = CompilerUtils.getString( validationFieldAnn, DISPLAY_NAME_ATTR, true );
        String displayNameKey = CompilerUtils.getString( validationFieldAnn, DISPLAY_NAME_KEY_ATTR, true );
        RuleInfo ruleInfo = new RuleInfo( entityName, propertyName, displayName, displayNameKey );


        for ( Iterator ii = localeRulesAnnotations.iterator(); ii.hasNext(); )
        {
            AnnotationInstance ann = ( AnnotationInstance ) ii.next();
            addFieldRules( ann, ruleInfo, false );
        }

        addFieldRules( validationFieldAnn, ruleInfo, true );
    }

    private void addRulesFromActions( ClassDeclaration jclass, MergedControllerAnnotation mca )
    {
        MethodDeclaration[] methods = CompilerUtils.getClassMethods( jclass, ACTION_TAG_NAME );

        for ( int i = 0; i < methods.length; i++ )
        {
            MethodDeclaration method = methods[i];
            AnnotationInstance actionAnnotation = CompilerUtils.getAnnotation( method, ACTION_TAG_NAME );
            assert actionAnnotation != null;
            addRulesFromActionAnnotation( actionAnnotation, method.getSimpleName() );

            ParameterDeclaration[] parameters = method.getParameters();
            if ( parameters.length > 0 )
            {
                TypeInstance type = parameters[0].getType();

                if ( type instanceof ClassType )
                {
                    ClassDeclaration classDecl = ( ( ClassType ) type ).getClassTypeDeclaration();
                    if ( classDecl.getDeclaringType() == null ) addRulesFromBeanClass( classDecl );
                }
            }
        }


        Collection simpleActions = mca.getSimpleActions();

        if ( simpleActions != null )
        {
            for ( Iterator ii = simpleActions.iterator(); ii.hasNext(); )
            {
                AnnotationInstance simpleAction = ( AnnotationInstance ) ii.next();
                String actionName = CompilerUtils.getString( simpleAction, NAME_ATTR, true );
                assert actionName != null;  //checker should enforce this.
                addRulesFromActionAnnotation( simpleAction, actionName );
            }
        }
    }

    private void addRulesFromActionAnnotation( AnnotationInstance actionAnnotation, String actionName )
    {
        Collection validatablePropertyAnnotations =
                CompilerUtils.getAnnotationArray( actionAnnotation, VALIDATABLE_PROPERTIES_ATTR, false );

        for ( Iterator ii = validatablePropertyAnnotations.iterator(); ii.hasNext(); )
        {
            AnnotationInstance validationFieldAnnotation = ( AnnotationInstance ) ii.next();
            String propertyName = CompilerUtils.getString( validationFieldAnnotation, PROPERTY_NAME_ATTR, true );
            assert propertyName != null;            // TODO: checker must enforce this
            assert ! propertyName.equals( "" );     // TODO: checker must enforce this

            //
            // Add the rules, and associate them with the action path ("/" + the action name).
            //
            String actionPath = '/' + actionName;   // Struts validator needs the slash in front
            addRulesFromAnnotation( validationFieldAnnotation, actionPath, propertyName );
        }
    }

    /**
     * Returns a list of String names.
     */
    private List getFormBeanNames(TypeDeclaration beanType)
    {
        List formBeans = _strutsApp.getMatchingFormBeans(beanType, null);
        List formBeanNames = new ArrayList();

        for ( Iterator i = formBeans.iterator(); i.hasNext(); )
        {
            FormBeanModel formBeanModel = ( FormBeanModel ) i.next();
            formBeanNames.add( formBeanModel.getName() );
        }

        return formBeanNames;
    }

    private void addRulesFromClass( MergedControllerAnnotation mca )
    {
        Collection validationBeanAnnotations = mca.getValidatableBeans();

        for ( Iterator ii = validationBeanAnnotations.iterator(); ii.hasNext(); )
        {
            AnnotationInstance validationBeanAnnotation = ( AnnotationInstance ) ii.next();
            DeclaredType beanType = CompilerUtils.getDeclaredType( validationBeanAnnotation, TYPE_ATTR, true );
            assert beanType != null;    // checker should enforce this
            List formNames = getFormBeanNames(CompilerUtils.getDeclaration(beanType));
            Collection validationFieldAnnotations =
                    CompilerUtils.getAnnotationArray( validationBeanAnnotation, VALIDATABLE_PROPERTIES_ATTR, false );

            for ( Iterator i2 = validationFieldAnnotations.iterator(); i2.hasNext(); )
            {
                AnnotationInstance validationFieldAnnotation = ( AnnotationInstance ) i2.next();
                String propName = CompilerUtils.getString( validationFieldAnnotation, PROPERTY_NAME_ATTR, true );
                assert propName != null;            // checker should enforce this
                assert ! propName.equals( "" );     // TODO: get checker to enforce this

                //
                // Add the rules.  If the bean is derived from ActionForm, associate the rules with the *name* of
                // the form; otherwise, associate them with the classname of the bean type.
                //
                for ( Iterator j = formNames.iterator(); j.hasNext(); )
                {
                    String formName = ( String ) j.next();
                    addRulesFromAnnotation( validationFieldAnnotation, formName, propName );
                }
            }
        }
    }

    /**
     * Add field rules from either a Jpf.ValidationField or a Jpf.ValidationLocaleRules annotation.
     */
    private void addFieldRules( AnnotationInstance rulesContainerAnnotation, RuleInfo ruleInfo,
                                boolean applyToAllLocales )
    {
        //
        // First parse the locale from the wrapper annotation.  This will apply to all rules inside.
        //
        Locale locale = null;

        if ( ! applyToAllLocales )
        {
            String language = CompilerUtils.getString( rulesContainerAnnotation, LANGUAGE_ATTR, true );

            //
            // If there's no language specified, then this rule will only apply for the default ruleset
            // (i.e., if there are explicit rules for the requested locale, this rule will not be run).
            //
            if ( language != null )
            {
                String country = CompilerUtils.getString( rulesContainerAnnotation, COUNTRY_ATTR, true );
                String variant = CompilerUtils.getString( rulesContainerAnnotation, VARIANT_ATTR, true );

                language = language.trim();
                if ( country != null ) country = country.trim();
                if ( variant != null ) variant = variant.trim();

                if ( country != null && variant != null ) locale = new Locale( language, country, variant );
                else if ( country != null ) locale = new Locale( language, country );
                else locale = new Locale( language );
            }
        }

        Map valuesPresent = rulesContainerAnnotation.getElementValues();

        for ( Iterator ii = valuesPresent.entrySet().iterator(); ii.hasNext(); )
        {
            Map.Entry entry = ( Map.Entry ) ii.next();
            AnnotationValue value = ( AnnotationValue ) entry.getValue();
            Object val = value.getValue();

            if ( val instanceof AnnotationInstance )
            {
                addFieldRuleFromAnnotation( ruleInfo, ( AnnotationInstance ) val, locale, applyToAllLocales );
            }
            else if ( val instanceof List )
            {
                List annotations = CompilerUtils.getAnnotationArray( value );

                for ( Iterator i3 = annotations.iterator(); i3.hasNext(); )
                {
                    AnnotationInstance i = ( AnnotationInstance ) i3.next();
                    addFieldRuleFromAnnotation( ruleInfo, i, locale, applyToAllLocales );
                }
            }
        }

        setEmpty( false );  // this ValidationModel is only "empty" if there are no rules.
    }

    private void addFieldRuleFromAnnotation( RuleInfo ruleInfo, AnnotationInstance annotation, Locale locale,
                                             boolean applyToAllLocales )
    {

        ValidatorRule rule = getFieldRule( ruleInfo.getEntityName(), ruleInfo.getFieldName(), annotation );

        if ( rule != null )
        {
            if ( applyToAllLocales )
            {
                addFieldRuleForAllLocales( ruleInfo, rule );
            }
            else
            {
                addFieldRule( ruleInfo, rule, locale );
            }
        }
    }

    private static ValidatorRule getFieldRule( String entityName, String propertyName, AnnotationInstance ruleAnnotation )
    {
        ValidatorRule rule = VALIDATOR_RULE_FACTORY.getFieldRule( entityName, propertyName, ruleAnnotation );

        if ( rule != null )
        {
            //
            // message/message-key
            //
            rule.setMessage( CompilerUtils.getString( ruleAnnotation, MESSAGE_ATTR, true ) );
            rule.setMessageKey( CompilerUtils.getString( ruleAnnotation, MESSAGE_KEY_ATTR, true ) );
            rule.setBundle( CompilerUtils.getString( ruleAnnotation, BUNDLE_NAME_ATTR, true ) );
            if ( rule.getMessage() != null ) assert rule.getMessageKey() == null;   // TODO: checker should enforce

            //
            // args
            //
            addMessageArgs( rule, ruleAnnotation );
        }

        return rule;
    }

    protected static void addMessageArgs( ValidatorRule rule, AnnotationInstance annotation )
    {
        List messageArgs =
                CompilerUtils.getAnnotationArray( annotation, MESSAGE_ARGS_ATTR, true );

        if ( messageArgs != null )
        {
            int inferredPosition = 0;
            for ( Iterator ii = messageArgs.iterator(); ii.hasNext(); )
            {
                AnnotationInstance ann = ( AnnotationInstance ) ii.next();
                String arg = CompilerUtils.getString( ann, ARG_ATTR, true );
                String bundle = CompilerUtils.getString( ann, BUNDLE_NAME_ATTR, true );
                Integer position = CompilerUtils.getInteger( ann, POSITION_ATTR, true );

                if ( position == null )
                {
                    position = new Integer( inferredPosition );
                }

                if ( arg != null )
                {
                    rule.setArg( arg, false, bundle, position );
                }
                else
                {
                    String argKey = CompilerUtils.getString( ann, ARG_KEY_ATTR, true );
                    if ( argKey != null ) rule.setArg( argKey, true, bundle, position );
                }

                inferredPosition++;
            }
        }
    }

    protected String getHeaderComment( File mergeFile )
            throws FatalCompileTimeException
    {
        return _strutsApp.getHeaderComment( mergeFile );
    }

    public void writeToFile()
        throws FileNotFoundException, IOException, FatalCompileTimeException, XmlModelWriterException
    {
        String outputFilePath = getOutputFileURI();
        File outputFile = new File( outputFilePath );
        PrintWriter writer = _env.getFiler().createTextFile( outputFile );
        try
        {
            writeXml( writer, _mergeFile );
        }
        finally
        {
            writer.close();
        }
    }

    public String getOutputFileURI()
    {
        return _strutsApp.getOutputFileURI( _strutsApp.getValidationFilePrefix() );
    }
}
