package org.apache.velocity.tools;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.velocity.util.ArrayIterator;
import org.apache.velocity.util.EnumerationIterator;

/**
 * Repository for common class and reflection methods.
 *
 * @author Nathan Bubna
 * @version $Id: ClassUtils.java 511959 2007-02-26 19:24:39Z nbubna $
 */
public class ClassUtils
{
    private ClassUtils() {}

    // shortcuts for readability...
    private static final ClassLoader getThreadContextLoader()
    {
        return Thread.currentThread().getContextClassLoader();
    }

    private static final ClassLoader getClassLoader()
    {
        return ClassUtils.class.getClassLoader();
    }

    private static final ClassLoader getCallerLoader(Object caller)
    {
        if (caller instanceof Class)
        {
            return ((Class)caller).getClassLoader();
        }
        else
        {
            return caller.getClass().getClassLoader();
        }
    }

    /**
     * Load a class with a given name.
     * It will try to load the class in the following order:
     * <ul>
     * <li>From {@link Thread}.currentThread().getContextClassLoader()
     * <li>Using the basic {@link Class#forName(java.lang.String) }
     * <li>From {@link ClassUtils}.class.getClassLoader()
     * </ul>
     *
     * @param name Fully qualified class name to be loaded
     * @return Class object
     * @exception ClassNotFoundException if the class cannot be found
     */
    public static Class getClass(String name) throws ClassNotFoundException
    {
        try
        {
            return getThreadContextLoader().loadClass(name);
        }
        catch (ClassNotFoundException e)
        {
            try
            {
                return Class.forName(name);
            }
            catch (ClassNotFoundException ex)
            {
                return getClassLoader().loadClass(name);
            }
        }
    }

    /**
     * Get an instance of a named class.
     * @param classname class name
     * @return new class instance
     * @throws ClassNotFoundException if class is not found
     * @throws IllegalAccessException if not granted
     * @throws InstantiationException if instance creation throwed
     */
    public static Object getInstance(String classname)
         throws ClassNotFoundException, IllegalAccessException,
                InstantiationException
    {
        return getClass(classname).newInstance();
    }

    /**
     * Load all resources with the specified name. If none are found, we 
     * prepend the name with '/' and try again.
     *
     * This will attempt to load the resources from the following methods (in order):
     * <ul>
     * <li>Thread.currentThread().getContextClassLoader().getResources(name)</li>
     * <li>{@link ClassUtils}.class.getClassLoader().getResources(name)</li>
     * <li>{@link ClassUtils}.class.getResource(name)</li>
     * <li>{@link #getCallerLoader(Object caller)}.getResources(name)</li>
     * <li>caller.getClass().getResource(name)</li>
     * </ul>
     *
     * @param name The name of the resources to load
     * @param caller The instance or {@link Class} calling this method
     * @return the list of found resources
     */
    public static List<URL> getResources(String name, Object caller)
    {
        Set<String> urls = new LinkedHashSet<String>();

        // try to load all from the current thread context classloader
        addResources(name, urls, getThreadContextLoader());

        // try to load all from this class' classloader
        if (!addResources(name, urls, getClassLoader()))
        {
            // ok, try to load one directly from this class
            addResource(name, urls, ClassUtils.class);
        }

        // try to load all from the classloader of the calling class
        if (!addResources(name, urls, getCallerLoader(caller)))
        {
            // try to load one directly from the calling class
            addResource(name, urls, caller.getClass());
        }

        if (!urls.isEmpty())
        {
            List<URL> result = new ArrayList<URL>(urls.size());
            try
            {
                for (String url : urls)
                {
                    result.add(new URL(url));
                }
            }
            catch (MalformedURLException mue)
            {
                throw new IllegalStateException("A URL could not be recreated from its own toString() form", mue);
            }
            return result;
        }
        else if (!name.startsWith("/"))
        {
            // try again with a / in front of the name
            return getResources("/"+name, caller);
        }
        else
        {
            return Collections.emptyList();
        }
    }

    private static final void addResource(String name, Set<String> urls, Class c)
    {
        URL url = c.getResource(name);
        if (url != null)
        {
            urls.add(url.toString());
        }
    }

    private static final boolean addResources(String name, Set<String> urls,
                                              ClassLoader loader)
    {
        boolean foundSome = false;
        try
        {
            Enumeration<URL> e = loader.getResources(name);
            while (e.hasMoreElements())
            {
                urls.add(e.nextElement().toString());
                foundSome = true;
            }
        }
        catch (IOException ioe)
        {
            // ignore
        }
        return foundSome;
    }

    private static URL getResourceImpl(final String name, final Object caller)
    {
        URL url = getThreadContextLoader().getResource(name);
        if (url == null)
        {
            url = getClassLoader().getResource(name);
            if (url == null)
            {
                url = ClassUtils.class.getResource(name);
                if (url == null && caller != null)
                {
                    Class callingClass = caller.getClass();
                    if (callingClass == Class.class)
                    {
                        callingClass = (Class)caller;
                    }
                    url = callingClass.getResource(name);
                }
            }
        }
        return url;
    }

