/*
 * 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.core.validation.parameter;

import org.apache.myfaces.extensions.validator.internal.UsageInformation;
import org.apache.myfaces.extensions.validator.internal.UsageCategory;
import org.apache.myfaces.extensions.validator.internal.ToDo;
import org.apache.myfaces.extensions.validator.internal.Priority;

import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.WildcardType;
import java.lang.reflect.Modifier;

/**
 * Default implementation of the class which is capable of extracting ValidationParameter's from an annotation.
 * 
 * @author Gerhard Petracek
 * @since x.x.3
 */
@UsageInformation(UsageCategory.INTERNAL)
public class DefaultValidationParameterExtractor implements ValidationParameterExtractor
{
    protected final Logger logger = Logger.getLogger(getClass().getName());

    public Map<Object, List<Object>> extract(Annotation annotation)
    {
        return extractById(annotation, null);
    }

    public List<Object> extract(Annotation annotation, Object key)
    {
        return extractById(annotation, key, null);
    }

    public <T> List<T> extract(Annotation annotation, Object key, Class<T> valueType)
    {
        return extractById(annotation, key, valueType, null);
    }

    public <T> T extract(Annotation annotation, Object key, Class<T> valueType, Class valueId)
    {
        List<T> results = extractById(annotation, key, valueType, valueId);

        if(results.iterator().hasNext())
        {
            return results.iterator().next();
        }

        return null;
    }

    @SuppressWarnings({"unchecked"})
    public <T> List<T> extractById(Annotation annotation, Object key, Class<T> valueType, Class valueId)
    {
        List<Object> result = new ArrayList<Object>();

        for(Object entry : extractById(annotation, key, valueId))
        {
            if(valueType.isAssignableFrom(entry.getClass()))
            {
                result.add(entry);
            }
        }

        return (List<T>)result;
    }

    public List<Object> extractById(Annotation annotation, Object key, Class valueId)
    {
        Map<Object, List<Object>> fullResult = extractById(annotation, valueId);

        if(fullResult.containsKey(key))
        {
            return fullResult.get(key);
        }

        return new ArrayList<Object>();
    }

    @ToDo(value = Priority.MEDIUM, description = "add web.xml parameter for performance tuning to deactivate the scan")
    public Map<Object, List<Object>> extractById(Annotation annotation, Class valueId)
    {
        Map<Object, List<Object>> result = new HashMap<Object, List<Object>>();

        for(Method currentAnnotationAttribute : annotation.annotationType().getDeclaredMethods())
        {
            try
            {
                if(!isValidationParameter(currentAnnotationAttribute.getGenericReturnType()))
                {
                    continue;
                }

                Object parameterValue = currentAnnotationAttribute.invoke(annotation);

                if(parameterValue instanceof Class[])
                {
                    for(Class currentParameterValue : (Class[])parameterValue)
                    {
                        //keep check so that following is true:
                        //if at least one parameter is found which tells that it isn't a blocking error, let it pass
                        processParameterValue(annotation, currentParameterValue, result, valueId);
                    }
                }
                else if(parameterValue instanceof Class)
                {
                    //keep check so that following is true:
                    //if at least one parameter is found which tells that it isn't a blocking error, let it pass
                    processParameterValue(annotation, (Class)parameterValue, result, valueId);
                }
            }
            catch (Exception e)
            {
                this.logger.log(Level.WARNING, "invalid method", e);
            }
        }

        return result;
    }

    /*
     * don't use the Introspector in this case
     * if you have a better solution which supports all supported parameter styles (see extval wiki),
     * you can impl. it and use it (exchange the impls. via the ExtValContext).
     * furthermore, you can provide the fix for the community
     */
    private void processParameterValue(
            Annotation annotation, Class paramClass, Map<Object, List<Object>> result, Class valueId) throws Exception
    {
        Object key = null;
        List<Object> parameterValues = new ArrayList<Object>();

        if(ValidationParameter.class.isAssignableFrom(paramClass))
        {
            List<Field> processedFields = new ArrayList<Field>();
            List<Method> processedMethods = new ArrayList<Method>();

            Class currentParamClass = paramClass;
            while (currentParamClass != null && !Object.class.getName().equals(currentParamClass.getName()))
            {
                /*
                 * process class
                 */
                //support pure interface approach e.g. ViolationSeverity.Warn.class
                for(Field currentField : currentParamClass.getDeclaredFields())
                {
                    if(!processedFields.contains(currentField))
                    {
                        key = processFoundField(annotation, currentField, parameterValues, key, valueId);
                        processedFields.add(currentField);
                    }
                }

                //inspect the other methods of the implementing class
                for(Method currentMethod : currentParamClass.getDeclaredMethods())
                {
                    if(!processedMethods.contains(currentMethod))
                    {
                        key = processFoundMethod(currentParamClass, currentMethod, parameterValues, key, valueId);
                        processedMethods.add(currentMethod);
                    }
                }

                /*
                 * process interfaces
                 */
                for(Class currentInterface : currentParamClass.getInterfaces())
                {
                    if(!ValidationParameter.class.isAssignableFrom(currentInterface))
                    {
                        continue;
                    }

                    //support interface + impl. approach e.g. MyParamImpl.class
                    //(MyParamImpl implements MyParam
                    //MyParam extends ValidationParameter
                    //methods in the interface have to be marked with @ParameterValue and @ParameterKey
                    for(Method currentMethod : currentInterface.getDeclaredMethods())
                    {
                        if(!processedMethods.contains(currentMethod))
                        {
                            key = processFoundMethod(currentParamClass, currentMethod, parameterValues, key, valueId);
                            processedMethods.add(currentMethod);
                        }
                    }

                    for(Field currentField : currentInterface.getDeclaredFields())
                    {
                        if(!processedFields.contains(currentField))
                        {
                            key = processFoundField(annotation, currentField, parameterValues, key, valueId);
                            processedFields.add(currentField);
                        }
                    }
                }

                currentParamClass = currentParamClass.getSuperclass();
            }
        }

        key = createDefaultKey(key, paramClass);

        if(parameterValues.isEmpty())
        {
            //@ParameterValue is optional as well
            parameterValues.add(key);
        }

        if(result.containsKey(key))
        {
            result.get(key).addAll(parameterValues);
        }
        else
        {
            result.put(key, parameterValues);
        }
    }

