/*
 * 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.
 */
package org.apache.myfaces.extensions.validator.beanval.validation;

import org.apache.myfaces.extensions.validator.beanval.BeanValidationModuleKey;
import org.apache.myfaces.extensions.validator.beanval.ExtValBeanValidationContext;
import org.apache.myfaces.extensions.validator.beanval.annotation.ModelValidation;
import org.apache.myfaces.extensions.validator.beanval.storage.ModelValidationEntry;
import org.apache.myfaces.extensions.validator.beanval.util.BeanValidationUtils;
import org.apache.myfaces.extensions.validator.core.el.ELHelper;
import org.apache.myfaces.extensions.validator.core.property.DefaultPropertyInformation;
import org.apache.myfaces.extensions.validator.core.property.PropertyDetails;
import org.apache.myfaces.extensions.validator.core.property.PropertyInformation;
import org.apache.myfaces.extensions.validator.core.property.PropertyInformationKeys;
import org.apache.myfaces.extensions.validator.core.validation.message.FacesMessageHolder;
import org.apache.myfaces.extensions.validator.util.ExtValUtils;
import org.apache.myfaces.extensions.validator.internal.ToDo;
import org.apache.myfaces.extensions.validator.internal.Priority;
import org.apache.myfaces.extensions.validator.internal.UsageInformation;
import org.apache.myfaces.extensions.validator.internal.UsageCategory;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.Path;
import javax.validation.ValidatorFactory;
import javax.validation.metadata.ConstraintDescriptor;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

/**
 * @author Gerhard Petracek
 * @since x.x.3
 */
@ToDo(value = Priority.MEDIUM, description = "refactor implementation details")
@UsageInformation(UsageCategory.INTERNAL)
public class ModelValidationPhaseListener implements PhaseListener
{
    private static final long serialVersionUID = -3482233893186708878L;

    protected final Logger logger = Logger.getLogger(getClass().getName());

    public void afterPhase(PhaseEvent phaseEvent)
    {
        logger.finest("jsr303 start model validation");

        Map<Object, List<Class>> processedValidationTargets = new HashMap<Object, List<Class>>();

        Map<String, ModelValidationResult> results = new HashMap<String, ModelValidationResult>();

        for (ModelValidationEntry modelValidationEntry : getModelValidationEntriesToValidate())
        {
            processModelValidation(modelValidationEntry, processedValidationTargets, results);
        }

        processModelValidationResults(results);

        executeGlobalAfterValidationInterceptorsFor(results);

        logger.finest("jsr303 validation finished");
    }

    private List<ModelValidationEntry> getModelValidationEntriesToValidate()
    {
        return ExtValBeanValidationContext.getCurrentInstance().getModelValidationEntriesToValidate();
    }

    private void processModelValidation(ModelValidationEntry modelValidationEntry,
                                        Map<Object, List<Class>> processedValidationTargets,
                                        Map<String, ModelValidationResult> results)
    {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        PropertyInformation propertyInformation;
        Set<ConstraintViolation<Object>> violations;
        Class[] groupsToValidate;

        ELHelper elHelper = ExtValUtils.getELHelper();

        for (Object validationTarget : modelValidationEntry.getValidationTargets())
        {
            propertyInformation = createPropertyInformation(modelValidationEntry, validationTarget, elHelper);

            if (!executeGlobalBeforeValidationInterceptors(
                    facesContext, modelValidationEntry.getComponent(), validationTarget, propertyInformation))
            {
                continue;
            }

            if(modelValidationEntry.isDisplayMessageInline())
            {
                groupsToValidate = modelValidationEntry.getGroups();
            }
            //if violation should displayed inline validation has to take place with all groups
            //which means: global messages -> filter groups already used for the validation target
            else
            {
                groupsToValidate = filterGroupsToValidate(
                        modelValidationEntry, validationTarget, processedValidationTargets);
            }

            if(!shouldContinueValidation(modelValidationEntry, groupsToValidate))
            {
                continue;
            }

            addProcessedTarget(validationTarget, groupsToValidate, processedValidationTargets);
            violations = validateTarget(validationTarget, groupsToValidate);

            if (violations != null && !violations.isEmpty())
            {
                processViolations(facesContext, modelValidationEntry, validationTarget, violations, results);
            }
        }
    }

    private Class[] filterGroupsToValidate(ModelValidationEntry modelValidationEntry,
                                           Object validationTarget,
                                           Map<Object, List<Class>> processedValidationTargets)
    {
        if(!processedValidationTargets.containsKey(validationTarget))
        {
            return modelValidationEntry.getGroups();
        }

        List<Class> result = new ArrayList<Class>();
        List<Class> validatedGroups = processedValidationTargets.get(validationTarget);

        for(Class group : modelValidationEntry.getGroups())
        {
            if(!validatedGroups.contains(group))
            {
                result.add(group);
            }
        }
        return result.toArray(new Class[result.size()]);
    }

