/*
* Copyright (c) 2010 Red Hat, Inc.
*
* 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 org.ovirt.engine.api.common.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.ovirt.engine.api.common.resource.AbstractUpdatableResource;
import org.ovirt.engine.api.model.BaseResource;

public class ReflectionHelper {

    private static final String GET_ROOT = "get";
    private static final String SET_ROOT = "set";
    private static final String IS_SET_ROOT = "isSet";

    private ReflectionHelper() {}

    public static <R extends BaseResource> R newModel(Class<R> clz) {
        R ret = null;
        try {
            ret = clz.newInstance();
        } catch (Exception e) {
        }
        return ret;
    }

    @SuppressWarnings("unchecked")
    public static <R extends BaseResource> R newModel(AbstractUpdatableResource<R> resource) {
        return newModel((Class<R>)((ParameterizedType)resource.getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
    }

    public static <R extends BaseResource> R assignChildModel(BaseResource parent, Class<R> childType) {
        Method setter = getMethod(parent,
                                  SET_ROOT + capitalize(childType.getSimpleName().toLowerCase()));

        R child = newModel(childType);
        try {
            setter.invoke(parent, child);
        } catch (Exception e) {
            // InvocationTargetException etc. should never occur
            // as this is a simple setter
        }

        return child;
    }

    public static String capitalize(String s) {
        return Character.isLowerCase(s.charAt(0))
               ? s.substring(0, 1).toUpperCase() + s.substring(1)
               : s;
    }

    public static boolean isSet(Object o, String name) {
        if(o != null){
            Method m = getMethod(o, IS_SET_ROOT + name);
            Object ret = invoke(o, m);
            return ret != null && ret instanceof Boolean && ((Boolean)ret).booleanValue();
        }
        return false;
    }

    public static boolean different(Object lhs, Object rhs, String name) {
        Method lhsm = getMethod(lhs, GET_ROOT + name);
        Method rhsm = getMethod(rhs, GET_ROOT + name);
        Object lhsr = lhsm != null ? invoke(lhs, lhsm) : null;
        Object rhsr = rhsm != null ? invoke(rhs, rhsm) : null;
        return !(lhsr == null || lhsr.equals(rhsr));
    }

    public static Object invoke(Object o, Method m) {
        Object ret = null;
        try {
            ret = m.invoke(o);
        } catch (Exception e) {
            // InvocationTargetException etc. should never occur
            // as this is a simple getter
        }
        return ret;
    }

    public static boolean set(Object o, String field, String value) {
        boolean success = false;
        String name = SET_ROOT + capitalize(field);
        for (Method m : o.getClass().getMethods()) {
            if (m.getName().equals(name) && isPrimitive(m)) {
                Object arg = isString(m)
                             ? value
                             : isBoolean(m)
                               ? Boolean.parseBoolean(value)
                               : isInteger(m)
                                 ? Integer.parseInt(value)
                                 : null;
                if (arg != null) {
                    try {
                        m.invoke(o, arg);
                        success = true;
                    } catch (Exception e) {
                        // InvocationTargetException etc. should never occur
                        // as this is a simple getter
                    }
                }
                break;
            }
        }
        return success;
    }

    public static Object get(Object o, String field) {
        Object ret = null;
        String name = GET_ROOT + capitalize(field);
        for (Method m : o.getClass().getMethods()) {
            if (m.getName().equals(name)) {
                try {
                    ret = m.invoke(o);
                } catch (Exception e) {
                    // InvocationTargetException etc. should never occur
                    // as this is a simple getter
                }
                break;
            }
        }
        return ret;
    }

    private static boolean isPrimitive(Method m) {
        Class<?>[] params = m.getParameterTypes();
        return params.length == 1
               && (String.class.equals(params[0])
                   || Boolean.TYPE.equals(params[0])
                   || Integer.TYPE.equals(params[0]));
    }

    private static boolean isString(Method m) {
        return String.class.equals(m.getParameterTypes()[0]);
    }

    private static boolean isInteger(Method m) {
        return Integer.TYPE.equals(m.getParameterTypes()[0]);
    }

    private static boolean isBoolean(Method m) {
        return Boolean.TYPE.equals(m.getParameterTypes()[0]);
    }

    private static Method getMethod(Object o, String name) {
        Method ret = null;
        for (Method m : o.getClass().getMethods()) {
            if (m.getName().equals(name)) {
                ret = m;
                break;
            }
        }
        return ret;
    }
    /**
     * Locate all directories in given package
     *
     * @param path
     * @return Map<URL, File>
     * @throws IOException
     */
    public static List<URL> getDirectories(String path) throws IOException {
         ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
         assert classLoader != null;
         Enumeration<URL> resources = classLoader.getResources(path);
         List<URL> dirs = new ArrayList<URL>();
         while (resources.hasMoreElements()) {
             dirs.add(resources.nextElement());
         }
         return dirs;
    }
    /**
     * Locates all classes in given package
     *
     * @param  packageName
     * @return array of the classes
     * @throws ClassNotFoundException
     * @throws IOException
     */
    public static List<Class<?>> getClasses(String packageName) throws ClassNotFoundException,
            IOException {
        String path = packageName.replace('.', '/');
        List<URL> dirs = getDirectories(path);
        ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
        ClassLoader loader = URLClassLoader.newInstance(dirs.toArray(new URL[0]),
                                                        Thread.currentThread().getContextClassLoader());

        for (URL directory : dirs) {
            String resource = URLDecoder.decode(directory.getPath(), "UTF-8").replace("/"+path+"/", "");
            if (resource.endsWith(".jar")) {
                classes.addAll(getClassNamesInJarPackage(loader, resource, packageName));
            } else {
                classes.addAll(getClassNamesInPackage(new File(URLDecoder.decode(directory.getFile(), "UTF-8")), packageName));
            }
        }
        return classes;
    }
    /**
     * Locates all classes in given package
     *
     * @param  directory
     * @param  packageName
     * @return List of classes
     * @throws ClassNotFoundException
     */
    private static List<Class<?>> getClassNamesInPackage(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(getClassNamesInPackage(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;
    }

    /**
     * Locates all classes in given jar package
     *
     * @param  jarName
     * @param  url
     * @param  packageName
     * @return List of classes
     * @throws MalformedURLException
     */
    static List<Class<?>> getClassNamesInJarPackage(ClassLoader loader, String jarName, String packageName) throws MalformedURLException {
        ArrayList<Class<?>> classes = new ArrayList<Class<?>>();

        packageName = packageName.replaceAll("\\.", "/");

        JarInputStream jarFileInputStream = null;
        try {
            jarFileInputStream = new JarInputStream(new FileInputStream(jarName));
            JarEntry jarEntry;
            while (true) {
                jarEntry = jarFileInputStream.getNextJarEntry();
                if (jarEntry == null) break;
                if ((jarEntry.getName().startsWith(packageName)) && (jarEntry.getName().endsWith(".class"))) {
                    classes.add(loader.loadClass(jarEntry.getName().replaceAll("/", "\\.").replace(".class", "")));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if(jarFileInputStream != null) {
                    jarFileInputStream.close();
                }
            } catch (IOException e) {
                //ignore
            }
        }
        return classes;
    }
}

