/**
 * 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.activemq.tool.properties;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class ReflectionUtil {
    private static final Logger LOG = LoggerFactory.getLogger(ReflectionUtil.class);

    private ReflectionUtil() {
    }

    public static void configureClass(Object obj, String key, String val) {
        try {
            String debugInfo;

            Object target = obj;
            Class<?> targetClass = obj.getClass();

            // DEBUG: Debugging Info
            debugInfo = "Invoking: " + targetClass.getName();

            StringTokenizer tokenizer = new StringTokenizer(key, ".");
            String keySubString = key;
            int tokenCount = tokenizer.countTokens();

            // For nested settings, get the object first. -1, do not count the
            // last token
            for (int j = 0; j < tokenCount - 1; j++) {
                // Find getter method first
                String name = tokenizer.nextToken();

                // Check if the target object will accept the settings
                if (target instanceof ReflectionConfigurable && !((ReflectionConfigurable)target).acceptConfig(keySubString, val)) {
                    return;
                } else {
                    // This will reduce the key, so that it will be recognize by
                    // the next object. i.e.
                    // Property name: factory.prefetchPolicy.queuePrefetch
                    // Calling order:
                    // this.getFactory().prefetchPolicy().queuePrefetch();
                    // If factory does not accept the config, it should be given
                    // prefetchPolicy.queuePrefetch as the key
                    // +1 to account for the '.'
                    keySubString = keySubString.substring(name.length() + 1);
                }

                String getMethod = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
                Method method = targetClass.getMethod(getMethod, new Class[] {});
                target = method.invoke(target, null);
                targetClass = target.getClass();

                debugInfo += "." + getMethod + "()";
            }

            // Property name
            String property = tokenizer.nextToken();
            // Check if the target object will accept the settings
            if (target instanceof ReflectionConfigurable && !((ReflectionConfigurable)target).acceptConfig(property, val)) {
                return;
            }

            // Find setter method
            Method setterMethod = findSetterMethod(targetClass, property);

            // Get the first parameter type. This assumes that there is only one
            // parameter.
            if (setterMethod == null) {
                throw new IllegalAccessException("Unable to find appropriate setter method signature for property: " + property);
            }
            Class<?> paramType = setterMethod.getParameterTypes()[0];

            // Set primitive type
            debugInfo += "." + setterMethod + "(" + paramType.getName() + ": " + val + ")";
            if (paramType.isPrimitive()) {
                if (paramType == Boolean.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Boolean.valueOf(val)
                    });
                } else if (paramType == Integer.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Integer.valueOf(val)
                    });
                } else if (paramType == Long.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Long.valueOf(val)
                    });
                } else if (paramType == Double.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Double.valueOf(val)
                    });
                } else if (paramType == Float.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Float.valueOf(val)
                    });
                } else if (paramType == Short.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Short.valueOf(val)
                    });
                } else if (paramType == Byte.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                        Byte.valueOf(val)
                    });
                } else if (paramType == Character.TYPE) {
                    setterMethod.invoke(target, new Object[] {
                            val.charAt(0)
                    });
                }
            } else {
                // Set String type
                if (paramType == String.class) {
                    setterMethod.invoke(target, new Object[] {
                        val
                    });

                    // For unknown object type, try to create an instance of the
                    // object using a String constructor
                } else {
                    Constructor c = paramType.getConstructor(new Class[] {
                        String.class
                    });
                    Object paramObject = c.newInstance(new Object[] {
                        val
                    });

                    setterMethod.invoke(target, new Object[] {
                        paramObject
                    });
                }
            }
            LOG.debug(debugInfo);

        } catch (Exception e) {
            LOG.warn(e.toString());
        }
    }

    public static void configureClass(Object obj, Properties props) {
        for (Iterator<Object> i = props.keySet().iterator(); i.hasNext();) {
            try {
                String key = (String)i.next();
                String val = props.getProperty(key);

                configureClass(obj, key, val);
            } catch (Throwable t) {
                // Let's catch any exception as this could be cause by the
                // foreign class
                t.printStackTrace();
            }
        }
    }

    public static Properties retrieveObjectProperties(Object obj) {
        Properties props = new Properties();
        try {
            props.putAll(retrieveClassProperties("", obj.getClass(), obj));
        } catch (Exception e) {
            LOG.warn(e.toString());
        }
        return props;
    }

    protected static Properties retrieveClassProperties(String prefix, Class targetClass, Object targetObject) {
        if (targetClass == null || targetObject == null) {
            return new Properties();
        } else {
            Properties props = new Properties();
            Method[] getterMethods = findAllGetterMethods(targetClass);
            for (int i = 0; i < getterMethods.length; i++) {
                try {
                    String propertyName = getPropertyName(getterMethods[i].getName());
                    Class retType = getterMethods[i].getReturnType();

                    // If primitive or string type, return it
                    if (retType.isPrimitive() || retType == String.class) {
                        // Check for an appropriate setter method to consider it
                        // as a property
                        if (findSetterMethod(targetClass, propertyName) != null) {
                            Object val = null;
                            try {
                                val = getterMethods[i].invoke(targetObject, null);
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                            props.setProperty(prefix + propertyName, val + "");
                        }
                    } else {
                        try {
                            Object val = getterMethods[i].invoke(targetObject, null);
                            if (val != null && val != targetObject) {
                                props.putAll(retrieveClassProperties(propertyName + ".", val.getClass(), val));
                            }
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                } catch (Throwable t) {
                    // Let's catch any exception, cause this could be cause by
                    // the foreign class
                    t.printStackTrace();
                }
            }
            return props;
        }
    }

    private static Method findSetterMethod(Class targetClass, String propertyName) {
        String methodName = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);

        Method[] methods = targetClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            if (methods[i].getName().equals(methodName) && isSetterMethod(methods[i])) {
                return methods[i];
            }
        }
        return null;
    }

    private static Method findGetterMethod(Class targetClass, String propertyName) {
        String methodName1 = "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
        String methodName2 = "is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);

        Method[] methods = targetClass.getMethods();
        for (int i = 0; i < methods.length; i++) {
            if ((methods[i].getName().equals(methodName1) || methods[i].getName().equals(methodName2)) && isGetterMethod(methods[i])) {
                return methods[i];
            }
        }
        return null;
    }

    private static Method[] findAllGetterMethods(Class targetClass) {
        List getterMethods = new ArrayList();
        Method[] methods = targetClass.getMethods();

        for (int i = 0; i < methods.length; i++) {
            if (isGetterMethod(methods[i])) {
                getterMethods.add(methods[i]);
            }
        }

        return (Method[])getterMethods.toArray(new Method[] {});
    }

    private static boolean isGetterMethod(Method method) {
        // Check method signature first
        // If 'get' method, must return a non-void value
        // If 'is' method, must return a boolean value
        // Both must have no parameters
        // Method must not belong to the Object class to prevent infinite loop
        return ((method.getName().startsWith("is") && method.getReturnType() == Boolean.TYPE) || (method.getName().startsWith("get") && method.getReturnType() != Void.TYPE))
               && (method.getParameterTypes().length == 0) && method.getDeclaringClass() != Object.class;
    }

    private static boolean isSetterMethod(Method method) {
        // Check method signature first
        if (method.getName().startsWith("set") && method.getReturnType() == Void.TYPE) {
            Class[] paramType = method.getParameterTypes();
            // Check that it can only accept one parameter
            if (paramType.length == 1) {
                // Check if parameter is a primitive or can accept a String
                // parameter
                if (paramType[0].isPrimitive() || paramType[0] == String.class) {
                    return true;
                } else {
                    // Check if object can accept a string as a constructor
                    try {
                        if (paramType[0].getConstructor(new Class[] {
                            String.class
                        }) != null) {
                            return true;
                        }
                    } catch (NoSuchMethodException e) {
                        // Do nothing
                    }
                }
            }
        }
        return false;
    }

    private static String getPropertyName(String methodName) {
        String name;
        if (methodName.startsWith("get")) {
            name = methodName.substring(3);
        } else if (methodName.startsWith("set")) {
            name = methodName.substring(3);
        } else if (methodName.startsWith("is")) {
            name = methodName.substring(2);
        } else {
            name = "";
        }

        return name.substring(0, 1).toLowerCase() + name.substring(1);
    }
}