    private boolean shouldContinueValidation(ModelValidationEntry modelValidationEntry, Class[] groupsToValidate)
    {
        return !(groupsToValidate == null || groupsToValidate.length == 0) ||
                modelValidationEntry.isDisplayMessageInline();
    }

    private void addProcessedTarget(Object validationTarget,
                                    Class[] groups,
                                    Map<Object, List<Class>> processedValidationTargets)
    {
        if(!processedValidationTargets.containsKey(validationTarget))
        {
            processedValidationTargets.put(validationTarget, new ArrayList<Class>());
        }

        List<Class> validatedGroups = processedValidationTargets.get(validationTarget);

        for(Class group : groups)
        {
            if(!validatedGroups.contains(group))
            {
                validatedGroups.add(group);
            }
        }
    }

    private PropertyInformation createPropertyInformation(
            ModelValidationEntry modelValidationEntry, Object validationTarget, ELHelper elHelper)
    {
        PropertyInformation propertyInformation;
        PropertyDetails propertyDetails;
        propertyInformation = new DefaultPropertyInformation();
        if (modelValidationEntry.getComponent() != null)
        {
            propertyDetails = elHelper.getPropertyDetailsOfValueBinding(modelValidationEntry.getComponent());
        }
        else
        {
            propertyDetails = new PropertyDetails(null, validationTarget, null);
        }
        propertyInformation.setInformation(PropertyInformationKeys.PROPERTY_DETAILS, propertyDetails);
        return propertyInformation;
    }

    private boolean executeGlobalBeforeValidationInterceptors(FacesContext facesContext,
                                                              UIComponent uiComponent,
                                                              Object validationTarget,
                                                              PropertyInformation propertyInformation)
    {
        return ExtValUtils.executeGlobalBeforeValidationInterceptors(facesContext, uiComponent, validationTarget,
                PropertyInformation.class.getName(), propertyInformation, BeanValidationModuleKey.class);
    }

    private void executeGlobalAfterValidationInterceptors(FacesContext facesContext,
                                                          UIComponent uiComponent,
                                                          Object validationTarget,
                                                          PropertyInformation propertyInformation)
    {
        ExtValUtils.executeGlobalAfterValidationInterceptors(facesContext, uiComponent, validationTarget,
                PropertyInformation.class.getName(), propertyInformation, BeanValidationModuleKey.class);
    }

    private Set<ConstraintViolation<Object>> validateTarget(Object validationTarget, Class[] groups)
    {
        if(groups == null || groups.length == 0)
        {
            return null;
        }

        ValidatorFactory validatorFactory = ExtValBeanValidationContext.getCurrentInstance().getValidatorFactory();
        return validatorFactory
                .usingContext()
                .messageInterpolator(ExtValBeanValidationContext.getCurrentInstance().getMessageInterpolator())
                .constraintValidatorFactory(validatorFactory.getConstraintValidatorFactory())
                .traversableResolver(validatorFactory.getTraversableResolver())
                .getValidator()
                .validate(validationTarget, groups);
    }

    private void processViolations(FacesContext facesContext,
                                   ModelValidationEntry modelValidationEntry,
                                   Object validationTarget,
                                   Set<ConstraintViolation<Object>> violations,
                                   Map<String, ModelValidationResult> results)
    {
        //jsf 2.0 is able to display multiple messages per component - so process all violations
        //jsf < 2.0 will just use the first one (for inline messages - so it's only a little overhead)
        Iterator<ConstraintViolation<Object>> violationsIterator = violations.iterator();
        ConstraintViolation<Object> constraintViolation;
        ModelValidationResult result;
        while (violationsIterator.hasNext())
        {
            tryToCreateModelValidationResult(facesContext, modelValidationEntry, results);

            result = resolveModelValidationResult(facesContext, modelValidationEntry, results);

            constraintViolation = violationsIterator.next();
            addViolationMessage(modelValidationEntry, validationTarget, constraintViolation, result);
        }
    }

    private void tryToCreateModelValidationResult(FacesContext facesContext,
                                                  ModelValidationEntry modelValidationEntry,
                                                  Map<String, ModelValidationResult> results)
    {
        ModelValidationResult result;
        if (!isModelValidationResultAvailableFor(facesContext, modelValidationEntry, results))
        {
            result = new ModelValidationResult();
            results.put(modelValidationEntry.getComponent().getClientId(facesContext), result);
        }
    }

    private ModelValidationResult resolveModelValidationResult(FacesContext facesContext,
                                                               ModelValidationEntry modelValidationEntry,
                                                               Map<String, ModelValidationResult> results)
    {
        return results.get(modelValidationEntry.getComponent().getClientId(facesContext));
    }

    private boolean isModelValidationResultAvailableFor(FacesContext facesContext,
                                                        ModelValidationEntry modelValidationEntry,
                                                        Map<String, ModelValidationResult> results)
    {
        return results.containsKey(modelValidationEntry.getComponent().getClientId(facesContext));
    }

