/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
 */

package net.sourceforge.pmd.dcd.graph;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import net.sourceforge.pmd.dcd.ClassLoaderUtil;
import net.sourceforge.pmd.util.filter.Filter;

/**
 * A UsageGraph tracks usage references between Java classes, based upon a
 * parsing of the class files. Once the UsageGraph is built, it may be visited
 * to perform additional post-processing, or usage relationship analysis.
 * <p>
 * The UsageGraph is composed of ClassNodes. Each ClassNode has various
 * MemberNodes, specifically ConstructorNodes, FieldNodes, and MethodNodes. Each
 * of these MemberNodes keeps track of other MemberNodes which it <em>uses</em>
 * and other MemberNodes which are <em>users</em> of it. In this sense, the
 * graph can navigated bi-directionally across the <em>use</em> relationship
 * between MemberNodes.
 * <p>
 * Great effort is taken to keep the bookkeeping of the UsageGraph as tight as
 * possible, so that rather large code bases can be analyzed. While nodes can
 * grant access to the underlying Java Reflection APIs (e.g. Class, Constructor,
 * Field, Member), the results are stored using WeakReferences to assist with
 * memory usage.
 * <p>
 * A class Filter can be specified to limit the set of classes on which usage
 * references will be tracked. This is often done to limit memory usage to
 * interesting classes. For example, the <code>java.util</code> package is very
 * often used, and tracking usages would require a massive bookkeeping effort
 * which has little value.
 *
 * @see UsageGraphBuilder
 * @see ClassNode
 * @see MemberNode
 * @see ConstructorNode
 * @see FieldNode
 * @see MethodNode
 * @see NodeVisitor
 * @see NodeVisitorAcceptor
 */
public class UsageGraph implements NodeVisitorAcceptor {

    private final List<ClassNode> classNodes = new ArrayList<>();

    protected final Filter<String> classFilter;

    public UsageGraph(Filter<String> classFilter) {
        this.classFilter = classFilter;
    }

    @Override
    public Object accept(NodeVisitor visitor, Object data) {
        for (ClassNode classNode : classNodes) {
            visitor.visit(classNode, data);
        }
        return data;
    }

    public boolean isClass(String className) {
        checkClassName(className);
        return Collections.binarySearch(classNodes, className, ClassNodeComparator.INSTANCE) >= 0;
    }

    public ClassNode defineClass(String className) {
        checkClassName(className);
        int index = Collections.binarySearch(classNodes, className, ClassNodeComparator.INSTANCE);
        ClassNode classNode;
        if (index >= 0) {
            classNode = classNodes.get(index);
        } else {
            classNode = new ClassNode(className);
            classNodes.add(-(index + 1), classNode);
        }
        return classNode;
    }

    public FieldNode defineField(String className, String name, String desc) {
        ClassNode classNode = defineClass(className);
        return classNode.defineField(name, desc);
    }

    public MemberNode defineConstructor(String className, String name, String desc) {
        ClassNode classNode = defineClass(className);
        return classNode.defineConstructor(name, desc);
    }

    public MemberNode defineMethod(String className, String name, String desc) {
        ClassNode classNode = defineClass(className);
        if (ClassLoaderUtil.CLINIT.equals(name) || ClassLoaderUtil.INIT.equals(name)) {
            return classNode.defineConstructor(name, desc);
        } else {
            return classNode.defineMethod(name, desc);
        }
    }

    public void usageField(String className, String name, String desc, MemberNode usingMemberNode) {
        checkClassName(className);
        if (classFilter.filter(className)) {
            FieldNode fieldNode = defineField(className, name, desc);
            usage(fieldNode, usingMemberNode);
        }
    }

    public void usageMethod(String className, String name, String desc, MemberNode usingMemberNode) {
        checkClassName(className);
        if (classFilter.filter(className)) {
            MemberNode memberNode;
            if (ClassLoaderUtil.CLINIT.equals(name) || ClassLoaderUtil.INIT.equals(name)) {
                memberNode = defineConstructor(className, name, desc);
            } else {
                memberNode = defineMethod(className, name, desc);
            }
            usage(memberNode, usingMemberNode);
        }
    }

    private void usage(MemberNode use, MemberNode user) {
        use.addUser(user);
        user.addUse(use);
    }

    private void checkClassName(String className) {
        // Make sure it's not in byte code internal format, or file system path.
        if (className.indexOf('/') >= 0 || className.indexOf('\\') >= 0) {
            throw new IllegalArgumentException("Invalid class name: " + className);
        }
    }
}
