/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.cdk.templatecompiler.el.types;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.Behavior;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.event.FacesEvent;
import javax.faces.model.DataModel;
import javax.faces.render.Renderer;
import javax.faces.validator.Validator;

import org.richfaces.cdk.CdkClassLoader;
import org.richfaces.cdk.Logger;
import org.richfaces.cdk.templatecompiler.el.ParsingException;
import org.richfaces.cdk.util.ArrayUtils;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;

/**
 * @author Nick Belaevski
 * @author Lukas Fryc
 *
 */
public final class TypesFactoryImpl implements TypesFactory {
    static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTORS = new PropertyDescriptor[0];
    static final ImmutableMap<Class<?>, Class<?>> PRIMITIVE_TO_WRAPPER_CLASSES_MAP = ImmutableMap.<Class<?>, Class<?>>builder()
            .put(Boolean.TYPE, Boolean.class).put(Float.TYPE, Float.class).put(Long.TYPE, Long.class)
            .put(Integer.TYPE, Integer.class).put(Short.TYPE, Short.class).put(Byte.TYPE, Byte.class)
            .put(Double.TYPE, Double.class).put(Character.TYPE, Character.class).put(Void.TYPE, Void.class).build();
    static final ImmutableMap<String, Class<?>> PRIMITIVE_CLASSES_MAP;

    static {

        Builder<String, Class<?>> builder = ImmutableMap.<String, Class<?>>builder();
        for (Class<?> primitiveClass : PRIMITIVE_TO_WRAPPER_CLASSES_MAP.keySet()) {
            builder.put(primitiveClass.getName(), primitiveClass);
        }
        PRIMITIVE_CLASSES_MAP = builder.build();
    }

    private static final Function<Class<?>, String> PACKAGE_NAME_FUNCTION = new Function<Class<?>, String>() {
        @Override
        public String apply(Class<?> from) {
            return from.getPackage().getName();
        }
    };
    private static final ImmutableCollection<String> GUESS_PACKAGES = ImmutableSet.<String>copyOf(Iterables.transform(
            ImmutableSet.<Class<?>>of(UIComponent.class, Behavior.class, Converter.class, Validator.class, FacesContext.class,
                    Application.class, FacesEvent.class, DataModel.class, Renderer.class, Collection.class, Object.class),
            PACKAGE_NAME_FUNCTION));
    private final ClassLoader classLoader;
    private final Logger log;
    private final Map<java.lang.reflect.Type, ELType> reflectionTypesCache = new ConcurrentHashMap<java.lang.reflect.Type, ELType>();
    private final Map<String, ELType> refferencedTypesCache = new ConcurrentHashMap<String, ELType>();
    private final Map<Class<?>, ClassDataHolder> classDataCache = Maps.newHashMap();
    private final TypeParserFactory typeParserFactory;

    @Inject
    public TypesFactoryImpl(Logger log, CdkClassLoader classLoader, TypeParserFactory typeParserFactory) {
        this.log = log;
        this.classLoader = classLoader;
        this.typeParserFactory = typeParserFactory;
    }

    private ELType getPlainClassType(Class<?> plainClass) {
        ELType plainClassType = reflectionTypesCache.get(plainClass);
        if (plainClassType == null) {
            plainClassType = new PlainClassType(plainClass);
            reflectionTypesCache.put(plainClass, plainClassType);
        }

        return plainClassType;
    }

    private ELType getReferencedType(String classCodeString) {
        ELType type = refferencedTypesCache.get(classCodeString);
        if (type == null) {
            type = new ReferencedType(classCodeString);
            refferencedTypesCache.put(classCodeString, type);
        }

        return type;
    }

