/*
 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.javafx.fxml;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

import java.lang.reflect.*;
import java.security.AccessController;
import java.security.PrivilegedAction;

import javafx.beans.value.ObservableValue;
import sun.reflect.misc.FieldUtil;
import sun.reflect.misc.MethodUtil;
import sun.reflect.misc.ReflectUtil;

/**
 * Exposes Java Bean properties of an object via the {@link Map} interface.
 * A call to {@link Map#get(Object)} invokes the getter for the corresponding
 * property, and a call to {@link Map#put(Object, Object)} invokes the
 * property's setter. Appending a "Property" suffix to the key returns the
 * corresponding property model.
 */
public class BeanAdapter extends AbstractMap<String, Object> {
    private final Object bean;

    private static class MethodCache {
        private final Map<String, List<Method>> methods;
        private final MethodCache nextClassCache;

        private MethodCache(Map<String, List<Method>> methods, MethodCache nextClassCache) {
            this.methods = methods;
            this.nextClassCache = nextClassCache;
        }

        private Method getMethod(String name, Class<?>... parameterTypes) {
            List<Method> namedMethods = methods.get(name);
            if (namedMethods != null) {
                for (int i = 0; i < namedMethods.size(); i++) {
                    Method namedMethod = namedMethods.get(i);
                    if (namedMethod.getName().equals(name)
                            && Arrays.equals(namedMethod.getParameterTypes(), parameterTypes)) {
                        return namedMethod;
                    }
                }
            }

            return nextClassCache != null ? nextClassCache.getMethod(name, parameterTypes) : null;
        }

    }

    private static final HashMap<Class<?>, MethodCache> globalMethodCache =
        new HashMap<>();

    private final MethodCache localCache;

    public static final String GET_PREFIX = "get";
    public static final String IS_PREFIX = "is";
    public static final String SET_PREFIX = "set";
    public static final String PROPERTY_SUFFIX = "Property";

    public static final String VALUE_OF_METHOD_NAME = "valueOf";

    /**
     * Creates a new Bean adapter.
     *
     * @param bean
     * The Bean object to wrap.
     */
    public BeanAdapter(Object bean) {
        this.bean = bean;

        localCache = getClassMethodCache(bean.getClass());
    }

