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

import org.jboss.as.pojo.logging.PojoLogger;
import org.jboss.as.pojo.descriptor.ValueConfig;
import org.jboss.as.server.deployment.reflect.ClassReflectionIndex;
import org.jboss.as.server.deployment.reflect.DeploymentReflectionIndex;
import org.jboss.common.beans.property.PropertiesValueResolver;
import org.jboss.logging.Logger;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;

/**
 * Configuration util.
 *
 * @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
 */
public class Configurator {
    /**
     * The log
     */
    protected static final Logger log = Logger.getLogger(Configurator.class);

    /**
     * No parameter types
     */
    public static final String[] NO_PARAMS_TYPES = new String[0];

    /**
     * Turn type into class.
     *
     * @param type the type
     * @return class
     */
    public static Class<?> toClass(Type type) {
        if (type instanceof Class) {
            return (Class) type;
        } else if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) type;
            return toClass(pt.getRawType());
        } else {
            throw PojoLogger.ROOT_LOGGER.unknownType(type);
        }
    }

    /**
     * Convert a value
     *
     * @param clazz             the class
     * @param value             the value
     * @param replaceProperties whether to replace system properties
     * @param trim              whether to trim string value
     * @return the value or null if there is no editor
     * @throws Throwable for any error
     */
    @SuppressWarnings("unchecked")
    public static Object convertValue(Class<?> clazz, Object value, boolean replaceProperties, boolean trim) throws Throwable {
        if (clazz == null)
            return value;
        if (value == null)
            return null;

        Class<?> valueClass = value.getClass();

        // If we have a string, trim and replace any system properties when requested
        if (valueClass == String.class) {
            String string = (String) value;
            if (trim)
                string = string.trim();
            if (replaceProperties)
                value = PropertiesValueResolver.replaceProperties(string);
        }

        if (clazz.isAssignableFrom(valueClass))
            return value;

        // First see if this is an Enum
        if (clazz.isEnum()) {
            Class<? extends Enum> eclazz = clazz.asSubclass(Enum.class);
            return Enum.valueOf(eclazz, value.toString());
        }

        // Next look for a property editor
        if (valueClass == String.class) {
            PropertyEditor editor = PropertyEditorManager.findEditor(clazz);
            if (editor != null) {
                editor.setAsText((String) value);
                return editor.getValue();
            }
        }

        // Try a static clazz.valueOf(value)
        try {
            Method method = clazz.getMethod("valueOf", valueClass);
            int modifiers = method.getModifiers();
            if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers)
                    && clazz.isAssignableFrom(method.getReturnType()))
                return method.invoke(null, value);
        } catch (Exception ignored) {
        }

        if (valueClass == String.class) {
            try {
                Constructor constructor = clazz.getConstructor(valueClass);
                if (Modifier.isPublic(constructor.getModifiers()))
                    return constructor.newInstance(value);
            } catch (Exception ignored) {
            }
        }

        return value;
    }

    /**
     * Get types from values.
     *
     * @param values the values
     * @return the values' types
     */
    public static String[] getTypes(ValueConfig[] values) {
        if (values == null || values.length == 0)
            return NO_PARAMS_TYPES;

        String[] types = new String[values.length];
        for (int i =0; i < types.length; i++)
            types[i] = values[i].getType();
        return types;
    }

    /**
     * Find method info
     *
     * @param index      the deployment reflection index
     * @param classInfo  the class info
     * @param name       the method name
     * @param paramTypes the parameter types
     * @param isStatic   must the method be static
     * @param isPublic   must the method be public
     * @param strict     is strict about method modifiers
     * @return the method info
     * @throws IllegalArgumentException when no such method
     */
    @SuppressWarnings("unchecked")
    public static Method findMethod(DeploymentReflectionIndex index, Class classInfo, String name, String[] paramTypes, boolean isStatic, boolean isPublic, boolean strict) throws IllegalArgumentException {
        if (name == null)
            throw PojoLogger.ROOT_LOGGER.nullName();

        if (classInfo == null)
            throw PojoLogger.ROOT_LOGGER.nullClassInfo();

        if (paramTypes == null)
            paramTypes = NO_PARAMS_TYPES;

        Class current = classInfo;
        while (current != null) {
            ClassReflectionIndex cri = index.getClassIndex(classInfo);
            Method result = locateMethod(cri, name, paramTypes, isStatic, isPublic, strict);
            if (result != null)
                return result;
            current = current.getSuperclass();
        }
        throw PojoLogger.ROOT_LOGGER.methodNotFound(name, Arrays.toString(paramTypes), classInfo.getName());
    }

    /**
     * Find method info
     *
     * @param classInfo  the class info
     * @param name       the method name
     * @param paramTypes the parameter types
     * @param isStatic   must the method be static
     * @param isPublic   must the method be public
     * @param strict     is strict about method modifiers
     * @return the method info or null if not found
     */
    @SuppressWarnings("unchecked")
    private static Method locateMethod(ClassReflectionIndex classInfo, String name, String[] paramTypes, boolean isStatic, boolean isPublic, boolean strict) {
        Collection<Method> methods = classInfo.getMethods();
        if (methods != null) {
            for (Method method : methods) {
                if (name.equals(method.getName()) &&
                        equals(paramTypes, method.getParameterTypes()) &&
                        (strict == false || (Modifier.isStatic(method.getModifiers()) == isStatic && Modifier.isPublic(method.getModifiers()) == isPublic)))
                    return method;
            }
        }
        return null;
    }

    /**
     * Test whether type names are equal to type infos
     *
     * @param typeNames the type names
     * @param typeInfos the type infos
     * @return true when they are equal
     */
    public static boolean equals(String[] typeNames, Class<?>[] typeInfos) {
        if (simpleCheck(typeNames, typeInfos) == false)
            return false;

        for (int i = 0; i < typeNames.length; ++i) {
            if (typeNames[i] != null && typeNames[i].equals(typeInfos[i].getName()) == false)
                return false;
        }
        return true;
    }

    /**
     * A simple null and length check.
     *
     * @param typeNames the type names
     * @param typeInfos the type infos
     * @return false if either argument is null or lengths differ, else true
     */
    protected static boolean simpleCheck(String[] typeNames, Class<?>[] typeInfos) {
        return typeNames != null && typeInfos != null && typeNames.length == typeInfos.length;
    }
}