    private void addViolationMessage(ModelValidationEntry modelValidationEntry,
                                     Object validationTarget,
                                     ConstraintViolation<Object> constraintViolation,
                                     ModelValidationResult result)
    {
        if (modelValidationEntry.isDisplayMessageInline())
        {
            result.addFacesMessageHolder(createFacesMessageHolderForConstraintViolation(
                    constraintViolation, modelValidationEntry, validationTarget, true));
        }
        else
        {
            result.addFacesMessageHolder(createFacesMessageHolderForConstraintViolation(
                    constraintViolation, modelValidationEntry, validationTarget, false));
        }
    }

    private FacesMessageHolder createFacesMessageHolderForConstraintViolation(final ConstraintViolation violation,
                                                                              ModelValidationEntry modelValidationEntry,
                                                                              final Object validationTarget,
                                                                              boolean displayAtComponent)
    {
        final String newViolationMessage = tryToChangeViolationMessage(
                modelValidationEntry, validationTarget, violation);

        ConstraintViolation newConstraintViolation = new ConstraintViolation()
        {
            private ConstraintViolation wrapped = violation;

            public String getMessage()
            {
                return newViolationMessage;
            }

            public String getMessageTemplate()
            {
                return wrapped.getMessageTemplate();
            }

            public Object getRootBean()
            {
                return wrapped.getRootBean();
            }

            public Class getRootBeanClass()
            {
                return wrapped.getRootBeanClass();
            }

            public Object getLeafBean()
            {
                return wrapped.getLeafBean();
            }

            public Path getPropertyPath()
            {
                return wrapped.getPropertyPath();
            }

            public Object getInvalidValue()
            {
                return wrapped.getInvalidValue();
            }

            public ConstraintDescriptor getConstraintDescriptor()
            {
                return wrapped.getConstraintDescriptor();
            }
        };


        UIComponent uiComponent = null;
        String clientId = null;

        if (displayAtComponent)
        {
            uiComponent = modelValidationEntry.getComponent();
            clientId = uiComponent.getClientId(FacesContext.getCurrentInstance());
        }

        FacesMessageHolder result = new FacesMessageHolder(BeanValidationUtils
                .createFacesMessageForConstraintViolation(uiComponent, validationTarget, newConstraintViolation));
        result.setClientId(clientId);
        return result;
    }

    private String tryToChangeViolationMessage(ModelValidationEntry modelValidationEntry,
                                               Object validationTarget,
                                               ConstraintViolation violation)
    {
        if (!isDefaultMessage(modelValidationEntry))
        {
            return interpolateValidationErrorMessage(
                    modelValidationEntry.getCustomMessage(), validationTarget, violation);
        }
        return violation.getMessage();
    }

    private boolean isDefaultMessage(ModelValidationEntry modelValidationEntry)
    {
        return ModelValidation.DEFAULT.equals(modelValidationEntry.getCustomMessage());
    }

    private String interpolateValidationErrorMessage(String extValInlineMessage,
                                                     final Object validationTarget, final ConstraintViolation violation)
    {
        return ExtValBeanValidationContext.getCurrentInstance().getMessageInterpolator()
                .interpolate(
                        extValInlineMessage,
                        new MessageInterpolator.Context()
                        {
                            public ConstraintDescriptor<?> getConstraintDescriptor()
                            {
                                return violation.getConstraintDescriptor();
                            }

                            public Object getValidatedValue()
                            {
                                return validationTarget;
                            }
                        }
                );
    }

    @ToDo(Priority.MEDIUM)
    private void processModelValidationResults(Map<String, ModelValidationResult> results)
    {
        for (ModelValidationResult result : results.values())
        {
            BeanValidationUtils.processViolationMessages(result.getFacesMessageHolderList());
        }
    }

    private void executeGlobalAfterValidationInterceptorsFor(Map<String, ModelValidationResult> results)
    {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        UIComponent component;
        for (ModelValidationResult result : results.values())
        {
            for (FacesMessageHolder facesMessageHolder : result.getFacesMessageHolderList())
            {
                component = null;
                if (facesMessageHolder.getClientId() != null && !facesMessageHolder.getClientId().equals("*"))
                {
                    component = facesContext.getViewRoot().findComponent(facesMessageHolder.getClientId());
                }
                executeGlobalAfterValidationInterceptors(facesContext, component, null, null);
            }
        }
    }

    public void beforePhase(PhaseEvent phaseEvent)
    {
        //do nothing
    }

    public PhaseId getPhaseId()
    {
        return PhaseId.UPDATE_MODEL_VALUES;
    }

    /*
     * generated
     */
    @Override
    public boolean equals(Object o)
    {
        if (this == o)
        {
            return true;
        }
        if (!(o instanceof ModelValidationPhaseListener))
        {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode()
    {
        return super.hashCode();
    }
}
