/*
 * 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.netbeans.modules.php.editor.verification;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.prefs.Preferences;
import javax.swing.JComponent;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.api.Hint;
import org.netbeans.modules.csl.api.HintSeverity;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.csl.spi.support.CancelSupport;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.api.ElementQuery;
import org.netbeans.modules.php.editor.api.elements.MethodElement;
import org.netbeans.modules.php.editor.model.ClassScope;
import org.netbeans.modules.php.editor.model.FileScope;
import org.netbeans.modules.php.editor.model.InterfaceScope;
import org.netbeans.modules.php.editor.model.ModelUtils;
import org.netbeans.modules.php.editor.model.TraitScope;
import org.netbeans.modules.php.editor.parser.PHPParseResult;
import org.netbeans.modules.php.editor.parser.astnodes.ASTNode;
import org.netbeans.modules.php.editor.parser.astnodes.ArrowFunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Assignment;
import org.netbeans.modules.php.editor.parser.astnodes.Block;
import org.netbeans.modules.php.editor.parser.astnodes.BodyDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.CastExpression;
import org.netbeans.modules.php.editor.parser.astnodes.CatchClause;
import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ClassInstanceCreation;
import org.netbeans.modules.php.editor.parser.astnodes.CloneExpression;
import org.netbeans.modules.php.editor.parser.astnodes.ConditionalExpression;
import org.netbeans.modules.php.editor.parser.astnodes.ConstantDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ContinueStatement;
import org.netbeans.modules.php.editor.parser.astnodes.DeclareStatement;
import org.netbeans.modules.php.editor.parser.astnodes.DoStatement;
import org.netbeans.modules.php.editor.parser.astnodes.EchoStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Expression;
import org.netbeans.modules.php.editor.parser.astnodes.ExpressionStatement;
import org.netbeans.modules.php.editor.parser.astnodes.FieldsDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.ForEachStatement;
import org.netbeans.modules.php.editor.parser.astnodes.ForStatement;
import org.netbeans.modules.php.editor.parser.astnodes.FormalParameter;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.GotoLabel;
import org.netbeans.modules.php.editor.parser.astnodes.GotoStatement;
import org.netbeans.modules.php.editor.parser.astnodes.GroupUseStatementPart;
import org.netbeans.modules.php.editor.parser.astnodes.Identifier;
import org.netbeans.modules.php.editor.parser.astnodes.IfStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Include;
import org.netbeans.modules.php.editor.parser.astnodes.InstanceOfExpression;
import org.netbeans.modules.php.editor.parser.astnodes.InterfaceDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.LambdaFunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.MatchExpression;
import org.netbeans.modules.php.editor.parser.astnodes.MethodDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.MethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.NamespaceDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.PHPDocBlock;
import org.netbeans.modules.php.editor.parser.astnodes.PHPDocMethodTag;
import org.netbeans.modules.php.editor.parser.astnodes.PHPDocStaticAccessType;
import org.netbeans.modules.php.editor.parser.astnodes.PHPDocTypeTag;
import org.netbeans.modules.php.editor.parser.astnodes.PHPDocVarTypeTag;
import org.netbeans.modules.php.editor.parser.astnodes.PHPVarComment;
import org.netbeans.modules.php.editor.parser.astnodes.PostfixExpression;
import org.netbeans.modules.php.editor.parser.astnodes.PrefixExpression;
import org.netbeans.modules.php.editor.parser.astnodes.Program;
import org.netbeans.modules.php.editor.parser.astnodes.ReflectionVariable;
import org.netbeans.modules.php.editor.parser.astnodes.ReturnStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Scalar;
import org.netbeans.modules.php.editor.parser.astnodes.SingleFieldDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.SingleUseStatementPart;
import org.netbeans.modules.php.editor.parser.astnodes.StaticFieldAccess;
import org.netbeans.modules.php.editor.parser.astnodes.StaticMethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.SwitchStatement;
import org.netbeans.modules.php.editor.parser.astnodes.ThrowExpression;
import org.netbeans.modules.php.editor.parser.astnodes.UnaryOperation;
import org.netbeans.modules.php.editor.parser.astnodes.UseStatement;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.WhileStatement;
import org.netbeans.modules.php.editor.parser.astnodes.visitors.DefaultVisitor;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle.Messages;

/**
 *
 * @author Ondrej Brejla <obrejla@netbeans.org>
 */
