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

package net.sourceforge.pmd.dcd;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import net.sourceforge.pmd.dcd.graph.ClassNode;
import net.sourceforge.pmd.dcd.graph.ConstructorNode;
import net.sourceforge.pmd.dcd.graph.FieldNode;
import net.sourceforge.pmd.dcd.graph.MemberNode;
import net.sourceforge.pmd.dcd.graph.MethodNode;
import net.sourceforge.pmd.dcd.graph.NodeVisitorAdapter;
import net.sourceforge.pmd.dcd.graph.UsageGraph;

/**
 * Perform a visitation a UsageGraph, looking for <em>dead code</em>, which is
 * essential code which is not used by any other code. There are various options
 * for configuration how this determination is made.
 */
public class UsageNodeVisitor extends NodeVisitorAdapter {

    /**
     * Configuration options for usage analysis.
     */
    public static final class Options {
        private boolean ignoreClassAnonymous = true;

        private boolean ignoreConstructorStaticInitializer = true;

        private boolean ignoreConstructorSinglePrivateNoArg = true;

        private boolean ignoreConstructorAllPrivate = false;

        private boolean ignoreMethodJavaLangObjectOverride = true;

        private boolean ignoreMethodAllOverride = false;

        private boolean ignoreMethodMain = true;

        private boolean ignoreFieldInlinable = true;

        public boolean isIgnoreClassAnonymous() {
            return ignoreClassAnonymous;
        }

        public void setIgnoreClassAnonymous(boolean ignoreClassAnonymous) {
            this.ignoreClassAnonymous = ignoreClassAnonymous;
        }

        public boolean isIgnoreConstructorStaticInitializer() {
            return ignoreConstructorStaticInitializer;
        }

        public void setIgnoreConstructorStaticInitializer(boolean ignoreConstructorStaticInitializer) {
            this.ignoreConstructorStaticInitializer = ignoreConstructorStaticInitializer;
        }

        public boolean isIgnoreConstructorSinglePrivateNoArg() {
            return ignoreConstructorSinglePrivateNoArg;
        }

        public void setIgnoreConstructorSinglePrivateNoArg(boolean ignoreConstructorSinglePrivateNoArg) {
            this.ignoreConstructorSinglePrivateNoArg = ignoreConstructorSinglePrivateNoArg;
        }

        public boolean isIgnoreConstructorAllPrivate() {
            return ignoreConstructorAllPrivate;
        }

        public void setIgnoreConstructorAllPrivate(boolean ignoreConstructorAllPrivate) {
            this.ignoreConstructorAllPrivate = ignoreConstructorAllPrivate;
        }

        public boolean isIgnoreMethodJavaLangObjectOverride() {
            return ignoreMethodJavaLangObjectOverride;
        }

        public void setIgnoreMethodJavaLangObjectOverride(boolean ignoreMethodJavaLangObjectOverride) {
            this.ignoreMethodJavaLangObjectOverride = ignoreMethodJavaLangObjectOverride;
        }

        public boolean isIgnoreMethodAllOverride() {
            return ignoreMethodAllOverride;
        }

        public void setIgnoreMethodAllOverride(boolean ignoreMethodAllOverride) {
            this.ignoreMethodAllOverride = ignoreMethodAllOverride;
        }

        public boolean isIgnoreMethodMain() {
            return ignoreMethodMain;
        }

        public void setIgnoreMethodMain(boolean ignoreMethodMain) {
            this.ignoreMethodMain = ignoreMethodMain;
        }

        public boolean isIgnoreFieldInlinable() {
            return ignoreFieldInlinable;
        }

        public void setIgnoreFieldInlinable(boolean ignoreFieldInlinable) {
            this.ignoreFieldInlinable = ignoreFieldInlinable;
        }

    }

    private final Options options = new Options();

    @Override
    public Object visit(UsageGraph usageGraph, Object data) {
        System.out.println("----------------------------------------");
        super.visit(usageGraph, data);
        System.out.println("----------------------------------------");
        return data;
    }

