/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.jboss.as.connector.util;

import static org.wildfly.common.Assert.checkNotNullParam;

import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;

import org.jboss.as.connector.logging.ConnectorLogger;
import org.wildfly.security.manager.WildFlySecurityManager;

/**
 * Injection utility which can inject values into objects. This file is a copy
 * of the <code>com.github.fungal.api.util.Injection</code> class.
 *
 * @author <a href="mailto:jesper.pedersen@jboss.org">Jesper Pedersen</a>
 */
public class Injection {
    /**
     * Constructor
     */
    public Injection() {
    }

    /**
     * Inject a value into an object property
     *
     * @param object        The object
     * @param propertyName  The property name
     * @param propertyValue The property value
     * @throws NoSuchMethodException     If the property method cannot be found
     * @throws IllegalAccessException    If the property method cannot be accessed
     * @throws InvocationTargetException If the property method cannot be executed
     */
    @SuppressWarnings("unchecked")
    public void inject(Object object, String propertyName, Object propertyValue)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        inject(object, propertyName, propertyValue, null, false);
    }

    /**
     * Inject a value into an object property
     *
     * @param object        The object
     * @param propertyName  The property name
     * @param propertyValue The property value
     * @param propertyType  The property type as a fully quilified class name
     * @throws NoSuchMethodException     If the property method cannot be found
     * @throws IllegalAccessException    If the property method cannot be accessed
     * @throws InvocationTargetException If the property method cannot be executed
     */
    @SuppressWarnings("unchecked")
    public void inject(Object object, String propertyName, Object propertyValue, String propertyType)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        inject(object, propertyName, propertyValue, propertyType, false);
    }

    /**
     * Inject a value into an object property
     *
     * @param object        The object
     * @param propertyName  The property name
     * @param propertyValue The property value
     * @param propertyType  The property type as a fully quilified class name
     * @param includeFields Should fields be included for injection if a method can't be found
     * @throws NoSuchMethodException     If the property method cannot be found
     * @throws IllegalAccessException    If the property method cannot be accessed
     * @throws InvocationTargetException If the property method cannot be executed
     */
    @SuppressWarnings("unchecked")
    public void inject(Object object,
                       String propertyName, Object propertyValue, String propertyType,
                       boolean includeFields)
            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        checkNotNullParam("object", object);

        if (propertyName == null || propertyName.trim().equals(""))
            throw ConnectorLogger.ROOT_LOGGER.undefinedVar("PropertyName");

        String methodName = "set" + propertyName.substring(0, 1).toUpperCase(Locale.US);
        if (propertyName.length() > 1) {
            methodName += propertyName.substring(1);
        }

        Method method = findMethod(object.getClass(), methodName, propertyType);

        if (method != null) {
            Class<?> parameterClass = method.getParameterTypes()[0];
            Object parameterValue = null;
            try {
                parameterValue = getValue(propertyName, parameterClass, propertyValue,
                        WildFlySecurityManager.getClassLoaderPrivileged(object.getClass()));
            } catch (Throwable t) {
                throw new InvocationTargetException(t, t.getMessage());
            }

            if (!parameterClass.isPrimitive() || parameterValue != null)
                method.invoke(object, new Object[]{parameterValue});
        } else {
            if (!includeFields)
                throw ConnectorLogger.ROOT_LOGGER.noSuchMethod(methodName);

            // Ok, we didn't find a method - assume field
            Field field = findField(object.getClass(), propertyName, propertyType);

            if (field != null) {
                Class<?> fieldClass = field.getType();
                Object fieldValue = null;
                try {
                    fieldValue = getValue(propertyName, fieldClass, propertyValue,
                            WildFlySecurityManager.getClassLoaderPrivileged(object.getClass()));
                } catch (Throwable t) {
                    throw new InvocationTargetException(t, t.getMessage());
                }

                field.set(object, fieldValue);
            } else {
                throw ConnectorLogger.ROOT_LOGGER.noSuchField(propertyName);
            }
        }
    }

    /**
     * Compare the type of a class with the actual value
     *
     * @param classType    The class type
     * @param propertyType The property type
     * @return True if they match, or if there is a primitive mapping
     */
    private boolean argumentMatches(String classType, String propertyType) {
        return (classType.equals(propertyType))
                || (classType.equals("java.lang.Byte") && propertyType.equals("byte"))
                || (classType.equals("java.lang.Short") && propertyType.equals("short"))
                || (classType.equals("java.lang.Integer") && propertyType.equals("int"))
                || (classType.equals("java.lang.Long") && propertyType.equals("long"))
                || (classType.equals("java.lang.Float") && propertyType.equals("float"))
                || (classType.equals("java.lang.Double") && propertyType.equals("double"))
                || (classType.equals("java.lang.Boolean") && propertyType.equals("boolean"))
                || (classType.equals("java.lang.Character") && propertyType.equals("char"));
    }

    /**
     * Find a method
     *
     * @param clz          The class
     * @param methodName   The method name
     * @param propertyType The property type; can be <code>null</code>
     * @return The method; <code>null</code> if not found
     */
    protected Method findMethod(Class<?> clz, String methodName, String propertyType) {
        while (!clz.equals(Object.class)) {
            List<Method> hits = null;
            Method[] methods = SecurityActions.getDeclaredMethods(clz);
            for (int i = 0; i < methods.length; i++) {
                final Method method = methods[i];
                if (methodName.equals(method.getName())
                        && method.getParameterCount() == 1
                        && (propertyType == null || argumentMatches(propertyType, method.getParameterTypes()[0].getName()))) {

                    if (hits == null)
                        hits = new ArrayList<Method>(1);

                    SecurityActions.setAccessible(method);
                    hits.add(method);
                }
            }

            if (hits != null) {
                if (hits.size() == 1) {
                    return hits.get(0);
                } else {
                    Collections.sort(hits, new MethodSorter());
                    if (propertyType != null) {
                        for (Method m : hits) {
                            if (propertyType.equals(m.getParameterTypes()[0].getName()))
                                return m;
                        }
                    }

                    return hits.get(0);
                }
            }

            clz = clz.getSuperclass();
        }

        return null;
    }

    /**
     * Find a field
     *
     * @param clz       The class
     * @param fieldName The field name
     * @param fieldType The field type; can be <code>null</code>
     * @return The field; <code>null</code> if not found
     */
    protected Field findField(Class<?> clz, String fieldName, String fieldType) {
        while (!clz.equals(Object.class)) {
            List<Field> hits = null;
            Field[] fields = SecurityActions.getDeclaredFields(clz);
            for (int i = 0; i < fields.length; i++) {
                final Field field = fields[i];
                if (fieldName.equals(field.getName())
                        && (fieldType == null || argumentMatches(fieldType, field.getType().getName()))) {

                    if (hits == null)
                        hits = new ArrayList<Field>(1);

                    SecurityActions.setAccessible(field);
                    hits.add(field);
                }
            }

            if (hits != null) {
                if (hits.size() == 1) {
                    return hits.get(0);
                } else {
                    Collections.sort(hits, new FieldSorter());
                    if (fieldType != null) {
                        for (Field f : hits) {
                            if (fieldType.equals(f.getType().getName()))
                                return f;
                        }
                    }

                    return hits.get(0);
                }
            }

            clz = clz.getSuperclass();
        }

        return null;
    }

    /**
     * Get the value
     *
     * @param name The value name
     * @param clz  The value class
     * @param v    The value
     * @param cl   The class loader
     * @return The substituted value
     * @throws Exception Thrown in case of an error
     */
    protected Object getValue(String name, Class<?> clz, Object v, ClassLoader cl) throws Exception {
        if (v instanceof String) {
            String substituredValue = getSubstitutionValue((String) v);

            if (clz.equals(String.class)) {
                v = substituredValue;
            } else if (clz.equals(byte.class) || clz.equals(Byte.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Byte.valueOf(substituredValue);
            } else if (clz.equals(short.class) || clz.equals(Short.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Short.valueOf(substituredValue);
            } else if (clz.equals(int.class) || clz.equals(Integer.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Integer.valueOf(substituredValue);
            } else if (clz.equals(long.class) || clz.equals(Long.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Long.valueOf(substituredValue);
            } else if (clz.equals(float.class) || clz.equals(Float.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Float.valueOf(substituredValue);
            } else if (clz.equals(double.class) || clz.equals(Double.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Double.valueOf(substituredValue);
            } else if (clz.equals(boolean.class) || clz.equals(Boolean.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Boolean.valueOf(substituredValue);
            } else if (clz.equals(char.class) || clz.equals(Character.class)) {
                if (substituredValue != null && !substituredValue.trim().equals(""))
                    v = Character.valueOf(substituredValue.charAt(0));
            } else if (clz.equals(InetAddress.class)) {
                v = InetAddress.getByName(substituredValue);
            } else if (clz.equals(Class.class)) {
                v = Class.forName(substituredValue, true, cl);
            } else if (clz.equals(Properties.class)) {
                Properties prop = new Properties();

                StringTokenizer st = new StringTokenizer(substituredValue, " ,");
                while (st.hasMoreTokens()) {
                    String token = st.nextToken();
                    String key = "";
                    String value = "";

                    int index = token.indexOf("=");
                    if (index != -1) {
                        key = token.substring(0, index);

                        if (token.length() > index + 1)
                            value = token.substring(index + 1);
                    } else {
                        key = token;
                    }

                    if (!"".equals(key))
                        prop.setProperty(key, value);
                }

                v = prop;
            } else {
                try {
                    Constructor<?> constructor = SecurityActions.getConstructor(clz, String.class);
                    v = constructor.newInstance(substituredValue);
                } catch (Throwable t) {
                    // Try static String valueOf method
                    try {
                        Method valueOf = SecurityActions.getMethod(clz, "valueOf", String.class);
                        v = valueOf.invoke((Object) null, substituredValue);
                    } catch (Throwable inner) {
                        throw ConnectorLogger.ROOT_LOGGER.noPropertyResolution(name);
                    }
                }
            }
        }

        return v;
    }

    /**
     * System property substitution
     *
     * @param input The input string
     * @return The output
     */
    protected String getSubstitutionValue(String input) {
        if (input == null || input.trim().equals(""))
            return input;

        while (input.indexOf("${") != -1) {
            int from = input.indexOf("${");
            int to = input.indexOf("}");
            int dv = input.indexOf(":", from + 2);

            if (dv != -1 && dv > to) {
                dv = -1;
            }

            String systemProperty = "";
            String defaultValue = "";
            if (dv == -1) {
                String s = input.substring(from + 2, to);
                if ("/".equals(s)) {
                    systemProperty = File.separator;
                } else if (":".equals(s)) {
                    systemProperty = File.pathSeparator;
                } else {
                    systemProperty = WildFlySecurityManager.getSystemPropertiesPrivileged().getProperty(s);
                }
            } else {
                systemProperty = WildFlySecurityManager.getSystemPropertiesPrivileged().getProperty(input.substring(from + 2, dv));
                defaultValue = input.substring(dv + 1, to);
            }
            String prefix = "";
            String postfix = "";

            if (from != 0) {
                prefix = input.substring(0, from);
            }

            if (to + 1 < input.length() - 1) {
                postfix = input.substring(to + 1);
            }

            if (systemProperty != null && !systemProperty.trim().equals("")) {
                input = prefix + systemProperty + postfix;
            } else if (!defaultValue.trim().equals("")) {
                input = prefix + defaultValue + postfix;
            } else {
                input = prefix + postfix;
            }
        }
        return input;
    }

    /**
     * Method sorter
     */
    static class MethodSorter implements Comparator<Method>, Serializable {
        private static final long serialVersionUID = 1L;

        /**
         * Constructor
         */
        MethodSorter() {
        }

        /**
         * {@inheritDoc}
         */
        public int compare(Method o1, Method o2) {
            int m1 = o1.getModifiers();
            int m2 = o2.getModifiers();

            if (Modifier.isPublic(m1))
                return -1;

            if (Modifier.isPublic(m2))
                return 1;

            if (Modifier.isProtected(m1))
                return -1;

            if (Modifier.isProtected(m2))
                return 1;

            if (Modifier.isPrivate(m1))
                return -1;

            if (Modifier.isPrivate(m2))
                return 1;

            return 0;
        }

        /**
         * {@inheritDoc}
         */
        public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || !(o instanceof MethodSorter))
                return false;

            return true;
        }

        /**
         * {@inheritDoc}
         */
        public int hashCode() {
            return 42;
        }
    }

    /**
     * Field sorter
     */
    static class FieldSorter implements Comparator<Field>, Serializable {
        private static final long serialVersionUID = 1L;

        /**
         * Constructor
         */
        FieldSorter() {
        }

        /**
         * {@inheritDoc}
         */
        public int compare(Field o1, Field o2) {
            int m1 = o1.getModifiers();
            int m2 = o2.getModifiers();

            if (Modifier.isPublic(m1))
                return -1;

            if (Modifier.isPublic(m2))
                return 1;

            if (Modifier.isProtected(m1))
                return -1;

            if (Modifier.isProtected(m2))
                return 1;

            if (Modifier.isPrivate(m1))
                return -1;

            if (Modifier.isPrivate(m2))
                return 1;

            return 0;
        }

        /**
         * {@inheritDoc}
         */
        public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || !(o instanceof FieldSorter))
                return false;

            return true;
        }

        /**
         * {@inheritDoc}
         */
        public int hashCode() {
            return 42;
        }
    }
}

