/*
 *  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.apache.tools.ant.util.depend.bcel;

import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.EmptyVisitor;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

/**
 * A BCEL visitor implementation to collect class dependency information
 *
 */
public class DependencyVisitor extends EmptyVisitor {
    /** The collected dependencies */
    private final Set<String> dependencies = new HashSet<>();
    /**
     * The current class's constant pool - used to determine class names
     * from class references.
     */
    private ConstantPool constantPool;

    /**
     * Get the dependencies collected by this visitor
     *
     * @return a Enumeration of classnames, being the classes upon which the
     *      visited classes depend.
     */
    public Enumeration<String> getDependencies() {
        return Collections.enumeration(dependencies);
    }

    /** Clear the current set of collected dependencies. */
    public void clearDependencies() {
        dependencies.clear();
    }

    /**
     * Visit the constant pool of a class
     *
     * @param constantPool the constant pool of the class being visited.
     */
    @Override
    public void visitConstantPool(final ConstantPool constantPool) {
        this.constantPool = constantPool;
    }

    /**
     * Visit a class reference
     *
     * @param constantClass the constantClass entry for the class reference
     */
    @Override
    public void visitConstantClass(final ConstantClass constantClass) {
        final String classname
             = constantClass.getConstantValue(constantPool).toString();
        addSlashClass(classname);
    }

    /**
     * Visit a name and type ref
     *
     * Look for class references in this
     *
     * @param obj the name and type reference being visited.
     */
    @Override
    public void visitConstantNameAndType(final ConstantNameAndType obj) {
        final String name = obj.getName(constantPool);
        if ("Ljava/lang/Class;".equals(obj.getSignature(constantPool))
                && name.startsWith("class$")) {
            String classname
                = name.substring("class$".length()).replace('$', '.');
            // does the class have a package structure
            final int index = classname.lastIndexOf('.');
            if (index > 0) {
                char start;
                // check if the package structure is more than 1 level deep
                final int index2 = classname.lastIndexOf('.', index - 1);
                if (index2 != -1) {
                    // class name has more than 1 package level 'com.company.Class'
                    start = classname.charAt(index2 + 1);
                } else {
                    // class name has only 1 package level 'package.Class'
                    start = classname.charAt(0);
                }
                // Check to see if it's an inner class 'com.company.Class$Inner'
                // CheckStyle:MagicNumber OFF
                if ((start > 0x40) && (start < 0x5B)) {
                    // first letter of the previous segment of the class name 'Class'
                    // is upper case ascii. so according to the spec it's an inner class
                    classname = classname.substring(0, index) + "$"
                        + classname.substring(index + 1);
                    addClass(classname);
                } else {
                    // Add the class in dotted notation 'com.company.Class'
                    addClass(classname);
                }
                // CheckStyle:MagicNumber ON
            } else {
                // Add a class with no package 'Class'
                addClass(classname);
            }
        }
    }

    /**
     * Visit a field of the class.
     *
     * @param field the field being visited
     */
    @Override
    public void visitField(final Field field) {
        addClasses(field.getSignature());
    }

    /**
     * Visit a Java class
     *
     * @param javaClass the class being visited.
     */
    @Override
    public void visitJavaClass(final JavaClass javaClass) {
        addClass(javaClass.getClassName());
    }

    /**
     * Visit a method of the current class
     *
     * @param method the method being visited.
     */
    @Override
    public void visitMethod(final Method method) {
        final String signature = method.getSignature();
        final int pos = signature.indexOf(')');
        addClasses(signature.substring(1, pos));
        addClasses(signature.substring(pos + 1));
    }

    /**
     * Add a classname to the list of dependency classes
     *
     * @param classname the class to be added to the list of dependencies.
     */
    void addClass(final String classname) {
        dependencies.add(classname);
    }

    /**
     * Add all the classes from a descriptor string.
     *
     * @param string the descriptor string, being descriptors separated by
     *      ';' characters.
     */
    private void addClasses(final String string) {
        final StringTokenizer tokens = new StringTokenizer(string, ";");
        while (tokens.hasMoreTokens()) {
            final String descriptor = tokens.nextToken();
            final int pos = descriptor.indexOf('L');
            if (pos != -1) {
                addSlashClass(descriptor.substring(pos + 1));
            }
        }
    }

    /**
     * Adds a class name in slash format
     * (for example org/apache/tools/ant/Main).
     *
     * @param classname the class name in slash format
     */
    private void addSlashClass(final String classname) {
        addClass(classname.replace('/', '.'));
    }
}