    @Override
    public Object visit(ClassNode classNode, Object data) {
        boolean log = true;
        if (options.isIgnoreClassAnonymous() && classNode.getType().isAnonymousClass()) {
            ignore("class anonymous", classNode);
            log = false;
        }
        if (log) {
            System.out.println("--- " + classNode.getName() + " ---");
            return super.visit(classNode, data);
        } else {
            return data;
        }
    }

    @Override
    public Object visit(FieldNode fieldNode, Object data) {
        if (fieldNode.getUsers().isEmpty()) {
            boolean log = true;
            // A field is inlinable if:
            // 1) It is final
            // 2) It is a primitive, or a java.lang.String
            if (options.isIgnoreFieldInlinable()) {
                if (Modifier.isFinal(fieldNode.getMember().getModifiers())
                        && fieldNode.getMember().getType().isPrimitive()
                        || fieldNode.getMember().getType().getName().equals("java.lang.String")) {
                    ignore("field inlinable", fieldNode);
                    log = false;
                }
            }
            if (log) {
                System.out.println("\t" + fieldNode.toStringLong());
            }
        }
        return super.visit(fieldNode, data);
    }

    @Override
    public Object visit(ConstructorNode constructorNode, Object data) {
        if (constructorNode.getUsers().isEmpty()) {
            boolean log = true;
            if (constructorNode.isStaticInitializer()) {
                if (options.isIgnoreConstructorStaticInitializer()) {
                    ignore("constructor static initializer", constructorNode);
                    log = false;
                }
            } else if (constructorNode.isInstanceInitializer()) {
                if (Modifier.isPrivate(constructorNode.getMember().getModifiers())) {
                    if (options.isIgnoreConstructorAllPrivate()) {
                        ignore("constructor all private", constructorNode);
                        log = false;
                    } else if (options.isIgnoreConstructorSinglePrivateNoArg()
                            && constructorNode.getMember().getParameterTypes().length == 0
                            && constructorNode.getClassNode().getConstructorNodes().size() == 1) {
                        ignore("constructor single private no-arg", constructorNode);
                        log = false;
                    }
                }
            }
            if (log) {
                System.out.println("\t" + constructorNode.toStringLong());
            }
        }
        return super.visit(constructorNode, data);
    }

    private static boolean isMainMethod(MethodNode node) {

        final Method method = node.getMember();

        return method.getName().equals("main") && Modifier.isPublic(method.getModifiers())
                && Modifier.isStatic(method.getModifiers()) && method.getReturnType() == Void.TYPE
                && method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isArray()
                && method.getParameterTypes()[0].getComponentType().equals(java.lang.String.class);
    }

    @Override
    public Object visit(MethodNode methodNode, Object data) {
        if (methodNode.getUsers().isEmpty()) {
            boolean log = true;
            if (options.isIgnoreMethodAllOverride()) {
                if (ClassLoaderUtil.isOverridenMethod(methodNode.getClassNode().getClass(), methodNode.getMember(),
                        false)) {
                    ignore("method all override", methodNode);
                    log = false;
                }
            } else if (options.isIgnoreMethodJavaLangObjectOverride()) {
                if (ClassLoaderUtil.isOverridenMethod(java.lang.Object.class, methodNode.getMember(), true)) {
                    ignore("method java.lang.Object override", methodNode);
                    log = false;
                }
            }
            if (options.isIgnoreMethodMain()) {
                if (isMainMethod(methodNode)) {
                    ignore("method public static void main(String[])", methodNode);
                    log = false;
                }
            }
            if (log) {
                System.out.println("\t" + methodNode.toStringLong());
            }
        }
        return super.visit(methodNode, data);
    }

    private void ignore(String description, ClassNode classNode) {
        System.out.println("Ignoring " + description + ": " + classNode.getName());
    }

    private void ignore(String description, MemberNode memberNode) {
        System.out.println("Ignoring " + description + ": " + memberNode.toStringLong());
    }
}