    private static MethodCache getClassMethodCache(final Class<?> type) {
        if (type == Object.class) {
            return null;
        }
        MethodCache classMethodCache;
        synchronized (globalMethodCache) {
            if ((classMethodCache = globalMethodCache.get(type)) != null) {
                return classMethodCache;
            }
            Map<String, List<Method>> classMethods = new HashMap<>();

            ReflectUtil.checkPackageAccess(type);
            if (Modifier.isPublic(type.getModifiers())) {
                // only interested in public methods in public classes in
                // non-restricted packages
                final Method[] declaredMethods =
                        AccessController.doPrivileged(
                                new PrivilegedAction<Method[]>() {
                                    @Override
                                    public Method[] run() {
                                        return type.getDeclaredMethods();
                                    }
                                });
                for (int i = 0; i < declaredMethods.length; i++) {
                    Method method = declaredMethods[i];
                    int modifiers = method.getModifiers();

                    if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) {
                        String name = method.getName();
                        List<Method> namedMethods = classMethods.get(name);

                        if (namedMethods == null) {
                            namedMethods = new ArrayList<>();
                            classMethods.put(name, namedMethods);
                        }

                        namedMethods.add(method);
                    }
                }
            }
            MethodCache cache = new MethodCache(classMethods, getClassMethodCache(type.getSuperclass()));
            globalMethodCache.put(type, cache);
            return cache;
        }
    }

    /**
     * Returns the Bean object this adapter wraps.
     *
     * @return
     * The Bean object, or <tt>null</tt> if no Bean has been set.
     */
    public Object getBean() {
        return bean;
    }

    private Method getGetterMethod(String key) {
        Method getterMethod = localCache.getMethod(getMethodName(GET_PREFIX, key));

        if (getterMethod == null) {
            getterMethod = localCache.getMethod(getMethodName(IS_PREFIX, key));
        }

        return getterMethod;
    }

    private Method getSetterMethod(String key) {
        Class<?> type = getType(key);

        if (type == null) {
            throw new UnsupportedOperationException("Cannot determine type for property.");
        }

        return localCache.getMethod(getMethodName(SET_PREFIX, key), type);
    }

    private static String getMethodName(String prefix, String key) {
        return prefix + Character.toUpperCase(key.charAt(0)) + key.substring(1);
    }

    /**
     * Invokes the getter method for the given property.
     *
     * @param key
     * The property name.
     *
     * @return
     * The value returned by the method, or <tt>null</tt> if no such method
     * exists.
     */
    @Override
    public Object get(Object key) {
        if (key == null) {
            throw new NullPointerException();
        }

        return get(key.toString());
    }

    private Object get(String key) {
        Method getterMethod = key.endsWith(PROPERTY_SUFFIX) ? localCache.getMethod(key) : getGetterMethod(key);

        Object value;
        if (getterMethod != null) {
            try {
                value = MethodUtil.invoke(getterMethod, bean, (Object[]) null);
            } catch (IllegalAccessException exception) {
                throw new RuntimeException(exception);
            } catch (InvocationTargetException exception) {
                throw new RuntimeException(exception);
            }
        } else {
            value = null;
        }

        return value;
    }

    /**
     * Invokes a setter method for the given property. The
     * {@link #coerce(Object, Class)} method is used as needed to attempt to
     * convert a given value to the property type, as defined by the return
     * value of the getter method.
     *
     * @param key
     * The property name.
     *
     * @param value
     * The new property value.
     *
     * @return
     * Returns <tt>null</tt>, since returning the previous value would require
     * an unnecessary call to the getter method.
     *
     * @throws PropertyNotFoundException
     * If the given property does not exist or is read-only.
     */
    @Override
    public Object put(String key, Object value) {
        if (key == null) {
            throw new NullPointerException();
        }

        Method setterMethod = getSetterMethod(key);

        if (setterMethod == null) {
            throw new PropertyNotFoundException("Property \"" + key + "\" does not exist"
                + " or is read-only.");
        }

        try {
            MethodUtil.invoke(setterMethod, bean, new Object[] { coerce(value, getType(key)) });
        } catch (IllegalAccessException exception) {
            throw new RuntimeException(exception);
        } catch (InvocationTargetException exception) {
            throw new RuntimeException(exception);
        }

        return null;
    }

    /**
     * Verifies the existence of a property.
     *
     * @param key
     * The property name.
     *
     * @return
     * <tt>true</tt> if the property exists; <tt>false</tt>, otherwise.
     */
    @Override
    public boolean containsKey(Object key) {
        if (key == null) {
            throw new NullPointerException();
        }

        return getType(key.toString()) != null;
    }

    @Override
    public Set<Entry<String, Object>> entrySet() {
        throw new UnsupportedOperationException();
    }

    /**
     * Tests the mutability of a property.
     *
     * @param key
     * The property name.
     *
     * @return
     * <tt>true</tt> if the property is read-only; <tt>false</tt>, otherwise.
     */
    public boolean isReadOnly(String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        return getSetterMethod(key) == null;
    }

    /**
     * Returns the property model for the given property.
     *
     * @param key
     * The property name.
     *
     * @return
     * The named property model, or <tt>null</tt> if no such property exists.
     */
    @SuppressWarnings("unchecked")
    public <T> ObservableValue<T> getPropertyModel(String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        return (ObservableValue<T>)get(key + BeanAdapter.PROPERTY_SUFFIX);
    }

    /**
     * Returns the type of a property.
     *
     * @param key
     * The property name.
     */
    public Class<?> getType(String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        Method getterMethod = getGetterMethod(key);

        return (getterMethod == null) ? null : getterMethod.getReturnType();
    }

    /**
     * Returns the generic type of a property.
     *
     * @param key
     * The property name.
     */
    public Type getGenericType(String key) {
        if (key == null) {
            throw new NullPointerException();
        }

        Method getterMethod = getGetterMethod(key);

        return (getterMethod == null) ? null : getterMethod.getGenericReturnType();
    }

    @Override
    public boolean equals(Object object) {
        boolean equals = false;

        if (object instanceof BeanAdapter) {
            BeanAdapter beanAdapter = (BeanAdapter)object;
            equals = (bean == beanAdapter.bean);
        }

        return equals;
    }

    @Override
    public int hashCode() {
        return (bean == null) ? -1 : bean.hashCode();
    }

    /**
     * Coerces a value to a given type.
     *
     * @param value
     * @param type
     *
     * @return
     * The coerced value.
     */
    @SuppressWarnings("unchecked")
    public static <T> T coerce(Object value, Class<? extends T> type) {
        if (type == null) {
            throw new NullPointerException();
        }

        Object coercedValue = null;

        if (value == null) {
            // Null values can only be coerced to null
            coercedValue = null;
        } else if (type.isAssignableFrom(value.getClass())) {
            // Value doesn't require coercion
            coercedValue = value;
        } else if (type == Boolean.class
            || type == Boolean.TYPE) {
            coercedValue = Boolean.valueOf(value.toString());
        } else if (type == Character.class
            || type == Character.TYPE) {
            coercedValue = value.toString().charAt(0);
        } else if (type == Byte.class
            || type == Byte.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).byteValue();
            } else {
                coercedValue = Byte.valueOf(value.toString());
            }
        } else if (type == Short.class
            || type == Short.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).shortValue();
            } else {
                coercedValue = Short.valueOf(value.toString());
            }
        } else if (type == Integer.class
            || type == Integer.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).intValue();
            } else {
                coercedValue = Integer.valueOf(value.toString());
            }
        } else if (type == Long.class
            || type == Long.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).longValue();
            } else {
                coercedValue = Long.valueOf(value.toString());
            }
        } else if (type == BigInteger.class) {
            if (value instanceof Number) {
                coercedValue = BigInteger.valueOf(((Number)value).longValue());
            } else {
                coercedValue = new BigInteger(value.toString());
            }
        } else if (type == Float.class
            || type == Float.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).floatValue();
            } else {
                coercedValue = Float.valueOf(value.toString());
            }
        } else if (type == Double.class
            || type == Double.TYPE) {
            if (value instanceof Number) {
                coercedValue = ((Number)value).doubleValue();
            } else {
                coercedValue = Double.valueOf(value.toString());
            }
        } else if (type == Number.class) {
            String number = value.toString();
            if (number.contains(".")) {
                coercedValue = Double.valueOf(number);
            } else {
                coercedValue = Long.valueOf(number);
            }
        } else if (type == BigDecimal.class) {
            if (value instanceof Number) {
                coercedValue = BigDecimal.valueOf(((Number)value).doubleValue());
            } else {
                coercedValue = new BigDecimal(value.toString());
            }
        } else if (type == Class.class) {
            try {
                final String className = value.toString();
                ReflectUtil.checkPackageAccess(className);
                final ClassLoader cl = Thread.currentThread().getContextClassLoader();
                coercedValue = Class.forName(
                        className,
                        false,
                        cl);
            } catch (ClassNotFoundException exception) {
                throw new IllegalArgumentException(exception);
            }
        } else {
            Class<?> valueType = value.getClass();
            Method valueOfMethod = null;

            while (valueOfMethod == null
                && valueType != null) {
                try {
                    ReflectUtil.checkPackageAccess(type);
                    valueOfMethod = type.getDeclaredMethod(VALUE_OF_METHOD_NAME, valueType);
                } catch (NoSuchMethodException exception) {
                    // No-op
                }

                if (valueOfMethod == null) {
                    valueType = valueType.getSuperclass();
                }
            }

            if (valueOfMethod == null) {
                throw new IllegalArgumentException("Unable to coerce " + value + " to " + type + ".");
            }

            if (type.isEnum()
                && value instanceof String
                && Character.isLowerCase(((String)value).charAt(0))) {
                value = toAllCaps((String)value);
            }

            try {
                coercedValue = MethodUtil.invoke(valueOfMethod, null, new Object[] { value });
            } catch (IllegalAccessException exception) {
                throw new RuntimeException(exception);
            } catch (InvocationTargetException exception) {
                throw new RuntimeException(exception);
            } catch (SecurityException exception) {
                throw new RuntimeException(exception);
            }
        }

        return (T)coercedValue;
    }

    /**
     * Invokes the static getter method for the given property.
     *
     * @param target
     * The object to which the property is attached.
     *
     * @param sourceType
     * The class that defines the property.
     *
     * @param key
     * The property name.
     *
     * @return
     * The value returned by the method, or <tt>null</tt> if no such method
     * exists.
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Object target, Class<?> sourceType, String key) {
        T value = null;

        Class<?> targetType = target.getClass();
        Method getterMethod = getStaticGetterMethod(sourceType, key, targetType);

        if (getterMethod != null) {
            try {
                value = (T) MethodUtil.invoke(getterMethod, null, new Object[] { target } );
            } catch (InvocationTargetException exception) {
                throw new RuntimeException(exception);
            } catch (IllegalAccessException exception) {
                throw new RuntimeException(exception);
            }
        }

        return value;
    }

    /**
     * Invokes a static setter method for the given property. If the value is
     * <tt>null</tt> or there is no explicit setter for a given type, the
     * {@link #coerce(Object, Class)} method is used to attempt to convert the
     * value to the actual property type (defined by the return value of the
     * getter method).
     *
     * @param target
     * The object to which the property is or will be attached.
     *
     * @param sourceType
     * The class that defines the property.
     *
     * @param key
     * The property name.
     *
     * @param value
     * The new property value.
     *
     * @throws PropertyNotFoundException
     * If the given static property does not exist or is read-only.
     */
    public static void put(Object target, Class<?> sourceType, String key, Object value) {
        Class<?> targetType = target.getClass();

        Method setterMethod = null;
        if (value != null) {
            setterMethod = getStaticSetterMethod(sourceType, key, value.getClass(), targetType);
        }

        if (setterMethod == null) {
            // Get the property type and attempt to coerce the value to it
            Class<?> propertyType = getType(sourceType, key, targetType);

            if (propertyType != null) {
                setterMethod = getStaticSetterMethod(sourceType, key, propertyType, targetType);
                value = coerce(value, propertyType);
            }
        }

        if (setterMethod == null) {
            throw new PropertyNotFoundException("Static property \"" + key + "\" does not exist"
                + " or is read-only.");
        }

        // Invoke the setter
        try {
            MethodUtil.invoke(setterMethod, null, new Object[] { target, value });
        } catch (InvocationTargetException exception) {
            throw new RuntimeException(exception);
        } catch (IllegalAccessException exception) {
            throw new RuntimeException(exception);
        }
    }

    /**
     * Tests the existence of a static property.
     *
     * @param sourceType
     * The class that defines the property.
     *
     * @param key
     * The property name.
     *
     * @param targetType
     * The type of the object to which the property applies.
     *
     * @return
     * <tt>true</tt> if the property exists; <tt>false</tt>, otherwise.
     */
    public static boolean isDefined(Class<?> sourceType, String key, Class<?> targetType) {
        return (getStaticGetterMethod(sourceType, key, targetType) != null);
    }

    /**
     * Returns the type of a static property.
     *
     * @param sourceType
     * The class that defines the property.
     *
     * @param key
     * The property name.
     *
     * @param targetType
     * The type of the object to which the property applies.
     */
    public static Class<?> getType(Class<?> sourceType, String key, Class<?> targetType) {
        Method getterMethod = getStaticGetterMethod(sourceType, key, targetType);
        return (getterMethod == null) ? null : getterMethod.getReturnType();
    }

    /**
     * Returns the generic type of a static property.
     *
     * @param sourceType
     * The class that defines the property.
     *
     * @param key
     * The property name.
     *
     * @param targetType
     * The type of the object to which the property applies.
     */
    public static Type getGenericType(Class<?> sourceType, String key, Class<?> targetType) {
        Method getterMethod = getStaticGetterMethod(sourceType, key, targetType);
        return (getterMethod == null) ? null : getterMethod.getGenericReturnType();
    }

    /**
     * Determines the type of a list item.
     *
     * @param listType
     */
    public static Class<?> getListItemType(Type listType) {
        Type itemType = getGenericListItemType(listType);

        if (itemType instanceof ParameterizedType) {
            itemType = ((ParameterizedType)itemType).getRawType();
        }

        return (Class<?>)itemType;
    }

    /**
     * Determines the type of a map value.
     *
     * @param mapType
     */
    public static Class<?> getMapValueType(Type mapType) {
        Type valueType = getGenericMapValueType(mapType);

        if (valueType instanceof ParameterizedType) {
            valueType = ((ParameterizedType)valueType).getRawType();
        }

        return (Class<?>)valueType;
    }

    /**
     * Determines the type of a list item.
     *
     * @param listType
     */
    public static Type getGenericListItemType(Type listType) {
        Type itemType = null;

        Type parentType = listType;
        while (parentType != null) {
            if (parentType instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType)parentType;
                Class<?> rawType = (Class<?>)parameterizedType.getRawType();

                if (List.class.isAssignableFrom(rawType)) {
                    itemType = parameterizedType.getActualTypeArguments()[0];
                }

                break;
            }

            Class<?> classType = (Class<?>)parentType;
            Type[] genericInterfaces = classType.getGenericInterfaces();

            for (int i = 0; i < genericInterfaces.length; i++) {
                Type genericInterface = genericInterfaces[i];

                if (genericInterface instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType)genericInterface;
                    Class<?> interfaceType = (Class<?>)parameterizedType.getRawType();

                    if (List.class.isAssignableFrom(interfaceType)) {
                        itemType = parameterizedType.getActualTypeArguments()[0];
                        break;
                    }
                }
            }

            if (itemType != null) {
                break;
            }

            parentType = classType.getGenericSuperclass();
        }

        if (itemType != null && itemType instanceof TypeVariable<?>) {
            itemType = Object.class;
        }

        return itemType;
    }

    /**
     * Determines the type of a map value.
     *
     * @param mapType
     */
    public static Type getGenericMapValueType(Type mapType) {
        Type valueType = null;

        Type parentType = mapType;
        while (parentType != null) {
            if (parentType instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType)parentType;
                Class<?> rawType = (Class<?>)parameterizedType.getRawType();

                if (Map.class.isAssignableFrom(rawType)) {
                    valueType = parameterizedType.getActualTypeArguments()[1];
                }

                break;
            }

            Class<?> classType = (Class<?>)parentType;
            Type[] genericInterfaces = classType.getGenericInterfaces();

            for (int i = 0; i < genericInterfaces.length; i++) {
                Type genericInterface = genericInterfaces[i];

                if (genericInterface instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType)genericInterface;
                    Class<?> interfaceType = (Class<?>)parameterizedType.getRawType();

                    if (Map.class.isAssignableFrom(interfaceType)) {
                        valueType = parameterizedType.getActualTypeArguments()[1];
                        break;
                    }
                }
            }

            if (valueType != null) {
                break;
            }

            parentType = classType.getGenericSuperclass();
        }

        if (valueType != null && valueType instanceof TypeVariable<?>) {
            valueType = Object.class;
        }

        return valueType;
    }

    /**
     * Returns the value of a named constant.
     *
     * @param type
     * The type that defines the constant.
     *
     * @param name
     * The name of the constant.
     */
    public static Object getConstantValue(Class<?> type, String name) {
        if (type == null) {
            throw new IllegalArgumentException();
        }

        if (name == null) {
            throw new IllegalArgumentException();
        }

        Field field;
        try {
            field = FieldUtil.getField(type, name);
        } catch (NoSuchFieldException exception) {
            throw new IllegalArgumentException(exception);
        }

        int fieldModifiers = field.getModifiers();
        if ((fieldModifiers & Modifier.STATIC) == 0
            || (fieldModifiers & Modifier.FINAL) == 0) {
            throw new IllegalArgumentException("Field is not a constant.");
        }

        Object value;
        try {
            value = field.get(null);
        } catch (IllegalAccessException exception) {
            throw new IllegalArgumentException(exception);
        }

        return value;
    }

    private static Method getStaticGetterMethod(Class<?> sourceType, String key,
        Class<?> targetType) {
        if (sourceType == null) {
            throw new NullPointerException();
        }

        if (key == null) {
            throw new NullPointerException();
        }

        Method method = null;

        if (targetType != null) {
            key = Character.toUpperCase(key.charAt(0)) + key.substring(1);

            String getMethodName = GET_PREFIX + key;
            String isMethodName = IS_PREFIX + key;

            try {
                method = MethodUtil.getMethod(sourceType, getMethodName, new Class[] { targetType });
            } catch (NoSuchMethodException exception) {
                // No-op
            }

            if (method == null) {
                try {
                    method = MethodUtil.getMethod(sourceType, isMethodName, new Class[] { targetType });
                } catch (NoSuchMethodException exception) {
                    // No-op
                }
            }

            // Check for interfaces
            if (method == null) {
                Class<?>[] interfaces = targetType.getInterfaces();
                for (int i = 0; i < interfaces.length; i++) {
                    try {
                        method = MethodUtil.getMethod(sourceType, getMethodName, new Class[] { interfaces[i] });
                    } catch (NoSuchMethodException exception) {
                        // No-op
                    }

                    if (method == null) {
                        try {
                            method = MethodUtil.getMethod(sourceType, isMethodName, new Class[] { interfaces[i] });
                        } catch (NoSuchMethodException exception) {
                            // No-op
                        }
                    }

                    if (method != null) {
                        break;
                    }
                }
            }

            if (method == null) {
                method = getStaticGetterMethod(sourceType, key, targetType.getSuperclass());
            }
        }

        return method;
    }

    private static Method getStaticSetterMethod(Class<?> sourceType, String key,
        Class<?> valueType, Class<?> targetType) {
        if (sourceType == null) {
            throw new NullPointerException();
        }

        if (key == null) {
            throw new NullPointerException();
        }

        if (valueType == null) {
            throw new NullPointerException();
        }

        Method method = null;

        if (targetType != null) {
            key = Character.toUpperCase(key.charAt(0)) + key.substring(1);

            String setMethodName = SET_PREFIX + key;
            try {
                method = MethodUtil.getMethod(sourceType, setMethodName, new Class[] { targetType, valueType });
            } catch (NoSuchMethodException exception) {
                // No-op
            }

            // Check for interfaces
            if (method == null) {
                Class<?>[] interfaces = targetType.getInterfaces();
                for (int i = 0; i < interfaces.length; i++) {
                    try {
                        method = MethodUtil.getMethod(sourceType, setMethodName, new Class[] { interfaces[i], valueType });
                    } catch (NoSuchMethodException exception) {
                        // No-op
                    }

                    if (method != null) {
                        break;
                    }
                }
            }

            if (method == null) {
                method = getStaticSetterMethod(sourceType, key, valueType, targetType.getSuperclass());
            }
        }

        return method;
    }

    private static String toAllCaps(String value) {
        if (value == null) {
            throw new NullPointerException();
        }

        StringBuilder allCapsBuilder = new StringBuilder();

        for (int i = 0, n = value.length(); i < n; i++) {
            char c = value.charAt(i);

            if (Character.isUpperCase(c)) {
                allCapsBuilder.append('_');
            }

            allCapsBuilder.append(Character.toUpperCase(c));
        }

        return allCapsBuilder.toString();
    }
}
