package org.javacs;

import com.sun.source.tree.*;
import com.sun.source.util.*;
import java.util.*;
import javax.lang.model.element.*;

class WarnUnused extends TreePathScanner<Void, Void> {
    private final Trees trees;
    // TODO ignore writes when calculating used
    private final Set<Element> reachable = new HashSet<>(), unused = new HashSet<>();

    WarnUnused(JavacTask task) {
        this.trees = Trees.instance(task);
    }

    Set<Element> notUsed() {
        unused.removeAll(reachable);
        return unused;
    }

    private void foundPrivateDeclaration() {
        unused.add(trees.getElement(getCurrentPath()));
    }

    private void foundReference() {
        var fromPath = getCurrentPath();
        var toEl = trees.getElement(fromPath);
        var toPath = trees.getPath(toEl);
        if (toPath == null) return;
        var fromUri = fromPath.getCompilationUnit().getSourceFile().toUri();
        var toUri = toPath.getCompilationUnit().getSourceFile().toUri();
        if (!fromUri.equals(toUri)) return;
        if (isLocalVariable(toPath)) {
            reachable.add(toEl);
        }
        if (!isReachable(toPath)) {
            reachable.add(toEl);
            scan(toPath, null);
        }
    }

    private boolean isReachable(TreePath path) {
        // Check if t is reachable because it's public
        var t = path.getLeaf();
        if (t instanceof VariableTree) {
            var v = (VariableTree) t;
            var isPrivate = v.getModifiers().getFlags().contains(Modifier.PRIVATE);
            if (!isPrivate || isLocalVariable(path)) {
                return true;
            }
        }
        if (t instanceof MethodTree) {
            var m = (MethodTree) t;
            var isPrivate = m.getModifiers().getFlags().contains(Modifier.PRIVATE);
            var isEmptyConstructor = m.getParameters().isEmpty() && m.getReturnType() == null;
            if (!isPrivate || isEmptyConstructor) {
                return true;
            }
        }
        if (t instanceof ClassTree) {
            var c = (ClassTree) t;
            var isPrivate = c.getModifiers().getFlags().contains(Modifier.PRIVATE);
            if (!isPrivate) {
                return true;
            }
        }
        // Check if t has been referenced by a reachable element
        var el = trees.getElement(path);
        if (reachable.contains(el)) {
            return true;
        }
        return false;
    }

    private boolean isLocalVariable(TreePath path) {
        if (path.getLeaf() instanceof VariableTree) {
            var parent = path.getParentPath().getLeaf();
            return !(parent instanceof ClassTree)
                    && !(parent instanceof MethodTree) // TODO hint for unused parameters
                    && !(parent instanceof LambdaExpressionTree);
        }
        return false;
    }

    @Override
    public Void visitVariable(VariableTree t, Void __) {
        if (isLocalVariable(getCurrentPath())) {
            foundPrivateDeclaration();
            super.visitVariable(t, null);
        } else if (isReachable(getCurrentPath())) {
            super.visitVariable(t, null);
        } else {
            foundPrivateDeclaration();
        }
        return null;
    }

    @Override
    public Void visitMethod(MethodTree t, Void __) {
        if (isReachable(getCurrentPath())) {
            super.visitMethod(t, null);
        } else {
            foundPrivateDeclaration();
        }
        return null;
    }

    @Override
    public Void visitClass(ClassTree t, Void __) {
        if (isReachable(getCurrentPath())) {
            super.visitClass(t, null);
        } else {
            foundPrivateDeclaration();
        }
        return null;
    }

    @Override
    public Void visitIdentifier(IdentifierTree t, Void __) {
        foundReference();
        return super.visitIdentifier(t, null);
    }

    @Override
    public Void visitMemberSelect(MemberSelectTree t, Void __) {
        foundReference();
        return super.visitMemberSelect(t, null);
    }

    @Override
    public Void visitMemberReference(MemberReferenceTree t, Void __) {
        foundReference();
        return super.visitMemberReference(t, null);
    }

    @Override
    public Void visitNewClass(NewClassTree t, Void __) {
        foundReference();
        return super.visitNewClass(t, null);
    }
}

@@ -159,20 +159,16 @@
 nds Tree
-Path
 Scanner%3C
@@ -181,16 +181,706 @@
 Void%3E %7B%0A
+    // Copied from TreePathScanner%0A    // We need to be able to call scan(path, _) recursively%0A    private TreePath path;%0A%0A    public Void scan(TreePath path, Void p) %7B%0A        TreePath prev = this.path;%0A        this.path = path;%0A        try %7B%0A            return path.getLeaf().accept(this, p);%0A        %7D finally %7B%0A            this.path = prev; // So we can call scan(path, _) recursively%0A        %7D%0A    %7D%0A%0A    @Override%0A    public Void scan(Tree tree, Void p) %7B%0A        if (tree == null) return null;%0A%0A        TreePath prev = path;%0A        path = new TreePath(path, tree);%0A        try %7B%0A            return tree.accept(this, p);%0A        %7D finally %7B%0A            path = prev;%0A        %7D%0A    %7D%0A%0A
     priv
@@ -1297,32 +1297,20 @@
 Element(
-getCurrentP
+p
 ath
-()
 ));%0A    
@@ -1371,32 +1371,20 @@
 mPath = 
-getCurrentP
+p
 ath
-()
 ;%0A      
@@ -3618,32 +3618,20 @@
 ariable(
-getCurrentP
+p
 ath
-()
 )) %7B%0A   
@@ -3739,32 +3739,20 @@
 achable(
-getCurrentP
+p
 ath
-()
 )) %7B%0A   
@@ -3975,32 +3975,20 @@
 achable(
-getCurrentP
+p
 ath
-()
 )) %7B%0A   
@@ -4211,24 +4211,12 @@
 ble(
-getCurrentP
+p
 ath
-()
 )) %7B

Fix null pointer when using scan(path, _) recursively