/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, 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.el;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutionException;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.context.FacesContext;

import com.google.common.base.Function;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

/**
 * @author Nick Belaevski
 *
 */
public class GenericsIntrospectionServiceImpl implements GenericsIntrospectionService {
    private static final class GenericsCacheEntry {
        private Class<?> beanClass;

        private LoadingCache<String, Class<?>> containerClassesMap = CacheBuilder.newBuilder().initialCapacity(2)
                .build(CacheLoader.from(new Function<String, Class<?>>() {
                    public Class<?> apply(String input) {
                        PropertyDescriptor propertyDescriptor = getPropertyDescriptor(input);
                        return getGenericContainerClass(propertyDescriptor);
                    }
                }));

        public GenericsCacheEntry(Class<?> beanClass) {
            this.beanClass = beanClass;
        }

        private Class<?> resolveType(Type type) {
            Class<?> result = Object.class;

            if (type instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] types = parameterizedType.getActualTypeArguments();

                if (types != null && types.length != 0) {
                    Type actualType = types[0];

                    if (actualType instanceof Class) {
                        result = (Class<?>) actualType;
                    }
                }
            }

            return result;
        }

        private Class<?> getGenericContainerClass(PropertyDescriptor pd) {
            if (pd == null) {
                return null;
            }

            Method readMethod = pd.getReadMethod();
            if (readMethod == null) {
                return null;
            }

            return resolveType(readMethod.getGenericReturnType());
        }

        private PropertyDescriptor getPropertyDescriptor(String propertyName) {
            BeanInfo beanInfo = null;
            try {
                beanInfo = Introspector.getBeanInfo(beanClass);
            } catch (IntrospectionException e) {
                throw new FacesException(e.getMessage(), e);
            } finally {
                Introspector.flushFromCaches(beanClass);
            }

            if (beanInfo == null) {
                return null;
            }

            PropertyDescriptor[] propertyDescriptorsArray = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : propertyDescriptorsArray) {
                if (propertyName.equals(pd.getName())) {
                    return pd;
                }
            }

            return null;
        }

        public Class<?> getContainerClass(String propertyName) throws ExecutionException {
            return containerClassesMap.get(propertyName);
        }
    }

    private final LoadingCache<Class<?>, GenericsCacheEntry> cache = CacheBuilder.newBuilder().weakKeys().softValues()
            .build(CacheLoader.from(new Function<Class<?>, GenericsCacheEntry>() {
                public GenericsCacheEntry apply(java.lang.Class<?> input) {
                    return new GenericsCacheEntry(input);
                }
            }));

    private Class<?> getGenericCollectionType(FacesContext context, Object base, String propertyName) {
        Class<?> genericPropertyClass = null;

        if ((base != null) && (propertyName != null)) {
            Class<? extends Object> beanClass = base.getClass();

            // Map and ResourceBundle have special resolvers that we doesn't support
            if (!Map.class.isAssignableFrom(beanClass) && !ResourceBundle.class.isAssignableFrom(beanClass)) {
                try {
                    return cache.get(beanClass).getContainerClass(propertyName);
                } catch (ExecutionException e) {
                    throw new FacesException(String.format("Can't resolve the beanClass '%s' from propertyName '%s'", beanClass, propertyName), e);
                }
            }
        }

        return genericPropertyClass;
    }

    public Class<?> getContainerClass(FacesContext facesContext, ValueExpression expression) {
        ELContext initialELContext = facesContext.getELContext();
        CapturingELResolver capturingELResolver = new CapturingELResolver(initialELContext.getELResolver());
        Class<?> type = expression.getType(new ELContextWrapper(initialELContext, capturingELResolver));
        Class<?> containerType = type.getComponentType();

        if ((containerType == null) && (type != null)) {
            if (Collection.class.isAssignableFrom(type)) {
                Object base = capturingELResolver.getBase();
                Object property = capturingELResolver.getProperty();

                if ((base != null) && (property != null)) {
                    containerType = getGenericCollectionType(facesContext, base, property.toString());
                }
            }
        }

        return containerType;
    }
}
