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

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.beans.Introspector;
import java.lang.reflect.Method;

import org.apache.myfaces.extensions.validator.core.ExtValCoreConfiguration;
import org.apache.myfaces.extensions.validator.core.property.PropertyDetails;
import org.apache.myfaces.extensions.validator.core.storage.MappedConstraintSourceStorage;
import org.apache.myfaces.extensions.validator.core.storage.PropertyStorage;
import org.apache.myfaces.extensions.validator.internal.UsageCategory;
import org.apache.myfaces.extensions.validator.internal.UsageInformation;

/**
 * Contains helper methods that deal with annotation usage in the context of ExtVal.
 *
 * @author Gerhard Petracek
 * @since r4
 */
@UsageInformation(UsageCategory.INTERNAL)
@SuppressWarnings("unchecked")
public final class ConstraintSourceUtils
{
    protected ConstraintSourceUtils()
    {
        // Utility class, don't allow instantiation.
    }

    public static PropertyDetails resolveMappedConstraintSourceFor(String originalKey,
                                                                   Class originalClass,
                                                                   String originalProperty)
    {
        MappedConstraintSourceStorage mappedConstraintSourceStorage = getConstraintSourceStorage();

        if (isMappedConstraintSourceCached(mappedConstraintSourceStorage, originalClass, originalProperty))
        {
            return getMappedConstraintSource(mappedConstraintSourceStorage, originalClass, originalProperty);
        }

        originalClass = ProxyUtils.getUnproxiedClass(originalClass);

        PropertyStorage propertyStorage = ReflectionUtils.getPropertyStorage();
        Class newClass = findMappedClass(propertyStorage, originalClass, originalProperty);

        //mapped source is ignored via @IgnoreConstraintSource or there is just no mapping annotation at the target
        if (newClass == null)
        {
            tryToCacheMappedConstraintSourceMetaData(
                    mappedConstraintSourceStorage, originalClass, originalProperty, null);

            return null;
        }

        String newProperty = findMappedProperty(propertyStorage, originalClass, newClass, originalProperty);

        PropertyDetails result = new PropertyDetails(originalKey, newClass, newProperty);

        tryToCacheMappedConstraintSourceMetaData(
                mappedConstraintSourceStorage, originalClass, originalProperty, result);

        return result;
    }

    private static boolean isMappedConstraintSourceCached(
            MappedConstraintSourceStorage storage, Class baseBeanClass, String property)
    {
        return storage.containsMapping(baseBeanClass, property);
    }

    private static PropertyDetails getMappedConstraintSource(
            MappedConstraintSourceStorage storage, Class baseBeanClass, String property)
    {
        return storage.getMappedConstraintSource(baseBeanClass, property);
    }

    private static void tryToCacheMappedConstraintSourceMetaData(
            MappedConstraintSourceStorage storage, Class originalClass, String originalProperty, PropertyDetails result)
    {
        storage.storeMapping(originalClass, originalProperty, result);
    }

    private static MappedConstraintSourceStorage getConstraintSourceStorage()
    {
        return ExtValUtils
                .getStorage(MappedConstraintSourceStorage.class, MappedConstraintSourceStorage.class.getName());
    }

    private static Class findMappedClass(PropertyStorage storage, Class baseBeanClass, String property)
    {
        Class<? extends Annotation> constraintSourceAnnotationImplementation = ExtValCoreConfiguration.get()
                .constraintSourceAnnotation();

        Annotation foundConstraintSourceAnnotation = tryToGetAnnotationFromProperty(
                storage, baseBeanClass, property, constraintSourceAnnotationImplementation);

        if (foundConstraintSourceAnnotation == null)
        {
            foundConstraintSourceAnnotation = tryToGetAnnotationFromField(
                    storage, baseBeanClass, property, constraintSourceAnnotationImplementation);
        }

        if (foundConstraintSourceAnnotation == null && !isMappedConstraintSourceIgnored(baseBeanClass, property))
        {
            foundConstraintSourceAnnotation = tryToGetConstraintSourceAnnotationFromClass(
                    baseBeanClass, constraintSourceAnnotationImplementation);
        }

        if (foundConstraintSourceAnnotation != null)
        {
            return ExtValAnnotationUtils.extractValueOf(foundConstraintSourceAnnotation, Class.class);
        }

        return null;
    }

    private static String findMappedProperty(
            PropertyStorage storage, Class baseBeanClass, Class newBaseBeanClass, String originalProperty)
    {
        Annotation targetPropertyAnnotation = getTargetPropertyMetaData(storage, baseBeanClass, originalProperty);
        if (targetPropertyAnnotation != null)
        {
            return extractNewPropertyName(newBaseBeanClass, targetPropertyAnnotation);
        }

        return originalProperty;
    }

    private static Annotation getTargetPropertyMetaData(
            PropertyStorage storage, Class baseBeanClass, String originalProperty)
    {
        Class<? extends Annotation> targetPropertyAnnotation = getTargetPropertyAnnotationImplementation();
        Class<? extends Annotation> targetPropertyIdAnnotation = getTargetPropertyIdAnnotationImplementation();

        Annotation result = findTargetPropertyIdAnnotation(
                storage, baseBeanClass, originalProperty, targetPropertyIdAnnotation);

        if (result == null)
        {
            result = findTargetPropertyAnnotation(
                    storage, baseBeanClass, originalProperty, targetPropertyAnnotation);
        }

        return result;
    }