    private Object createDefaultKey(Object key, Class currentClass)
    {
        if(key == null)
        {
            //check for super-interface (exclude ValidationParameter itself)
            for(Class interfaceClass : currentClass.getInterfaces())
            {
                if(ValidationParameter.class.isAssignableFrom(interfaceClass) &&
                        (!interfaceClass.getName().equals(ValidationParameter.class.getName())))
                {
                    key = interfaceClass;
                    break;
                }
            }
        }

        if(key == null)
        {
            key = currentClass;
        }

        return key;
    }

    private Object processFoundField(
            Object instance, Field currentField, List<Object> paramValues, Object key, Class valueId)
    {
        Object newKey = null;
        if(key == null && currentField.isAnnotationPresent(ParameterKey.class))
        {
            try
            {
                newKey = currentField.get(instance);
            }
            catch (Exception e)
            {
                this.logger.log(Level.WARNING, "invalid field", e);
            }
        }
        //no "else if" to allow both at one field
        if(currentField.isAnnotationPresent(ParameterValue.class))
        {
            if(valueId == null || valueId.equals(currentField.getAnnotation(ParameterValue.class).id()))
            {
                currentField.setAccessible(true);
                try
                {
                    paramValues.add(currentField.get(instance));
                }
                catch (Exception e)
                {
                    this.logger.log(Level.WARNING, "invalid field", e);
                }
            }
        }

        return newKey != null ? newKey : key;
    }

    private Object processFoundMethod(
            Class paramClass, Method currentMethod, List<Object> parameterValues, Object key, Class valueId)
    {
        Object newKey = null;
        if(key == null && currentMethod.isAnnotationPresent(ParameterKey.class))
        {
            try
            {
                if(!(Modifier.isAbstract(paramClass.getModifiers()) || Modifier.isInterface(paramClass.getModifiers())))
                {
                    newKey = currentMethod.invoke(paramClass.newInstance());
                }
            }
            catch (Exception e)
            {
                this.logger.log(Level.WARNING, "invalid method", e);
            }
        }
        //no "else if" to allow both at one field
        if(currentMethod.isAnnotationPresent(ParameterValue.class))
        {
            if(valueId == null || valueId.equals(currentMethod.getAnnotation(ParameterValue.class).id()))
            {
                currentMethod.setAccessible(true);
                try
                {
                    parameterValues.add(currentMethod.invoke(paramClass.newInstance()));
                }
                catch (Exception e)
                {
                    //check if it's a none-static inner class -> return this class
                    if(paramClass.getEnclosingClass() != null)
                    {
                        parameterValues.add(paramClass);
                    }
                    else
                    {
                        this.logger.log(Level.WARNING, "invalid method", e);
                    }
                }
            }
        }

        return newKey != null ? newKey : key;
    }

    private boolean isValidationParameter(Type genericReturnType)
    {
        if(genericReturnType instanceof GenericArrayType)
        {
            if(((GenericArrayType)genericReturnType).getGenericComponentType() instanceof ParameterizedType)
            {
                return analyzeParameterizedType(
                        (ParameterizedType)((GenericArrayType)genericReturnType).getGenericComponentType());
            }
        }
        else if(genericReturnType instanceof ParameterizedType)
        {
            return analyzeParameterizedType(
                    (ParameterizedType)genericReturnType);
        }

        return false;
    }

    private boolean analyzeParameterizedType(ParameterizedType parameterizedType)
    {
        for(Type type : parameterizedType.getActualTypeArguments())
        {
            if(type instanceof WildcardType)
            {
                for(Type upperBounds : ((WildcardType)type).getUpperBounds())
                {
                    if(upperBounds instanceof Class &&
                            //for attributes like: Class<? extends InheritedFromValidationParameter> value();
                            ValidationParameter.class.isAssignableFrom((Class)upperBounds))
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }
}