public class UnusedVariableHint extends HintRule implements CustomisableRule {

    private static final String HINT_ID = "Unused.Variable.Hint"; //NOI18N
    private static final String CHECK_UNUSED_FORMAL_PARAMETERS = "php.verification.check.unused.formal.parameters"; //NOI18N
    private static final String CHECK_INHERITED_METHOD_PARAMETERS = "php.verification.check.inherited.method.parameters"; //NOI18N
    private static final List<String> UNCHECKED_VARIABLES = new ArrayList<>();
    private Preferences preferences;

    static {
        UNCHECKED_VARIABLES.add("this"); //NOI18N
        UNCHECKED_VARIABLES.add("GLOBALS"); //NOI18N
        UNCHECKED_VARIABLES.add("_SERVER"); //NOI18N
        UNCHECKED_VARIABLES.add("_GET"); //NOI18N
        UNCHECKED_VARIABLES.add("_POST"); //NOI18N
        UNCHECKED_VARIABLES.add("_FILES"); //NOI18N
        UNCHECKED_VARIABLES.add("_COOKIE"); //NOI18N
        UNCHECKED_VARIABLES.add("_SESSION"); //NOI18N
        UNCHECKED_VARIABLES.add("_REQUEST"); //NOI18N
        UNCHECKED_VARIABLES.add("_ENV"); //NOI18N
    }