    private static InputStream getResourceAsStreamImpl(final String name, final Object caller)
    {
        InputStream inputStream = getThreadContextLoader().getResourceAsStream(name);
        if (inputStream == null)
        {
            inputStream = getClassLoader().getResourceAsStream(name);
            if (inputStream == null)
            {
                inputStream = ClassUtils.class.getResourceAsStream(name);
                if (inputStream == null && caller != null)
                {
                    Class callingClass = caller.getClass();
                    if (callingClass == Class.class)
                    {
                        callingClass = (Class)caller;
                    }
                    inputStream = callingClass.getResourceAsStream(name);
                }
            }
        }
        return inputStream;
    }

    /**
     * Load a given resource.
     * This method will try to load the resource using the following methods (in order):
     * <ul>
     * <li>Thread.currentThread().getContextClassLoader().getResource(name)</li>
     * <li>{@link ClassUtils}.class.getClassLoader().getResource(name)</li>
     * <li>{@link ClassUtils}.class.getResource(name)</li>
     * <li>caller.getClass().getResource(name) or, if caller is a Class,
     *     caller.getResource(name)</li>
     * </ul>
     *
     * @param name The name of the resource to load
     * @param caller The instance or {@link Class} calling this method
     * @return the found URL, or null if not found
     */
    public static URL getResource(final String name, final Object caller)
    {
        URL url = null;
        if (System.getSecurityManager() != null)
        {
            url = AccessController.doPrivileged(
                new PrivilegedAction<URL>()
                {
                    @Override
                    public URL run()
                    {
                        return getResourceImpl(name, caller);
                    }
                });
        }
        else
        {
            url = getResourceImpl(name, caller);
        }
        return url;
    }

    /**
     * This is a convenience method to load a resource as a stream.
     * The algorithm used to find the resource is given in getResource()
     *
     * @param name The name of the resource to load
     * @param caller The instance or {@link Class} calling this method
     * @return the resource input stream or null if not found
     */
    public static InputStream getResourceAsStream(final String name, final Object caller)
    {
        InputStream inputStream = null;
        if (System.getSecurityManager() != null)
        {
            inputStream = AccessController.doPrivileged(
                new PrivilegedAction<InputStream>()
                {
                    @Override
                    public InputStream run()
                    {
                        return getResourceAsStreamImpl(name, caller);
                    }
                });
        }
        else
        {
            inputStream = getResourceAsStreamImpl(name, caller);
        }
        return inputStream;
    }

    /**
     * Find a callable method in a class
     * @param clazz target class
     * @param name method name
     * @param params method arguments classes
     * @return method object
     * @throws SecurityException if not granted
     */
    public static Method findMethod(Class clazz, String name, Class... params)
        throws SecurityException
    {
        try
        {
            // check for a public setup(Map) method first
            return clazz.getMethod(name, params);
        }
        catch (NoSuchMethodException nsme)
        {
            // ignore this
        }
        return findDeclaredMethod(clazz, name, params);
    }

    /**
     * Find a declared method in a class. It will be made accessible if needed and allowed.
     * @param clazz target class
     * @param name method name
     * @param params method arguments classes
     * @return
     * @throws SecurityException if not allowed
     */
    public static Method findDeclaredMethod(Class clazz, String name, Class... params)
        throws SecurityException
    {
        try
        {
            // check for a protected one
            Method method = clazz.getDeclaredMethod(name, params);
            if (method != null)
            {
                // and give this class access to it
                method.setAccessible(true);
                return method;
            }
        }
        catch (NoSuchMethodException nsme)
        {
            // ignore this
        }

        // ok, didn't find it declared in this class, try the superclass
        Class supclazz = clazz.getSuperclass();
        if (supclazz != null)
        {
            // recurse upward
            return findDeclaredMethod(supclazz, name, params);
        }
        // otherwise, return null
        return null;
    }

    /**
     * Given a static field path, aka <i>classname</i>.<i>field</i>, get the field value.
     * @param fieldPath field path
     * @return field value
     * @throws ClassNotFoundException if class hasn't been found
     * @throws NoSuchFieldException if field hasn't been found
     * @throws SecurityException if not granted
     * @throws IllegalAccessException if field is not accessible
     */
    public static Object getFieldValue(String fieldPath)
        throws ClassNotFoundException, NoSuchFieldException,
               SecurityException, IllegalAccessException
    {
        int lastDot = fieldPath.lastIndexOf('.');
        String classname = fieldPath.substring(0, lastDot);
        String fieldname = fieldPath.substring(lastDot + 1, fieldPath.length());

        Class clazz = getClass(classname);
        return getFieldValue(clazz, fieldname);
    }