    private Class<?> tryLoadClas(String className) throws ClassNotFoundException {
        int dotIndex = className.indexOf('.');
        if (dotIndex < 0) {
            // guess type
            for (String guessPackage : GUESS_PACKAGES) {
                String guessTypeName = guessPackage + "." + className;
                try {
                    // while by default initialize = true for Class.forName(String) method
                    // initialize = false used here prevents loading of dependencies that
                    // are accessible only in runtime, e.g. log initializer from API
                    // depends on the concrete logger implementation provided in runtime only
                    return Class.forName(guessTypeName, false, classLoader);
                } catch (ClassNotFoundException e) {
                    // ignore
                } catch (LinkageError e) {
                    if (log.isInfoEnabled()) {
                        log.info(MessageFormat.format("Class {0} couldn''t be loaded because of: {1}", guessTypeName,
                                e.getMessage()));
                    }
                }
            }
        }

        Class<?> result = PRIMITIVE_CLASSES_MAP.get(className);
        if (result == null) {
            try {
                // initialize = false here for the same reason as already mentioned for the previous load block
                result = Class.forName(className, false, classLoader);
            } catch (LinkageError e) {
                String errorMessage = MessageFormat.format("Class {0} couldn''t be loaded because of: {1}", className,
                        e.getMessage());
                if (log.isInfoEnabled()) {
                    log.info(errorMessage);
                }
                throw new ClassNotFoundException(errorMessage, e);
            }
        }

        return result;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.cdk.templatecompiler.el.types.TypesFactory#getType(java.lang.String)
     */
    @Override
    public ELType getType(String typeString) {
        TypeParser typeParser = typeParserFactory.getInstance(typeString);

        if (typeParser.isParseable()) {
            final String className = typeParser.getClassName();

            ELType baseType;
            try {
                // NB: loadedClass can have name that differs from className!
                Class<?> loadedClas = tryLoadClas(className);

                baseType = getType(loadedClas);
            } catch (ClassNotFoundException e) {
                baseType = getReferencedType(className);
            }

            if (typeParser.isArray()) {
                return new ComplexType(baseType, typeParser.getTypeArguments(), typeParser.getArrayDepth());
            } else {
                return baseType;
            }
        } else {
            if (log.isWarnEnabled()) {
                log.warn(MessageFormat.format("Cannot parse type signature: ''{0}''", typeString));
            }
            return getReferencedType(typeString);
        }
    }

    @Override
    public ELType getGeneratedType(String typeString, ELType superType) {
        TypeParser typeParser = typeParserFactory.getInstance(typeString);

        if (typeParser.isParseable()) {
            final String className = typeParser.getClassName();

            ELType generatedType = new GeneratedType(className, superType);

            if (typeParser.isArray()) {
                return new ComplexType(generatedType, typeParser.getTypeArguments(), typeParser.getArrayDepth());
            } else {
                return generatedType;
            }
        } else {
            if (log.isWarnEnabled()) {
                log.warn(MessageFormat.format("Cannot parse type signature: ''{0}''", typeString));
            }
            return getReferencedType(typeString);
        }
    }

    ELType createType(java.lang.reflect.Type reflectionType) {
        java.lang.reflect.Type[] actualTypeArguments = null;
        Class<?> rawType = null;
        int arrayDepth = 0;

        java.lang.reflect.Type localReflectionType = reflectionType;

        while (localReflectionType instanceof GenericArrayType) {
            localReflectionType = ((GenericArrayType) localReflectionType).getGenericComponentType();
            arrayDepth++;
        }

        if (localReflectionType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) localReflectionType;

            actualTypeArguments = parameterizedType.getActualTypeArguments();
            rawType = (Class<?>) parameterizedType.getRawType();
        } else if (localReflectionType instanceof Class<?>) {
            rawType = (Class<?>) localReflectionType;
        }

        if (rawType != null) {
            while (rawType.isArray()) {
                arrayDepth++;
                rawType = rawType.getComponentType();
            }

            ELType[] typeArguments = PlainClassType.NO_TYPES;
            if (!ArrayUtils.isEmpty(actualTypeArguments)) {
                typeArguments = getTypesArray(actualTypeArguments);
            }

            ELType clearComponentType = getPlainClassType(rawType);
            if (!ArrayUtils.isEmpty(typeArguments) || arrayDepth != 0) {
                return new ComplexType(clearComponentType, typeArguments, arrayDepth);
            } else {
                return clearComponentType;
            }
        } else {
            // TODO better way of handling unknown types
            return getReferencedType(reflectionType.toString());
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.richfaces.cdk.templatecompiler.el.types.TypesFactory#getType(java.lang.reflect.Type)
     */
    @Override
    public ELType getType(java.lang.reflect.Type reflectionType) {
        ELType result = reflectionTypesCache.get(reflectionType);
        if (result == null) {
            result = createType(reflectionType);
            reflectionTypesCache.put(reflectionType, result);
        }

        return result;
    }

    private ELType[] getTypesArray(java.lang.reflect.Type[] reflectionTypes) {
        ELType[] types = new ELType[reflectionTypes.length];
        for (int i = 0; i < reflectionTypes.length; i++) {
            types[i] = getType(reflectionTypes[i]);
        }

        return types;
    }

    /**
     * Returns wrapper classes for passed-in class. If type is primitive, then corresponding wrapper class is returned (e.g.
     * boolean -> Boolean), otherwise does nothing and returns passed-in class.
     *
     * @return wrapper for primitive types, or passed-in class
     */
    static Class<?> getWrapperClass(Class<?> inClazz) {
        if (inClazz.isPrimitive()) {
            return PRIMITIVE_TO_WRAPPER_CLASSES_MAP.get(inClazz);
        } else {
            return inClazz;
        }
    }

    private static final class JavaELPropertyDescriptor implements ELPropertyDescriptor {
        private final PropertyDescriptor descriptor;
        private final String descriptorName;
        private final ELType propertyType;

        /**
         * <p class="changed_added_4_0">
         * </p>
         *
         * @param descriptor
         * @param propertyType
         * @param descriptorName
         */
        public JavaELPropertyDescriptor(PropertyDescriptor descriptor, ELType propertyType) {
            this.descriptor = descriptor;
            this.propertyType = propertyType;
            this.descriptorName = descriptor.getName();
        }

        @Override
        public boolean isWritable() {
            return null != descriptor.getWriteMethod();
        }

        @Override
        public boolean isReadable() {
            return null != descriptor.getReadMethod();
        }

        @Override
        public String getWriteMethosName() {
            return descriptor.getWriteMethod().getName();
        }

        @Override
        public ELType getType() {
            return propertyType;
        }

        @Override
        public String getReadMethodName() {
            return descriptor.getReadMethod().getName();
        }

        @Override
        public String getName() {
            return descriptorName;
        }
    }

    private static final class ClassDataHolder implements ClassVisitor {
        private Map<String, ELPropertyDescriptor> resolvedProperties;
        private List<Method> resolvedMethods;
        private final TypesFactory typesFactory;

        public ClassDataHolder(TypesFactory typesFactory) {
            super();
            this.typesFactory = typesFactory;

            this.resolvedProperties = Maps.newHashMap();
            this.resolvedMethods = Lists.newArrayList();
        }

        public Map<String, ELPropertyDescriptor> getResolvedProperties() {
            return resolvedProperties;
        }

        public List<Method> getResolvedMethods() {
            return resolvedMethods;
        }

        @Override
        public void visit(Class<?> clazz) throws ParsingException {
            PropertyDescriptor[] pds;
            Method[] declaredMethods;

            try {
                pds = getPropertyDescriptors(clazz);
                declaredMethods = clazz.getDeclaredMethods();
            } catch (LinkageError e) {
                throw new ParsingException(e.getMessage(), e);
            }

            for (PropertyDescriptor descriptor : pds) {
                String descriptorName = descriptor.getName();
                if (resolvedProperties.get(descriptorName) == null) {

                    Type reflectionType;
                    if (null != descriptor.getReadMethod()) {
                        reflectionType = descriptor.getReadMethod().getGenericReturnType();
                    } else if (null != descriptor.getWriteMethod()) {
                        reflectionType = descriptor.getWriteMethod().getGenericParameterTypes()[0];
                    } else {
                        reflectionType = descriptor.getPropertyType();
                    }
                    ELType propertyType = typesFactory.getType(reflectionType);
                    ELPropertyDescriptor elDescriptor = new JavaELPropertyDescriptor(descriptor, propertyType);
                    resolvedProperties.put(descriptorName, elDescriptor);
                }
            }

            resolvedMethods.addAll(Arrays.asList(declaredMethods));
        }
    }

    interface ClassVisitor {
        void visit(Class<?> clazz) throws ParsingException;
    }

    static class ClassWalkingLogic {
        private Queue<Class<?>> classesList = new LinkedList<Class<?>>();
        private Set<Class<?>> visitedClasses = new HashSet<Class<?>>();

        public ClassWalkingLogic(Class<?> clazz) {
            super();
            this.classesList.add(clazz);
        }

        public void walk(ClassVisitor visitor) throws ParsingException {
            // BFS algorithm
            while (!classesList.isEmpty()) {
                Class<?> clazz = classesList.remove();

                if (visitedClasses.add(clazz)) {
                    visitor.visit(clazz);

                    Class<?> superclass = clazz.getSuperclass();
                    if (superclass != null) {
                        if (!visitedClasses.contains(superclass)) {
                            classesList.add(superclass);
                        }
                    }

                    Class<?>[] interfaces = clazz.getInterfaces();
                    if (interfaces != null) {
                        for (Class<?> iface : interfaces) {
                            if (!visitedClasses.contains(iface)) {
                                classesList.add(iface);
                            }
                        }
                    }
                }
            }

            // While interfaces do not have Object.class in their hierarchy directly,
            // implementations of interface are always inherited from Object.
            // As methods in this class are primarily designed to work with implementations (beans),
            // we are adding Object.class explicitly if it hasn't been visited yet.
            if (visitedClasses.add(Object.class)) {
                visitor.visit(Object.class);
            }

            visitedClasses.clear();
        }
    }

    private ClassDataHolder resolveClassPropertiesAndMethods(Class<?> clazz) throws ParsingException {
        ClassDataHolder classDataHolder = classDataCache.get(clazz);
        if (classDataHolder == null) {
            classDataHolder = new ClassDataHolder(this);
            new ClassWalkingLogic(clazz).walk(classDataHolder);
            classDataCache.put(clazz, classDataHolder);
        }

        return classDataHolder;
    }

    /**
     * This method return PropertyDescriptor by specified propertyName and clazz.
     *
     * @param elType - class to search
     * @param propertyName - propertyName to search
     * @return property descriptor if found.
     * @throws ParsingException if error occured.
     */
    public ELPropertyDescriptor getPropertyDescriptor(ELType elType, String propertyName) throws ParsingException {
        ELPropertyDescriptor propertyDescriptor;

        if (elType == null) {
            propertyDescriptor = new DummyPropertyDescriptor(propertyName);
        } else {
            ClassDataHolder classDataHolder = resolveClassPropertiesAndMethods(getClassFromType(elType));
            Map<String, ELPropertyDescriptor> resolvedProperties = classDataHolder.getResolvedProperties();
            if (resolvedProperties.containsKey(propertyName)) {
                propertyDescriptor = resolvedProperties.get(propertyName);
            } else {
                propertyDescriptor = new DummyPropertyDescriptor(propertyName);
            }
        }
        return propertyDescriptor;
    }

    /**
     * <p>
     * Retrieve the property descriptors for the specified class, introspecting and caching them the first time a particular
     * bean class is encountered.
     * </p>
     *
     * <p>
     * <strong>FIXME</strong> - Does not work with DynaBeans.
     * </p>
     *
     * @param beanClass Bean class for which property descriptors are requested
     * @return the property descriptors
     * @throws ParsingException if error occured.
     *
     * @exception IllegalArgumentException if <code>beanClass</code> is null
     */
    private static PropertyDescriptor[] getPropertyDescriptors(Class<?> beanClass) throws ParsingException {
        if (beanClass == null) {
            throw new IllegalArgumentException("No bean class specified");
        }

        // Look up any cached descriptors for this bean class
        PropertyDescriptor[] descriptors = null;

        // Introspect the bean and cache the generated descriptors
        BeanInfo beanInfo = null;

        try {
            beanInfo = Introspector.getBeanInfo(beanClass);
            descriptors = beanInfo.getPropertyDescriptors();
        } catch (IntrospectionException e) {
            return EMPTY_PROPERTY_DESCRIPTORS;
        }

        if (descriptors == null) {
            descriptors = EMPTY_PROPERTY_DESCRIPTORS;
        }

        return descriptors;
    }

    private static boolean isMethodVisible(Method method) {
        return !Modifier.isPrivate(method.getModifiers());
    }

    private static Class<?> getClassFromType(ELType elType) {
        if (elType == null) {
            return Object.class;
        } else if (elType instanceof PlainClassType) {
            Class<?> clazz = ((PlainClassType) elType).getPlainJavaClass();
            return clazz;
        } else if (elType instanceof ComplexType) {
            return getClassFromType(elType.getRawType());
        }
        return Object.class;
    }

    /**
     * <p>
     * Find an accessible method that matches the given name and has compatible parameters. Compatible parameters mean that
     * every method parameter is assignable from the given parameters. In other words, it finds a method with the given name
     * that will take the parameters given.
     * <p>
     *
     * <p>
     * This method is slightly undeterminstic since it loops through methods names and return the first matching method.
     * </p>
     *
     * <p>
     * This method is used by {@link #invokeMethod(Object object, String methodName, Object [] args, Class[] parameterTypes)}.
     *
     * <p>
     * This method can match primitive parameter by passing in wrapper classes. For example, a <code>Boolean</code> will match a
     * primitive <code>boolean</code> parameter.
     *
     * @param elType find method in this class
     * @param methodName find method with this name
     * @param parameterTypes find method with compatible parameters
     * @return The accessible method
     * @throws ParsingException if error occured.
     */
    public ELType getMatchingVisibleMethodReturnType(ELType elType, final String methodName, ELType... parameterTypes)
            throws ParsingException {

        if (elType instanceof GeneratedType) {
            // use the super type to resolve properties and methods
            elType = ((GeneratedType) elType).getSuperType();
        }

        ClassDataHolder classDataHolder = resolveClassPropertiesAndMethods(getClassFromType(elType));
        List<Method> resolvedMethods = classDataHolder.getResolvedMethods();

        // search through all methods
        int paramSize = parameterTypes.length;
        Method bestMatch = null;

        for (Method resolvedMethod : resolvedMethods) {
            if (isMethodVisible(resolvedMethod) && resolvedMethod.getName().equals(methodName)) {
                // compare parameters
                ELType[] methodsParams = getTypesArray(resolvedMethod.getParameterTypes());
                int methodParamSize = methodsParams.length;
                if (methodParamSize == paramSize) {
                    boolean match = true;
                    for (int n = 0; n < methodParamSize; n++) {
                        if (!methodsParams[n].isAssignableFrom(parameterTypes[n])) {
                            match = false;
                            break;
                        }
                    }
                    if (match) {
                        bestMatch = resolvedMethod;
                    }
                }
            }
        }

        if (bestMatch != null) {
            return getType(bestMatch.getGenericReturnType());
        } else {
            return TypesFactory.OBJECT_TYPE;
        }
    }
}
