/*
 * 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.math.BigDecimal;
import java.util.List;
import java.util.Locale;

import com.strobel.assembler.metadata.JvmType;
import com.strobel.assembler.metadata.MethodDefinition;
import com.strobel.assembler.metadata.MethodReference;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.decompiler.ast.AstCode;
import com.strobel.decompiler.ast.Expression;

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.Exprs;
import one.util.huntbugs.util.Nodes;
import one.util.huntbugs.warning.Roles;

/**
 * @author Tagir Valeev
 *
 */
@WarningDefinition(category = "BadPractice", name = "FloatComparison", maxScore = 40)
public class FloatingPointComparison {
    @AstVisitor(nodes=AstNodes.EXPRESSIONS)
    public void visit(Expression node, MethodContext ctx, MethodDefinition md) {
        if (node.getCode() != AstCode.CmpEq && node.getCode() != AstCode.CmpNe)
            return;
        List<Expression> args = node.getArguments();
        TypeReference inferredType = args.get(0).getInferredType();
        if(inferredType == null)
            return;
        JvmType type = inferredType.getSimpleType();
        if (type != JvmType.Double && type != JvmType.Float)
            return;
        Expression leftArg = Exprs.getChild(node, 0);
        Expression rightArg = Exprs.getChild(node, 1);
        Object left = Nodes.getConstant(leftArg);
        Object right = Nodes.getConstant(rightArg);
        int priority = tweakPriority(args.get(0)) + tweakPriority(args.get(1));
        if(ValuesFlow.anyMatch(leftArg, rightArg::equals) || ValuesFlow.anyMatch(rightArg, leftArg::equals))
            priority += 20;
        String lcName = md.getName().toLowerCase(Locale.ENGLISH);
        if (lcName.contains("equal") || lcName.contains("compare"))
            priority += 20;
        Number n = left instanceof Number ? (Number) left : right instanceof Number ? (Number) right : null;
        if(n != null)
            ctx.report("FloatComparison", priority, node, Roles.NUMBER.create(n));
        else
            ctx.report("FloatComparison", priority, node);
    }

    private int tweakPriority(Expression expr) {
        Object val = Nodes.getConstant(expr);
        if (val instanceof Double || val instanceof Float) {
            double v = ((Number) val).doubleValue();
            float f = ((Number) val).floatValue();
            if (v == 0.0 || Double.isInfinite(v) || Double.isNaN(v))
                return 50;
            if (v == 1.0 || v == 2.0 || v == -1.0 || v == -2.0 || v == Double.MIN_VALUE || v == Double.MAX_VALUE
                || v == -Double.MAX_VALUE || v == -Double.MIN_VALUE || f == Float.MAX_VALUE || f == Float.MIN_VALUE ||
                f == -Float.MAX_VALUE || f == -Float.MIN_VALUE)
                return 40;
            if (v == 3.0 || v == 4.0 || v == -3.0 || v == -4.0)
                return 30;
            int prec = new BigDecimal(v).precision();
            if (prec < 3)
                return 25;
            if (prec < 7)
                return 20;
            if (prec < 10)
                return 15;
            return 5;
        }
        if (expr.getCode() == AstCode.InvokeStatic) {
            MethodReference method = (MethodReference) expr.getOperand();
            if(method.getName().equals("floor") || method.getName().equals("round") || method.getName().equals("rint")) {
                return method.getDeclaringType().getInternalName().equals("java/lang/Math") ? 50 : 35;
            }
        }
        if (Nodes.isToFloatingPointConversion(expr)) {
            Expression arg = expr.getArguments().get(0);
            // check like if ((int)d == d)
            if(arg.getCode() == AstCode.D2I || arg.getCode() == AstCode.D2L || arg.getCode() == AstCode.F2I || arg.getCode() == AstCode.F2L)
                return 40;
            return 20;
        }
        return 0;
    }
}
