/*******************************************************************************
 * Copyright (c)2015 IBM Corporation. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     02/19/2015 - Rick Curtis
 *       - 458877 : Add national character support
 *     03/06/2015-2.7.0 Dalia Abo Sheasha
 *       - 461607: PropertiesUtils does not process methods with String parameters correctly.
 *     05/06/2015-2.7.0 Rick Curtis
 *       - 466626: Fix bug in getMethods() when Java 2 security is enabled.
 ******************************************************************************/
package org.eclipse.persistence.config;

import java.lang.reflect.Method;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedGetMethods;
import org.eclipse.persistence.internal.security.PrivilegedMethodInvoker;

/**
 * A static utility class that handles parsing a String
 * "key=value,key1=value1...." and calls an appropriate set[key]([value]) method
 * on the provided instance.
 */
public class PropertiesUtils {
    /**
     * Attempts to parse and then set the provided kvs String into the
     * appropriate set method on the provided instance.
     *
     * Note: Keys and values cannot contain '=' or ','
     *
     * @param instance
     *            An JavaBean instance
     * @param propertyName
     *            The configuration name that is being set into instance. This
     *            is only used for informational purposes.
     * @param kvs
     *            A String of key=value comma delimited ie: k1=v1,k2=v2,...
     * @throws org.eclipse.persistence.exceptions.ConversionException
     *             if unable to process the provided kvs
     */
    public static void set(Object instance, String propertyName, String kvs) {
        if (instance == null || kvs == null || kvs.trim().isEmpty()) {
            return;
        }
        String[] keyValues = kvs.split(",");
        for (String kv : keyValues) {
            processKeyValue(instance, propertyName, kv);
        }
    }

    private static void processKeyValue(Object instance, String propertyName, String kv) {
        String[] t = kv.split("=");

        if (t.length != 2) {
            // We must have a single key and a single value.
            throw ConversionException.couldNotTranslatePropertiesIntoObject(instance, propertyName, kv, null);
        }
        String methodName = "set" + t[0].trim();
        // Stringified value to be set. Need to figure out it's type
        String value = t[1].trim();
        List<Method> methods = getMethodsMatchingName(instance, methodName);
        MethodMatch match = null;
        try {
            match = getMethodMatchingParameter(methods, value);
        } catch (Exception e) {
            throw ConversionException.couldNotTranslatePropertiesIntoObject(instance, propertyName, kv, e);
        }
        if (match != null) {
            Method method = match.getMethod();
            Object parsedValue = match.getParsedValue();
            try {
                invokeMethod(method, instance, parsedValue);
            } catch (Exception e) {
                throw ConversionException.couldNotTranslatePropertiesIntoObject(instance, propertyName, kv, e);
            }
        } else {
            throw ConversionException.couldNotTranslatePropertiesIntoObject(instance, propertyName, kv, null);
        }
    }

    private static MethodMatch getMethodMatchingParameter(List<Method> methods, String parameter) throws Exception {
        Object parsedParameterValue = null;
        Method res = null;
        for (Method method : methods) {
            Class<?>[] params = method.getParameterTypes();
            if (params.length == 1) {
                Class<?> param = params[0];
                if (param == String.class) {
                    parsedParameterValue = parameter;
                } else if (param == Integer.class || param == int.class) {
                    parsedParameterValue = Integer.parseInt(parameter);
                } else if (param == Boolean.class || param == boolean.class) {
                    // Don't use Boolean.parseBoolean(..) as it is to liberal
                    // with it's interpretation of false values
                    String lower = parameter.toLowerCase();
                    if ("true".equals(lower)) {
                        parsedParameterValue = Boolean.TRUE;
                    } else if ("false".equals(lower)) {
                        parsedParameterValue = Boolean.FALSE;
                    }
                } else if (param == Long.class || param == long.class) {
                    parsedParameterValue = Long.parseLong(parameter);
                } else if (param == Float.class || param == float.class) {
                    parsedParameterValue = Float.parseFloat(parameter);
                } else if (param == Double.class || param == double.class) {
                    parsedParameterValue = Double.parseDouble(parameter);
                }
            }
            if (parsedParameterValue != null) {
                return new MethodMatch(method, parsedParameterValue);
            }
        }// end for
        return null;
    }

    private static List<Method> getMethodsMatchingName(Object instance, String methodName) {
        List<Method> methods = new ArrayList<>();
        for (Method method : getMethods(instance.getClass())) {
            if (method.getName().equalsIgnoreCase(methodName)) {
                methods.add(method);
            }
        }
        return methods;
    }

    private static Method[] getMethods(Class<?> cls) {
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            return AccessController.doPrivileged(new PrivilegedGetMethods(cls));
        }
        return PrivilegedAccessHelper.getMethods(cls);
    }

    private static Object invokeMethod(Method method, Object instance, Object... params) throws Exception {
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            return AccessController.doPrivileged(new PrivilegedMethodInvoker(method, instance, params));
        }
        return method.invoke(instance, params);
    }

    private static class MethodMatch {
        final Method method;
        final Object parsedValue;

        public MethodMatch(Method m, Object p) {
            method = m;
            parsedValue = p;
        }

        public Method getMethod() {
            return method;
        }

        public Object getParsedValue() {
            return parsedValue;
        }
    }
}