    private static Class<? extends Annotation> getTargetPropertyAnnotationImplementation()
    {
        return ExtValCoreConfiguration.get().targetPropertyAnnotation();
    }

    private static Class<? extends Annotation> getTargetPropertyIdAnnotationImplementation()
    {
        return ExtValCoreConfiguration.get().targetPropertyIdAnnotation();
    }

    private static String extractNewPropertyName(Class targetClass, Annotation annotation)
    {
        Object annotationValue = ExtValAnnotationUtils.extractValueOf(annotation, Object.class);

        //@TargetProperty
        if (annotationValue instanceof String)
        {
            return (String) annotationValue;
        }

        //@TargetPropertyId
        if (annotationValue instanceof Class)
        {
            return findNameOfAnnotatedProperty(targetClass, (Class) annotationValue);
        }
        return null;
    }

    //EXTVAL-83/use-case 5
    private static String findNameOfAnnotatedProperty(
            Class targetClass, Class<? extends Annotation> customTargetMarkerAnnotation)
    {
        for (Method currentMethod : targetClass.getDeclaredMethods())
        {
            if (currentMethod.isAnnotationPresent(customTargetMarkerAnnotation))
            {
                return convertMethodToPropertyName(currentMethod.getName());
            }
        }

        for (Field currentField : targetClass.getDeclaredFields())
        {
            if (currentField.isAnnotationPresent(customTargetMarkerAnnotation))
            {
                return convertFieldToPropertyName(currentField.getName());
            }
        }
        return null;
    }

    private static String convertMethodToPropertyName(String name)
    {
        String result = name;

        if (name.startsWith("is"))
        {
            result = name.substring(2);
        }
        else if (name.startsWith("get"))
        {
            result = name.substring(3);
        }

        return Introspector.decapitalize(result);
    }

    private static String convertFieldToPropertyName(String name)
    {
        if (name.startsWith("_"))
        {
            return name.substring(1);
        }
        return name;
    }

    private static Annotation findTargetPropertyIdAnnotation(PropertyStorage storage,
                                                             Class baseBeanClass,
                                                             String property,
                                                             Class<? extends Annotation> targetPropertyIdAnnotation)
    {
        Annotation result = tryToGetAnnotationFromProperty(
                storage, baseBeanClass, property, targetPropertyIdAnnotation);

        if (result == null)
        {
            result = tryToGetAnnotationFromField(storage, baseBeanClass, property, targetPropertyIdAnnotation);
        }

        return result;
    }

    private static Annotation findTargetPropertyAnnotation(PropertyStorage storage,
                                                           Class baseBeanClass,
                                                           String property,
                                                           Class<? extends Annotation> targetPropertyAnnotation)
    {
        Annotation result = tryToGetAnnotationFromProperty(storage, baseBeanClass, property, targetPropertyAnnotation);

        if (result == null)
        {
            result = tryToGetAnnotationFromField(storage, baseBeanClass, property, targetPropertyAnnotation);
        }

        return result;
    }

    private static boolean isMappedConstraintSourceIgnored(Class baseBeanClass, String property)
    {
        PropertyStorage storage = ReflectionUtils.getPropertyStorage();
        Method method = ReflectionUtils.tryToGetMethodOfProperty(storage, baseBeanClass, property);

        if (method != null && method.isAnnotationPresent(getIgnoreConstraintSourceAnnotationImplementation()))
        {
            return true;
        }

        Field field = ReflectionUtils.tryToGetFieldOfProperty(storage, baseBeanClass, property);

        if (field != null && field.isAnnotationPresent(getIgnoreConstraintSourceAnnotationImplementation()))
        {
            return true;
        }

        return false;
    }

    private static Annotation tryToGetConstraintSourceAnnotationFromClass(
            Class baseBeanClass, Class<? extends Annotation> annotation)
    {
        if (baseBeanClass.isAnnotationPresent(annotation))
        {
            return baseBeanClass.getAnnotation(annotation);
        }
        return null;
    }

    private static Class<? extends Annotation> getIgnoreConstraintSourceAnnotationImplementation()
    {
        return ExtValCoreConfiguration.get().ignoreConstraintSourceAnnotation();
    }

    private static Annotation tryToGetAnnotationFromField(
            PropertyStorage storage, Class baseBeanClass, String property, Class<? extends Annotation> annotationClass)
    {
        Field field = ReflectionUtils.tryToGetFieldOfProperty(storage, baseBeanClass, property);

        if (field != null && field.isAnnotationPresent(annotationClass))
        {
            return field.getAnnotation(annotationClass);
        }
        return null;
    }

    private static Annotation tryToGetAnnotationFromProperty(PropertyStorage storage,
                                                             Class baseBeanClass,
                                                             String property,
                                                             Class<? extends Annotation> annotationClass)
    {
        Method method = ReflectionUtils.tryToGetMethodOfProperty(storage, baseBeanClass, property);

        if (method != null && method.isAnnotationPresent(annotationClass))
        {
            return method.getAnnotation(annotationClass);
        }
        return null;
    }
}