    /**
     * Given a class and a static field name, get the field value.
     * @param clazz target class
     * @param fieldname field name
     * @return field value
     * @throws NoSuchFieldException if field hasn't been found
     * @throws SecurityException if not granted
     * @throws IllegalAccessException if field is not accessible
     */
    public static Object getFieldValue(Class clazz, String fieldname)
        throws NoSuchFieldException, SecurityException, IllegalAccessException
    {
        Field field = clazz.getField(fieldname);
        int mod = field.getModifiers();
        if (!Modifier.isStatic(mod))
        {
            throw new UnsupportedOperationException("Field "+fieldname+" in class "+clazz.getName()+" is not static.  Only static fields are supported.");
        }
        return field.get(null);
    }

    /**
     * Retrieves an Iterator from or creates and Iterator for the specified object.
     * This method is almost entirely copied from Engine's UberspectImpl class.
     * @param obj the target obj
     * @return an iterator over the content of obj, or null if not found
     * @throws NoSuchMethodException if no iterator() method
     * @throws IllegalAccessException if iterator() method not callable
     * @throws InvocationTargetException if iterator() method throwed
     */
    public static Iterator getIterator(Object obj)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
    {
        if (obj.getClass().isArray())
        {
            return new ArrayIterator(obj);
        }
        else if (obj instanceof Collection)
        {
            return ((Collection) obj).iterator();
        }
        else if (obj instanceof Map)
        {
            return ((Map) obj).values().iterator();
        }
        else if (obj instanceof Iterator)
        {
            return ((Iterator) obj);
        }
        else if (obj instanceof Iterable)
        {
            return ((Iterable)obj).iterator();
        }
        else if (obj instanceof Enumeration)
        {
            return new EnumerationIterator((Enumeration) obj);
        }
        else
        {
            // look for an iterator() method to support
            // any user tools/DTOs that want to work in
            // foreach w/o implementing the Collection interface
            Method iter = obj.getClass().getMethod("iterator");
            if (Iterator.class.isAssignableFrom(iter.getReturnType()))
            {
                return (Iterator)iter.invoke(obj);
            }
            else
            {
                return null;
            }
        }
    }

    private static String factoryMethodPrefixes[] = { "create", "new", "get" };

    /**
     * <p>Given a factory class and a target class, search for the following methods:</p>
     * <ul>
     *     <li><code>create<i>TargetClassname</i>()</code>,</li>
     *     <li><code>new<i>TargetClassname</i>()</code>, or</li>
     *     <li><code>get<i>TargetClassname</i>()</code>.</li>
     * </ul>
     * @param factory factory class
     * @param target target class
     * @return first factory method found, or null otherwise
     */
    public static Method findFactoryMethod(Class factory, Class target)
    {
        Method ret = null;
        String undecoratedName = target.getSimpleName();
        for (String prefix : factoryMethodPrefixes)
        {
            String methodName = prefix + undecoratedName;
            ret = findMethod(factory, methodName, new Class[] {});
            if (ret != null) break;
        }
        return ret;
    }

    public static Method findGetter(String getterName, Class clazz) throws NoSuchMethodException
    {
        return findGetter(getterName, clazz, true);
    }

    public static Method findGetter(String getterName, Class clazz, boolean mandatory) throws NoSuchMethodException
    {
        do
        {
            for (Method method : clazz.getDeclaredMethods())
            {
                // prefix matching: we allow a method name like setWriteAccess for a parameter like write="..."
                if (method.getParameterCount() == 0 && method.getName().startsWith(getterName))
                {
                    return method;
                }
            }
            clazz = clazz.getSuperclass();
        }
        while (clazz != Object.class);
        if (mandatory)
        {
            throw new NoSuchMethodException(clazz.getName() + "::" + getterName);
        }
        else
        {
            return null;
        }
    }

    public static Method findSetter(String setterName, Class clazz) throws NoSuchMethodException
    {
        return findSetter(setterName, clazz, x -> true);
    }

    public static Method findSetter(String setterName, Class clazz, Predicate<Class> argumentClassFilter) throws NoSuchMethodException
    {
        return findSetter(setterName, clazz, argumentClassFilter, true);
    }

    public static Method findSetter(String setterName, Class clazz, Predicate<Class> argumentClassFilter, boolean mandatory) throws NoSuchMethodException
    {
        do
        {
            for (Method method : clazz.getDeclaredMethods())
            {
                // prefix matching: we allow a method name like setWriteAccess for a parameter like write="..."
                if (method.getParameterCount() == 1 && method.getName().startsWith(setterName) && argumentClassFilter.test(method.getParameterTypes()[0]))
                {
                    return method;
                }
            }
            clazz = clazz.getSuperclass();
        }
        while (clazz != Object.class);
        if (mandatory)
        {
            throw new NoSuchMethodException(clazz.getName() + "::" + setterName);
        }
        else
        {
            return null;
        }
    }


}