/*
 *  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.
 */
package org.codehaus.groovy.reflection;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.Collections;

/**
 * This class contains utility methods to determine which class called the
 * current class to multiple levels of depth.  Calls used to handle the
 * groovy MOP are excluded from the level counting.
 */
public class ReflectionUtils {

    // these are packages in the call stack that are only part of the groovy MOP
    private static final Set<String> IGNORED_PACKAGES = new HashSet<String>();

    static {
        //IGNORED_PACKAGES.add("java.lang.reflect");
        IGNORED_PACKAGES.add("groovy.lang");
        IGNORED_PACKAGES.add("org.codehaus.groovy.reflection");
        IGNORED_PACKAGES.add("org.codehaus.groovy.runtime.callsite");
        IGNORED_PACKAGES.add("org.codehaus.groovy.runtime.metaclass");
        IGNORED_PACKAGES.add("org.codehaus.groovy.runtime");
        IGNORED_PACKAGES.add("sun.reflect");
        IGNORED_PACKAGES.add("java.lang.invoke");
        IGNORED_PACKAGES.add("org.codehaus.groovy.vmplugin.v7");
    }

    private static final ClassContextHelper HELPER = new ClassContextHelper();

    /**
     * Determine whether or not the getCallingClass methods will return
     * any sensible results.  On JVMs that are not Sun derived i.e.
     * (gcj, Harmony) this will likely return false.  When not available
     * all getCallingClass methods will return null.
     *
     * @return true if getCallingClass can return anything but null, false if
     *         it will only return null.
     */
    public static boolean isCallingClassReflectionAvailable() {
        return true;
    }

    /**
     * Get the immediate calling class, ignoring MOP frames.
     *
     * @return The Class of the caller
     */
    public static Class getCallingClass() {
        return getCallingClass(1);
    }

    /**
     * Get the called that is matchLevel stack frames before the call,
     * ignoring MOP frames.
     *
     * @param matchLevel how may call stacks down to look.
     *                   If it is less than 1 it is treated as though it was 1.
     * @return The Class of the matched caller, or null if there aren't
     *         enough stackframes to satisfy matchLevel
     */
    public static Class getCallingClass(int matchLevel) {
        return getCallingClass(matchLevel, Collections.EMPTY_SET);
    }

    /**
     * Get the called that is matchLevel stack frames before the call,
     * ignoring MOP frames and desired exclude packages.
     *
     * @param matchLevel           how may call stacks down to look.
     *                             If it is less than 1 it is treated as though it was 1.
     * @param extraIgnoredPackages A collection of string names of packages to exclude
     *                             in addition to the MOP packages when counting stack frames.
     * @return The Class of the matched caller, or null if there aren't
     *         enough stackframes to satisfy matchLevel
     */
    public static Class getCallingClass(int matchLevel, Collection<String> extraIgnoredPackages) {
        Class[] classContext = HELPER.getClassContext();

        int depth = 0;
        try {
            Class c;
            // this super class stuff is for Java 1.4 support only
            // it isn't needed on a 5.0 VM
            Class sc;
            do {
                do {
                    c = classContext[depth++];
                    if (c != null) {
                        sc = c.getSuperclass();
                    } else {
                        sc = null;
                    }
                } while (classShouldBeIgnored(c, extraIgnoredPackages)
                        || superClassShouldBeIgnored(sc));
            } while (c != null && matchLevel-- > 0 && depth<classContext.length);
            return c;
        } catch (Throwable t) {
            return null;
        }
    }

    private static boolean superClassShouldBeIgnored(Class sc) {
        return ((sc != null) && (sc.getPackage() != null) && "org.codehaus.groovy.runtime.callsite".equals(sc.getPackage().getName()));
    }

    private static boolean classShouldBeIgnored(Class c, Collection<String> extraIgnoredPackages) {
        return ((c != null)
                && (c.isSynthetic()
                    || (c.getPackage() != null
                        && (IGNORED_PACKAGES.contains(c.getPackage().getName())
                          || extraIgnoredPackages.contains(c.getPackage().getName())))));
    }

    private static class ClassContextHelper extends SecurityManager {
        @Override
        public Class[] getClassContext() {
            return super.getClassContext();
        }
    }
}
