/*
 * Copyright 2016 HuntBugs contributors
 * 
 * Licensed 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 one.util.huntbugs.detect;

import java.util.Collections;
import java.util.List;
import java.util.Set;

import com.strobel.assembler.metadata.MethodDefinition;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.decompiler.ast.AstCode;
import com.strobel.decompiler.ast.Block;
import com.strobel.decompiler.ast.CatchBlock;
import com.strobel.decompiler.ast.Expression;
import com.strobel.decompiler.ast.Variable;

import one.util.huntbugs.flow.Inf;
import one.util.huntbugs.flow.ValuesFlow;
import one.util.huntbugs.registry.MethodContext;
import one.util.huntbugs.registry.anno.AstNodes;
import one.util.huntbugs.registry.anno.AstVisitor;
import one.util.huntbugs.registry.anno.WarningDefinition;
import one.util.huntbugs.util.NodeChain;
import one.util.huntbugs.util.Nodes;
import one.util.huntbugs.util.Types;
import one.util.huntbugs.warning.Roles;

/**
 * @author Tagir Valeev
 *
 */
@WarningDefinition(category="RedundantCode", name="DeadStoreInReturn", maxScore=50)
@WarningDefinition(category="RedundantCode", name="DeadIncrementInReturn", maxScore=60)
@WarningDefinition(category="RedundantCode", name="DeadIncrementInAssignment", maxScore=60)
@WarningDefinition(category="RedundantCode", name="DeadParameterStore", maxScore=60)
@WarningDefinition(category="RedundantCode", name="DeadLocalStore", maxScore=50)
@WarningDefinition(category="RedundantCode", name="UnusedLocalVariable", maxScore=35)
public class DeadLocalStore {
    @AstVisitor(nodes=AstNodes.EXPRESSIONS)
    public void visit(Expression expr, NodeChain nc, MethodContext mc, MethodDefinition md, TypeDefinition td) {
        if(expr.getCode() == AstCode.Return && expr.getArguments().size() == 1) {
            Expression arg = expr.getArguments().get(0);
            if(arg.getCode() == AstCode.Store) {
                mc.report("DeadStoreInReturn", 0, arg);
            } else if(arg.getCode() == AstCode.PostIncrement) {
                Expression var = arg.getArguments().get(0);
                if(var.getOperand() instanceof Variable)
                    mc.report("DeadIncrementInReturn", 0, var);
            }
        }
        if(expr.getCode() == AstCode.Store) {
            Variable var = (Variable) expr.getOperand();
            if(var.isGenerated())
                return;
            Expression child = expr.getArguments().get(0);
            if(child.getCode() == AstCode.PostIncrement) {
                Expression load = child.getArguments().get(0);
                if(load.getCode() == AstCode.Load && var.equals(load.getOperand()) && Integer.valueOf(1).equals(child.getOperand())) {
                    mc.report("DeadIncrementInAssignment", 0, expr, Roles.EXPRESSION.create(var.getName() + " = " + var
                            .getName() + "++"));
                }
            }
            List<Expression> args = child.getCode() == AstCode.TernaryOp ? child.getArguments().subList(1, 3)
                    : Collections.singletonList(child);
            for(Expression arg : args) {
                // exclude AstCode.Store (chaining assignment) as procyon sometimes reorders locals assignments
                if (mc.isAnnotated() && ValuesFlow.getSource(arg) == arg && arg.getCode() != AstCode.Store
                    && arg.getCode() != AstCode.PutField && arg.getCode() != AstCode.PutStatic) {
                    Set<Expression> usages = Inf.BACKLINK.findUsages(arg);
                    if(usages.size() == 1 && usages.iterator().next() == expr) {
                        Set<Expression> storeUsages = Inf.BACKLINK.findUsages(expr);
                        if(storeUsages.isEmpty()) {
                            if(mc.getCFG() != null && !mc.getCFG().isInCFG(expr)) {
                                // Was removed from CFG (probably inlined)
                                return;
                            }
                            if(nc.getNode() instanceof CatchBlock && nc.getNode().getChildren().get(0) == expr
                                    && ((CatchBlock)nc.getNode()).getCaughtTypes().size() > 1) {
                                // Exception variable in multi-catch block
                                return;
                            }
                            // autogenerated by javacc
                            if (md.getName().equals("adjustBeginLineColumn") && td.getSimpleName().equals(
                                "SimpleCharStream") || md.getName().startsWith("jj") && td.getSimpleName().endsWith(
                                    "TokenManager")) {
                                return;
                            }
                            // autogenerated by antlr
                            if(Types.is(td.getBaseType(), "org/antlr/runtime/Lexer") || Types.isInstance(td, "org/antlr/runtime/Parser")) {
                                return;
                            }
                            // autogenerated by protocol buffers
                            if(Types.is(td.getBaseType(), "com/google/protobuf/GeneratedMessage$Builder")) {
                                return;
                            }
                            // autogenerated by jflex
                            if (var.getName().equals("offset") && md.getName().startsWith("zz") && (td.getSimpleName()
                                    .endsWith("Lexer") || td.getSimpleName().contains("Tokenizer"))) {
                                return;
                            }
                            int priority = 0;
                            Block root = nc.getRoot();
                            if(arg.getCode() == AstCode.LdC && !var.isParameter()) {
                                Object val = arg.getOperand();
                                TypeReference tr = arg.getInferredType();
                                if(tr != null && (tr.isPrimitive() || Types.isString(tr))) {
                                    // probably final constant var which is inlined when used
                                    if (Nodes.find(root, n -> n != expr && n instanceof Expression
                                            && ((Expression) n).getOperand() == var) == null) {
                                        if(Nodes.find(root, n -> n != arg && n instanceof Expression
                                            && val.equals(((Expression) n).getOperand())) != null)
                                            return;
                                        // If constant value is not used explicitly, it's still possible that it's inlined
                                        // inside other constant expression, thus lower the priority
                                        priority += 20;
                                    }
                                }
                            }
                            if(var.isParameter()) {
                                mc.report("DeadParameterStore", priority, expr);
                            } else {
                                boolean unusedLocal = Nodes.find(root, n -> n instanceof Expression
                                    && ((Expression) n).getOperand() == var && ((Expression) n).getCode() != AstCode.Store) == null;
                                String type;
                                if (!unusedLocal) {
                                    type = "DeadLocalStore";
                                    if(arg.getCode() == AstCode.AConstNull) {
                                        priority += 20;
                                        if(mc.mayTerminateImplicitly(arg))
                                            return;
                                    } else if(arg.getCode() == AstCode.LdC) {
                                        if (arg.getOperand() instanceof Number
                                            && ((Number) arg.getOperand()).doubleValue() == 0.0)
                                            priority += 20;
                                        else if ("".equals(arg.getOperand()) || Integer.valueOf(1).equals(arg.getOperand())
                                                || Integer.valueOf(-1).equals(arg.getOperand()))
                                            priority += 10;
                                        if(mc.mayTerminateImplicitly(arg))
                                            return;
                                    }
                                } else {
                                    type = "UnusedLocalVariable";
                                }
                                mc.report(type, priority, expr);
                            }
                        }
                    }
                }
            }
        }
    }
}