    @Override
    public void invoke(PHPRuleContext context, List<Hint> hints) {
        PHPParseResult phpParseResult = (PHPParseResult) context.parserResult;
        if (phpParseResult.getProgram() == null) {
            return;
        }
        FileObject fileObject = phpParseResult.getSnapshot().getSource().getFileObject();
        if (fileObject != null) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            CheckVisitor checkVisitor = new CheckVisitor(fileObject, context.doc, getInheritedMethods(context));
            phpParseResult.getProgram().accept(checkVisitor);
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            hints.addAll(checkVisitor.getHints());
        }
    }

    private Map<String, List<String>> getInheritedMethods(PHPRuleContext context) {
        if (!checkUnusedFormalParameters(preferences) || checkInheritedMethodParameters(preferences)) {
            return Collections.emptyMap();
        }
        FileScope fileScope = context.fileScope;
        Collection<? extends ClassScope> allClasses = ModelUtils.getDeclaredClasses(fileScope);
        ElementQuery.Index index = context.getIndex();
        Map<String, List<String>> allInheritedMethods = new HashMap<>();
        for (ClassScope classScope : allClasses) {
            if (CancelSupport.getDefault().isCancelled()) {
                return Collections.emptyMap();
            }
            Set<MethodElement> inheritedMethods = getInheritedMethods(classScope, index);
            for (MethodElement inheritedMethod : inheritedMethods) {
                if (CancelSupport.getDefault().isCancelled()) {
                    return Collections.emptyMap();
                }
                List<String> methodElements = allInheritedMethods.get(inheritedMethod.getName());
                if (methodElements == null) {
                    methodElements = new ArrayList<>();
                    methodElements.add(classScope.getName());
                    allInheritedMethods.put(inheritedMethod.getName(), methodElements);
                } else {
                    methodElements.add(classScope.getName());
                }
            }
        }
        return allInheritedMethods;
    }

    private Set<MethodElement> getInheritedMethods(final ClassScope classScope, final ElementQuery.Index index) {
        Set<MethodElement> inheritedMethods = new HashSet<>();
        Set<MethodElement> declaredSuperMethods = new HashSet<>();
        Set<MethodElement> accessibleSuperMethods = new HashSet<>();
        Collection<? extends ClassScope> superClasses = classScope.getSuperClasses();
        for (ClassScope cls : superClasses) {
            declaredSuperMethods.addAll(index.getDeclaredMethods(cls));
            accessibleSuperMethods.addAll(index.getAccessibleMethods(cls, classScope));
        }
        Collection<? extends InterfaceScope> superInterface = classScope.getSuperInterfaceScopes();
        for (InterfaceScope interfaceScope : superInterface) {
            declaredSuperMethods.addAll(index.getDeclaredMethods(interfaceScope));
            accessibleSuperMethods.addAll(index.getAccessibleMethods(interfaceScope, classScope));
        }
        Collection<? extends TraitScope> traits = classScope.getTraits();
        for (TraitScope traitScope : traits) {
            declaredSuperMethods.addAll(index.getDeclaredMethods(traitScope));
            accessibleSuperMethods.addAll(index.getAccessibleMethods(traitScope, classScope));
        }
        inheritedMethods.addAll(declaredSuperMethods);
        inheritedMethods.addAll(accessibleSuperMethods);
        return inheritedMethods;
    }

    private class CheckVisitor extends DefaultVisitor {

        private final ArrayDeque<ASTNode> parentNodes = new ArrayDeque<>();
        private final Map<ASTNode, List<HintVariable>> unusedVariables = new HashMap<>();
        private final Map<ASTNode, List<HintVariable>> usedVariables = new HashMap<>();
        private final FileObject fileObject;
        private final BaseDocument baseDocument;
        private final List<Hint> hints;
        private boolean forceVariableAsUsed;
        private boolean forceVariableAsUnused;
        private boolean isInInheritedMethod;
        private String className = ""; // NOI18N
        private final Map<String, List<String>> allInheritedMethods; // method name, class names
        private final Map<ArrowFunctionDeclaration, Set<String>> arrowFunctionParameters = new HashMap<>();
        private ArrowFunctionDeclaration firstArrowFunctionNode = null;
        private ArrowFunctionDeclaration currentArrowFunctionNode = null;

        CheckVisitor(FileObject fileObject, BaseDocument baseDocument, Map<String, List<String>> allInheritedMethods) {
            this.fileObject = fileObject;
            this.baseDocument = baseDocument;
            this.allInheritedMethods = allInheritedMethods;
            hints = new ArrayList<>();
        }

        public List<Hint> getHints() {
            for (List<HintVariable> scopeVariables : unusedVariables.values()) {
                for (HintVariable variable : scopeVariables) {
                    createHint(variable);
                }
            }
            return Collections.unmodifiableList(hints);
        }

        @Messages({
            "# {0} - Name of the variable",
            "UnusedVariableHintCustom=Variable ${0} seems to be unused in its scope"
        })
        private void createHint(HintVariable variable) {
            int start = variable.getStartOffset();
            int end = variable.getEndOffset();
            OffsetRange offsetRange = new OffsetRange(start, end);
            if (showHint(offsetRange, baseDocument)) {
                hints.add(new Hint(UnusedVariableHint.this, Bundle.UnusedVariableHintCustom(variable.getName()), fileObject, offsetRange, null, 500));
            }
        }

        @CheckForNull
        private Identifier getIdentifier(Variable variable) {
            Identifier retval = null;
            if (variable != null && variable.isDollared()) {
                if (variable.getName() instanceof Identifier) {
                    retval = (Identifier) variable.getName();
                }
            }
            return retval;
        }

        private List<HintVariable> getUsedScopeVariables(ASTNode parentNode) {
            List<HintVariable> usedScopeVariables = usedVariables.get(parentNode);
            if (usedScopeVariables == null) {
                usedScopeVariables = new ArrayList<>();
                usedVariables.put(parentNode, usedScopeVariables);
            }
            return usedScopeVariables;
        }

        private List<HintVariable> getUnusedScopeVariables(ASTNode parentNode) {
            List<HintVariable> unusedScopeVariables = unusedVariables.get(parentNode);
            if (unusedScopeVariables == null) {
                unusedScopeVariables = new ArrayList<>();
                unusedVariables.put(parentNode, unusedScopeVariables);
            }
            return unusedScopeVariables;
        }

        @CheckForNull
        private HintVariable getUnusedVariable(String currentVarName, List<HintVariable> unusedScopeVariables) {
            HintVariable retval = null;
            for (HintVariable variable : unusedScopeVariables) {
                String varName = variable.getName();
                if (currentVarName.equals(varName)) {
                    retval = variable;
                    break;
                }
            }
            return retval;
        }

        private boolean isVariableUsed(String currentVarName, List<HintVariable> usedScopeVariables) {
            boolean retval = false;
            for (HintVariable variable : usedScopeVariables) {
                String varName = variable.getName();
                if (currentVarName.equals(varName)) {
                    retval = true;
                    break;
                }
            }
            return retval;
        }

        private void forceVariableAsUnused(HintVariable node, List<HintVariable> unusedScopeVariables) {
            HintVariable unusedVariable = getUnusedVariable(node.getName(), unusedScopeVariables);
            if (unusedVariable != null) {
                unusedScopeVariables.remove(unusedVariable);
            }
            unusedScopeVariables.add(node);
        }

        private void forceVariableAsUsed(HintVariable hintVariable, List<HintVariable> usedScopeVariables, List<HintVariable> unusedScopeVariables) {
            String currentVarName = hintVariable.getName();
            if (isVariableUsed(currentVarName, usedScopeVariables)) {
                return;
            }
            usedScopeVariables.add(hintVariable);
            HintVariable unusedVariable = getUnusedVariable(currentVarName, unusedScopeVariables);
            if (unusedVariable != null) {
                unusedScopeVariables.remove(unusedVariable);
            }
        }

        @Override
        public void visit(Variable node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            Identifier identifier = getIdentifier(node);
            if (identifier != null && !isInGlobalContext()) {
                if (parentNodes.peek() instanceof ArrowFunctionDeclaration) {
                    if (!isArrowFunctionParameter(node)) {
                        return;
                    }
                }
                process(HintVariable.create(node, identifier.getName()));
            }
        }

        private boolean isInGlobalContext() {
            return (parentNodes.peek() instanceof Program) || (parentNodes.peek() instanceof NamespaceDeclaration);
        }

        private boolean isArrowFunctionParameter(Variable variable) {
            Set<String> params = arrowFunctionParameters.get(currentArrowFunctionNode);
            return params != null && params.contains(CodeUtils.extractVariableName(variable));
        }

        private void process(HintVariable hintVariable) {
            if (hintVariable != null && !UNCHECKED_VARIABLES.contains(hintVariable.getName())) {
                ASTNode parentNode = parentNodes.peek();
                if (parentNode instanceof ArrowFunctionDeclaration) {
                    // for nested arrow function
                    if (currentArrowFunctionNode != null) {
                        parentNode = currentArrowFunctionNode;
                    }
                }
                String currentVarName = hintVariable.getName();
                List<HintVariable> usedScopeVariables = getUsedScopeVariables(parentNode);
                List<HintVariable> unusedScopeVariables = getUnusedScopeVariables(parentNode);
                if (forceVariableAsUnused) {
                    forceVariableAsUnused(hintVariable, unusedScopeVariables);
                    return;
                }
                if (forceVariableAsUsed) {
                    forceVariableAsUsed(hintVariable, usedScopeVariables, unusedScopeVariables);
                    return;
                }
                if (isVariableUsed(currentVarName, usedScopeVariables)) {
                    return;
                }
                HintVariable unusedVariable = getUnusedVariable(currentVarName, unusedScopeVariables);
                if (unusedVariable != null) {
                    unusedScopeVariables.remove(unusedVariable);
                    usedScopeVariables.add(hintVariable);
                    return;
                }
                unusedScopeVariables.add(hintVariable);
            }
        }

        @Override
        public void visit(Program node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            parentNodes.push(node);
            super.visit(node);
            parentNodes.pop();
        }

        @Override
        public void visit(NamespaceDeclaration node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            parentNodes.push(node);
            super.visit(node);
            parentNodes.pop();
        }

        @Override
        public void visit(FunctionDeclaration node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            if (node.getBody() != null) {
                parentNodes.push(node);
                super.visit(node);
                parentNodes.pop();
            }
        }

        @Override
        public void visit(EchoStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpressions());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(ExpressionStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            if (node.getExpression() instanceof Variable) { // just variable without anything: {  $var; }
                forceVariableAsUnused = true;
                scan(node.getExpression());
                forceVariableAsUnused = false;
            } else {
                scan(node.getExpression());
            }
        }

        @Override
        public void visit(Include node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(FunctionInvocation node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            String functionName = CodeUtils.extractFunctionName(node);
            if ("compact".equals(functionName)) { //NOI18N
                handleCompactFunction(node);
            }
            super.visit(node);
            forceVariableAsUsed = false;
        }

        private void handleCompactFunction(final FunctionInvocation node) {
            List<Expression> parameters = node.getParameters();
            for (Expression parameter : parameters) {
                handleFunctionParameter(parameter);
            }
        }

        private void handleFunctionParameter(final Expression parameter) {
            if (parameter instanceof Scalar) {
                handleScalar((Scalar) parameter);
            }
        }

        private void handleScalar(final Scalar scalar) {
            if (scalar.getScalarType().equals(Scalar.Type.STRING)) {
                process(HintVariable.create(scalar, extractVariableName(scalar.getStringValue())));
            }
        }

        private String extractVariableName(final String quotedValue) {
            String result = quotedValue;
            if ((quotedValue.startsWith("'") && quotedValue.endsWith("'")) || (quotedValue.startsWith("\"") && quotedValue.endsWith("\""))) { //NOI18N
                result = quotedValue.substring(1, quotedValue.length() - 1);
            }
            return result;
        }

        @Override
        public void visit(MethodInvocation node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getDispatcher());
            forceVariableAsUsed = false;
            scan(node.getMethod());
        }

        @Override
        public void visit(IfStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getCondition());
            forceVariableAsUsed = false;
            scan(node.getTrueStatement());
            scan(node.getFalseStatement());
        }

        @Override
        public void visit(InstanceOfExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            scan(node.getClassName());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(PostfixExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getVariable());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(PrefixExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getVariable());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(ReflectionVariable node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            Expression name = node.getName();
            if (name instanceof Scalar) {
                handleScalar((Scalar) name);
            } else {
                scan(name);
            }
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(CloneExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(CastExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(Assignment node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            scan(node.getLeftHandSide());
            forceVariableAsUsed = true;
            scan(node.getRightHandSide());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(ConditionalExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getCondition());
            scan(node.getIfTrue());
            scan(node.getIfFalse());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(ReturnStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(SwitchStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
            scan(node.getBody());
        }

        @Override
        public void visit(MatchExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
            scan(node.getMatchArms());
        }

        @Override
        public void visit(ThrowExpression node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(UnaryOperation node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(ClassDeclaration node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            className = node.getName().getName();
            scan(node.getBody());
            className = ""; // NOI18N
        }

        @Override
        public void visit(ClassInstanceCreation node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getClassName());
            scan(node.ctorParams());
            scan(node.getBody());
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(DoStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getCondition());
            forceVariableAsUsed = false;
            scan(node.getBody());
        }

        @Override
        public void visit(DeclareStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            scan(node.getBody());
        }

        @Override
        public void visit(CatchClause node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            Block body = node.getBody();
            if (!body.getStatements().isEmpty()) {
                scan(node.getVariable());
            }
            scan(body);
        }

        @Override
        public void visit(MethodDeclaration node) {
            if (checkUnusedFormalParameters(preferences) && !checkInheritedMethodParameters(preferences)) {
                String methodName = node.getFunction().getFunctionName().getName();
                List<String> classNames = allInheritedMethods.get(methodName);
                if (classNames != null) {
                    for (String clsName : classNames) {
                        if (CancelSupport.getDefault().isCancelled()) {
                            return;
                        }
                        if (className.equals(clsName)) {
                            isInInheritedMethod = true;
                            break;
                        }
                    }
                }
                super.visit(node);
                isInInheritedMethod = false;
            } else {
                super.visit(node);
            }
        }

        @Override
        public void visit(FormalParameter node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            if (BodyDeclaration.Modifier.isVisibilityModifier(node.getModifier())) {
                // [NETBEANS-4443] PHP 8.0 Construcotr Property Promotion
                forceVariableAsUsed = true;
                scan(node.getParameterName());
                forceVariableAsUsed = false;
            } else if (checkUnusedFormalParameters(preferences) && !isInInheritedMethod) {
                scan(node.getParameterName());
            } else {
                forceVariableAsUsed = true;
                scan(node.getParameterName());
                forceVariableAsUsed = false;
            }
        }

        @Override
        public void visit(ForEachStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getExpression());
            forceVariableAsUsed = false;
            scan(node.getKey());
            scan(node.getValue());
            scan(node.getStatement());
        }

        @Override
        public void visit(ForStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getInitializers());
            scan(node.getConditions());
            scan(node.getUpdaters());
            forceVariableAsUsed = false;
            scan(node.getBody());
        }

        @Override
        public void visit(StaticMethodInvocation node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getDispatcher());
            forceVariableAsUsed = false;
            scan(node.getMethod());
        }

        @Override
        public void visit(WhileStatement node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            scan(node.getCondition());
            forceVariableAsUsed = false;
            scan(node.getBody());
        }

        @Override
        public void visit(ArrowFunctionDeclaration node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            if (firstArrowFunctionNode == null) {
                firstArrowFunctionNode = node;
            }
            if (currentArrowFunctionNode == null || parentNodes.peek() instanceof LambdaFunctionDeclaration) {
                currentArrowFunctionNode = node;
            }
            isInInheritedMethod = false;
            collectArrowFunctionParameters(node);

            // avoid scaning formal parameters of Arrow Functions
            Expression expression = node.getExpression();
            while (expression instanceof ArrowFunctionDeclaration) {
                expression = ((ArrowFunctionDeclaration) expression).getExpression();
            }

            // scaning for current parent node
            // we have to check whether variables of an arrow function expression are used
            // e.g. $value is used in the following case:
            // function myFunction($param1, $param2) {
            //     $value = 100;
            //     return fn($x) => ($param1 + $param2) * $x * $value;
            // }
            forceVariableAsUsed = true;
            if (expression instanceof LambdaFunctionDeclaration) {
                scan(((LambdaFunctionDeclaration) expression).getLexicalVariables());
            } else {
                scan(expression);
            }
            forceVariableAsUsed = false;

            parentNodes.push(node);
            scan(node.getFormalParameters());
            scan(node.getExpression());
            parentNodes.pop();

            if (firstArrowFunctionNode == node) {
                // clear
                firstArrowFunctionNode = null;
                currentArrowFunctionNode = null;
                arrowFunctionParameters.clear();
            }
        }

        private void collectArrowFunctionParameters(ArrowFunctionDeclaration node) {
            Set<String> arrowFunctionParams = arrowFunctionParameters.get(currentArrowFunctionNode);
            if (arrowFunctionParams == null) {
                arrowFunctionParams = new HashSet<>();
                arrowFunctionParameters.put(currentArrowFunctionNode, arrowFunctionParams);
            }
            node.getFormalParameters().stream()
                    .map(param -> CodeUtils.extractFormalParameterName(param))
                    .filter(parameterName -> (parameterName != null))
                    .forEachOrdered(parameterName -> arrowFunctionParameters.get(currentArrowFunctionNode).add(parameterName));
        }

        @Override
        public void visit(LambdaFunctionDeclaration node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            isInInheritedMethod = false;
            forceVariableAsUsed = true;
            scan(node.getLexicalVariables());
            forceVariableAsUsed = false;
            parentNodes.push(node);
            scan(node.getLexicalVariables());
            scan(node.getFormalParameters());
            scan(node.getBody());
            parentNodes.pop();
        }

        @Override
        public void visit(StaticFieldAccess node) {
            if (CancelSupport.getDefault().isCancelled()) {
                return;
            }
            forceVariableAsUsed = true;
            super.visit(node);
            forceVariableAsUsed = false;
        }

        @Override
        public void visit(InterfaceDeclaration node) {
            // intentionally
        }

        @Override
        public void visit(FieldsDeclaration node) {
            // intentionally
        }

        @Override
        public void visit(PHPDocBlock node) {
            // intentionally
        }

        @Override
        public void visit(PHPDocTypeTag node) {
            // intentionally
        }

        @Override
        public void visit(PHPDocMethodTag node) {
            // intentionally
        }

        @Override
        public void visit(PHPDocVarTypeTag node) {
            // intentionally
        }

        @Override
        public void visit(PHPDocStaticAccessType node) {
            // intentionally
        }

        @Override
        public void visit(PHPVarComment node) {
            // intentionally
        }

        @Override
        public void visit(ConstantDeclaration node) {
            // intentionally
        }

        @Override
        public void visit(ContinueStatement node) {
            // intentionally
        }

        @Override
        public void visit(SingleFieldDeclaration node) {
            // intentionally
        }

        @Override
        public void visit(UseStatement node) {
            // intentionally
        }

        @Override
        public void visit(SingleUseStatementPart node) {
            // intentionally
        }

        @Override
        public void visit(GroupUseStatementPart node) {
            // intentionally
        }

        @Override
        public void visit(GotoLabel node) {
            // intentionally
        }

        @Override
        public void visit(GotoStatement node) {
            // intentionally
        }

    }

    private static final class HintVariable {
        private final ASTNode node;
        private final String name;

        static HintVariable create(final ASTNode node, final String name) {
            return new HintVariable(node, name);
        }

        private HintVariable(final ASTNode node, final String name) {
            this.node = node;
            this.name = name;
        }

        public String getName() {
            return name;
        }

        public int getStartOffset() {
            return node.getStartOffset() + 1;
        }

        public int getEndOffset() {
            return node.getEndOffset();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final HintVariable other = (HintVariable) obj;
            return Objects.equals(this.name, other.name);
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 37 * hash + Objects.hashCode(this.name);
            return hash;
        }

    }

    @Override
    public String getId() {
        return HINT_ID;
    }

    @Override
    @Messages("UnusedVariableHintDesc=Detects variables which are declared, but not used in their scope.")
    public String getDescription() {
        return Bundle.UnusedVariableHintDesc();
    }

    @Override
    @Messages("UnusedVariableHintDispName=Unused Variables")
    public String getDisplayName() {
        return Bundle.UnusedVariableHintDispName();
    }

    @Override
    public HintSeverity getDefaultSeverity() {
        return HintSeverity.WARNING;
    }

    @Override
    public void setPreferences(Preferences preferences) {
        this.preferences = preferences;
    }

    @Override
    public JComponent getCustomizer(Preferences preferences) {
        JComponent customizer = new UnusedVariableCustomizer(preferences, this);
        setCheckUnusedFormalParameters(preferences, checkUnusedFormalParameters(preferences));
        setCheckInheritedMethodParameters(preferences, checkInheritedMethodParameters(preferences));
        return customizer;
    }

    public void setCheckUnusedFormalParameters(Preferences preferences, boolean isEnabled) {
        preferences.putBoolean(CHECK_UNUSED_FORMAL_PARAMETERS, isEnabled);
    }

    public boolean checkUnusedFormalParameters(Preferences preferences) {
        return preferences.getBoolean(CHECK_UNUSED_FORMAL_PARAMETERS, true);
    }

    public void setCheckInheritedMethodParameters(Preferences preferences, boolean isEnabled) {
        preferences.putBoolean(CHECK_INHERITED_METHOD_PARAMETERS, isEnabled);
    }

    public boolean checkInheritedMethodParameters(Preferences preferences) {
        return preferences.getBoolean(CHECK_INHERITED_METHOD_PARAMETERS, true);
    }

}
