/*
 * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com
 *
 * Licensed 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 jsyntaxpane.util;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Reflection Utility methods
 * @author Ayman Al-Sairafi
 */
public class ReflectUtils {

    /**
     * Adds all methods (from Class.getMethodCalls) to the list
     * @param aClass
     * @param list
     * @return number of methods added
     */
    public static int addMethods(Class aClass, List<Member> list) {
        Method[] methods = aClass.getMethods();
        for (Method m : methods) {
            list.add(m);
        }
        return methods.length;
    }

    /**
     * Adds all static methods (from Class.getMethodCalls) to the list
     * @param aClass
     * @param list
     * @return number of methods added
     */
    public static int addStaticMethods(Class aClass, List<Member> list) {
        Method[] methods = aClass.getMethods();
        for (Method m : methods) {
            if (Modifier.isStatic(m.getModifiers())) {
                list.add(m);
            }
        }
        return methods.length;
    }

    /**
     * Adds all static Fields (from Class.getFields) to the list
     * @param aClass
     * @param list
     * @return number of fields added
     */
    public static int addStaticFields(Class aClass, List<Member> list) {
        Field[] fields = aClass.getFields();
        for (Field f : fields) {
            if (Modifier.isStatic(f.getModifiers())) {
                list.add(f);
            }
        }
        return fields.length;
    }

    /**
     * Adds all Fields (from Class.getFields) to the list
     * @param aClass
     * @param list
     * @return number of fields added
     */
    public static int addFields(Class aClass, List<Member> list) {
        Field[] fields = aClass.getFields();
        for (Field f : fields) {
            list.add(f);
        }
        return fields.length;
    }

    /**
     * Adds all Constructor (from Class.getConstructorCalls) to the list
     * @param aClass
     * @param list
     * @return number of constructors added
     */
    public static int addConstrcutors(Class aClass, List<Member> list) {
        Constructor[] constructors = aClass.getConstructors();
        for (Constructor c : constructors) {
            list.add(c);
        }
        return constructors.length;
    }

    /**
     * Convert the constructor to a Java Code String
     * (arguments are replaced by the simple types)
     * @param c Constructor
     * @return
     */
    public static String getJavaCallString(Constructor c) {
        StringBuilder call = new StringBuilder();
        call.append(c.getDeclaringClass().getSimpleName());
        addParamsString(call, c.getParameterTypes());
        return call.toString();
    }

    /**
     * Convert the Method to a Java Code String
     * (arguments are replaced by the simple types)
     * @param method Method
     * @return
     */
    public static String getJavaCallString(Method method) {
        StringBuilder call = new StringBuilder();
        call.append(method.getName());
        addParamsString(call, method.getParameterTypes());
        return call.toString();
    }

    /**
     * Adds the class SimpleNames, comma sepearated and surrounded by paranthesis to the
     * call StringBuffer
     * @param call
     * @param params
     * @return
     */
    public static StringBuilder addParamsString(StringBuilder call, Class[] params) {
        call.append("(");
        boolean firstArg = true;
        for (Class arg : params) {
            if (firstArg) {
                firstArg = false;
            } else {
                call.append(", ");
            }
            call.append(arg.getSimpleName());
        }
        call.append(")");
        return call;
    }

    /**
     * Gets a String array of all method calls for the given class
     * @param aClass
     * @return
     */
    public static String[] getMethodCalls(Class aClass) {
        String[] methods = new String[aClass.getMethods().length];
        int i = 0;
        for (Method method : aClass.getMethods()) {
            methods[i++] = getJavaCallString(method);
        }
        return methods;
    }

    /**
     * Gets an array of all Constructor calls for the given class
     * @param aClass
     * @return
     */
    public static String[] getConstructorCalls(Class aClass) {
        Constructor[] constructors = aClass.getConstructors();
        String[] cons = new String[constructors.length];
        int i = 0;
        for (Constructor c : constructors) {
            cons[i++] = getJavaCallString(c);
        }
        return cons;
    }

    /**
     * Return a paranthesis enclosed, comma sepearated String of all
     * SimpleClass names in params.
     * @param params
     * @return
     */
    public static String getParamsString(Class[] params) {
        StringBuilder sb = new StringBuilder();
        addParamsString(sb, params);
        return sb.toString();
    }

    /**
     * Scans all classes accessible from the context class loader which belong to the given package and subpackages.
     *
     * @param packageName The base package
     * @return The classes
     * @throws ClassNotFoundException
     * @throws IOException
     */
    private static Class[] getClasses(String packageName)
            throws ClassNotFoundException, IOException {
//        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        assert classLoader != null;
        String path = packageName.replace('.', '/');
        Enumeration<URL> resources = classLoader.getResources(path);
        List<File> dirs = new ArrayList<File>();
        while (resources.hasMoreElements()) {
            URL resource = resources.nextElement();
            dirs.add(new File(resource.getFile()));
        }
        ArrayList<Class> classes = new ArrayList<Class>();
        for (File directory : dirs) {
            classes.addAll(findClasses(directory, packageName));
        }
        return classes.toArray(new Class[classes.size()]);
    }

    /**
     * Recursive method used to find all classes in a given directory and subdirs.
     *
     * @param directory   The base directory
     * @param packageName The package name for classes found inside the base directory
     * @return The classes
     * @throws ClassNotFoundException
     */
    private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<Class>();
        if (!directory.exists()) {
            return classes;
        }
        File[] files = directory.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                assert !file.getName().contains(".");
                classes.addAll(findClasses(file, packageName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }

    /**
     * Attempt to find the given className within any of the packages.
     * If the class is not found, then null is returned
     * @param className Fully or partially qualified classname within any of the packages
     * @param packages List of packages for search
     * @return CLass object or null if not found.
     */
    public static Class findClass(String className, List<String> packages) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException ex) {
        } catch (NoClassDefFoundError ex) {
        }
        for (String pack : packages) {
            try {
                return Class.forName(pack + "." + className);
            } catch (ClassNotFoundException ex) {
            } catch (NoClassDefFoundError ex) {
            }
        }
        return null;
    }

    /**
     * Find a setter method for the give object's property and try to call it.
     * No exceptions are thrown. You typically call this method because either
     * you are sure no exceptions will be thrown, or to silently ignore
     * any that may be thrown.
     * This will also find a setter that accepts an interface that the value
     * implements.
     * <b>This is still not very effcient and should only be called if
     * performance is not of an issue.</b>
     * You can check the return value to see if the call was seuccessful or
     * not.
     * @param obj Object to receive the call
     * @param property property name (without set. First letter will be
     * capitalized)
     * @param value Value of the property.
     * @return
     */
    public static boolean callSetter(Object obj, String property, Object value) {
        String key = String.format("%s.%s(%s)", obj.getClass().getName(),
                property, value.getClass().getName());
        Method m = null;
        boolean result = false;
        if(!SETTERS_MAP.containsKey(key)) {
            m = findMethod(obj, property, value);
            SETTERS_MAP.put(key, m);
        } else {
            m = SETTERS_MAP.get(key);
        }
        if(m != null) {
            try {
                m.invoke(obj, value);
                result = true;
            } catch (IllegalAccessException ex) {
                Logger.getLogger(ReflectUtils.class.getName()).log(Level.SEVERE, null, ex);
            } catch (IllegalArgumentException ex) {
                Logger.getLogger(ReflectUtils.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InvocationTargetException ex) {
                Logger.getLogger(ReflectUtils.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return result;
    }

    private static synchronized Method findMethod(Object obj,
            String property, Object value) {
        Method m = null;
        Class<?> theClass = obj.getClass();
        String setter = String.format("set%C%s",
                property.charAt(0), property.substring(1));
        Class paramType = value.getClass();
        while (paramType != null) {
            try {
                m = theClass.getMethod(setter, paramType);
                return m;
            } catch (NoSuchMethodException ex) {
                // try on the interfaces of this class
                for (Class iface : paramType.getInterfaces()) {
                    try {
                        m = theClass.getMethod(setter, iface);
                        return m;
                    } catch (NoSuchMethodException ex1) {
                    }
                }
                paramType = paramType.getSuperclass();
            }
        }
        return m;
    }
    public static final List<String> DEFAULT_PACKAGES = new ArrayList<String>(3);

    static {
        DEFAULT_PACKAGES.add("java.lang");
        DEFAULT_PACKAGES.add("java.util");
        DEFAULT_PACKAGES.add("jsyntaxpane");
    }
    /**
     * To speed up find setter methods, this map will be used.
     * The Key String will be of the format objectClass.property(valueclass)
     * Where:
     * objectClass = obj.getClass().getName
     * property = property (as passed in to callSetter), before set is appended
     * valueCLass = value.getClass().getName()
     * The Method will be either the method, or null if a search was not and no
     * method is found.
     */
    private static HashMap<String, Method> SETTERS_MAP = new HashMap<String, Method>();
}
