/*
 * 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.util;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.strobel.assembler.ir.Instruction;
import com.strobel.assembler.ir.OpCode;
import com.strobel.assembler.metadata.MethodBody;
import com.strobel.assembler.metadata.MethodDefinition;
import com.strobel.assembler.metadata.MethodReference;
import com.strobel.assembler.metadata.ParameterDefinition;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;

import one.util.huntbugs.warning.WarningAnnotation.MemberInfo;

/**
 * @author Tagir Valeev
 *
 */
public class Methods {
    private static final Set<String> SERIALIZATION_METHODS = 
            new HashSet<>(Arrays.asList("writeReplace", "readResolve",
                "readObject", "readObjectNoData", "writeObject"));
    
    public static boolean isEqualsMethod(MethodReference mr) {
        return mr.getName().equals("equals") && mr.getSignature().equals("(Ljava/lang/Object;)Z");
    }

    public static boolean isHashCodeMethod(MethodReference mr) {
        return mr.getName().equals("hashCode") && mr.getSignature().equals("()I");
    }
    
    public static boolean isGetClass(MethodReference mr) {
        return mr.getName().equals("getClass") && mr.getErasedSignature().equals("()Ljava/lang/Class;");
    }
    
    public static boolean is(MethodReference mr, String internalTypeName, String methodName, String methodSig) {
        return mr.getName().equals(methodName) && mr.getErasedSignature().equals(methodSig) &&
                mr.getDeclaringType().getInternalName().equals(internalTypeName);
    }
    
    public static boolean knownToThrow(MethodReference mr) {
        if (mr.getName().startsWith("assert") || mr.getName().startsWith("require")
            || mr.getDeclaringType().getSimpleName().equals("Assert"))
            return true;
        if (Types.isBoxed(mr.getDeclaringType()) && mr.getName().startsWith("parse"))
            return true;
        return false;
    }
    
    public static MethodDefinition findSuperMethod(MethodReference mr) {
        MethodDefinition md = mr.resolve();
        if(md == null)
            return null;
        TypeDefinition td = md.getDeclaringType();
        return findSuperMethod(td, new MemberInfo(resolveToBridge(md)));
    }
    
    public static Set<MethodDefinition> findSuperMethods(MethodReference mr) {
        MethodDefinition md = mr.resolve();
        if(md == null)
            return null;
        TypeDefinition td = md.getDeclaringType();
        Set<MethodDefinition> set = new HashSet<>();
        collectSuperMethods(td, new MemberInfo(resolveToBridge(md)), set);
        return set;
    }
    
    private static void collectSuperMethods(TypeDefinition type, MemberInfo mi, Set<MethodDefinition> list) {
        TypeReference superType = type.getBaseType();
        if(superType != null) {
            TypeDefinition superTd = superType.resolve();
            if(superTd != null) {
                MethodDefinition result = findMethod(superTd, mi);
                if(result != null)
                    list.add(result);
                else
                    collectSuperMethods(superTd, mi, list);
            }
        }
        for(TypeReference iface : type.getExplicitInterfaces()) {
            TypeDefinition ifaceTd = iface.resolve();
            if(ifaceTd != null) {
                MethodDefinition result = findMethod(ifaceTd, mi);
                if(result != null)
                    list.add(result);
                else
                    collectSuperMethods(ifaceTd, mi, list);
            }
        }
    }

    public static MethodDefinition resolveToBridge(MethodDefinition md) {
        if (md.isBridgeMethod()) {
            return md;
        }
        for (MethodDefinition candidate : md.getDeclaringType().getDeclaredMethods()) {
            if (candidate.getName().equals(md.getName()) && candidate.isBridgeMethod()) {
                List<ParameterDefinition> params = candidate.getParameters();
                if (params.size() == md.getParameters().size()) {
                    MethodBody body = candidate.getBody();
                    if (body != null) {
                        for (Instruction instr : body.getInstructions()) {
                            if (instr.getOperandCount() == 1) {
                                Object operand = instr.getOperand(0);
                                if (operand instanceof MethodReference) {
                                    MethodReference mr = (MethodReference) operand;
                                    if (mr.getName().equals(md.getName()) && mr.getErasedSignature().equals(md
                                            .getErasedSignature()) && mr.getDeclaringType().isEquivalentTo(md
                                                    .getDeclaringType())) {
                                        return candidate;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return md;
    }
    
    public static MethodDefinition findSuperMethod(TypeDefinition type, MemberInfo mi) {
        TypeReference superType = type.getBaseType();
        if(superType != null) {
            TypeDefinition superTd = superType.resolve();
            if(superTd != null) {
                MethodDefinition result = findMethod(superTd, mi);
                if(result != null)
                    return result;
                result = findSuperMethod(superTd, mi);
                if(result != null)
                    return result;
            }
        }
        for(TypeReference iface : type.getExplicitInterfaces()) {
            TypeDefinition ifaceTd = iface.resolve();
            if(ifaceTd != null) {
                MethodDefinition result = findMethod(ifaceTd, mi);
                if(result != null)
                    return result;
                result = findSuperMethod(ifaceTd, mi);
                if(result != null)
                    return result;
            }
        }
        return null; 
    }

    public static MethodDefinition findMethod(TypeDefinition td, MemberInfo mi) {
        if(td == null)
            return null;
        for(MethodDefinition decl : td.getDeclaredMethods()) {
            if(decl.getName().equals(mi.getName())) {
                String sig1 = decl.getErasedSignature();
                String sig2 = mi.getSignature();
                if(sig1 == sig2)
                    return decl;
                if(sig1.substring(0, sig1.indexOf(')')).equals(sig2.substring(0, sig2.indexOf(')'))))
                    return decl;
            }
        }
        return null;
    }

    public static boolean isMain(MethodDefinition md) {
        return md.getName().equals("main") && md.isPublic() && md.isStatic() && md.getErasedSignature().startsWith("([Ljava/lang/String;)");
    }

    public static boolean isSideEffectFree(MethodReference mr) {
        if(isPure(mr))
            return true;
        if(isEqualsMethod(mr) || isHashCodeMethod(mr))
            return true;
        TypeReference tr = mr.getDeclaringType();
        String sig = mr.getErasedSignature();
        String name = mr.getName();
        if(name.equals("toString") && sig.equals("()Ljava/lang/String;"))
            return true;
        switch(tr.getInternalName()) {
        case "java/util/Arrays":
            return name.equals("hashCode") || name.equals("equals") || name.equals("toString")
                || name.equals("binarySearch") || name.equals("stream")
                || name.equals("spliterator") || name.startsWith("deep")
                || name.startsWith("copyOf") || name.equals("asList");
        case "java/lang/Object":
            return mr.isConstructor();
        case "java/util/Collections":
            return name.equals("min") || name.equals("max") || name.startsWith("unmodifiable") || name.startsWith("synchronized")
                    || name.startsWith("empty");
        }
        if(Types.isCollection(tr)) {
            if(name.equals("contains") && sig.equals("(Ljava/lang/Object;)Z"))
                return true;
            if(name.equals("containsAll") && sig.equals("(Ljava/util/Collection;)Z"))
                return true;
            if(name.equals("isEmpty") && sig.equals("()Z"))
                return true;
            if(name.equals("size") && sig.equals("()I"))
                return true;
            if(Types.isInstance(tr, "java/util/List")) {
                if (name.equals("get") && sig.equals("(I)Ljava/lang/Object;") || name.equals("subList")
                    && sig.equals("(II)Ljava/util/List;"))
                    return true;
            }
            return false;
        }
        if(Types.isInstance(tr, "java/util/Map")) {
            if ((name.equals("containsKey") || name.equals("containsValue"))
                && sig.equals("(Ljava/lang/Object;)Z"))
                return true;
            if (name.equals("get") && sig.equals("(Ljava/lang/Object;)Ljava/lang/Object;"))
                return true;
        }
        return Types.isSideEffectFreeType(tr);
    }

    public static boolean isPure(MethodReference mr) {
        TypeReference tr = mr.getDeclaringType();
        if(Types.isBoxed(tr) || tr.getInternalName().startsWith("java/time/"))
            return true;
        if(tr.getInternalName().equals("java/util/String"))
            return !mr.getName().equals("getChars");
        if(tr.getInternalName().equals("java/lang/Math"))
            return !mr.getName().equals("random");
        if(tr.getInternalName().equals("java/util/Objects"))
            return true;
        if(tr.getInternalName().equals("java/util/Optional"))
            return mr.getName().equals("get") || mr.getName().equals("orElse") || mr.getName().equals("isPresent");
        return false;
    }

    public static boolean isThrower(MethodDefinition md) {
        MethodBody body = md.getBody();
        if(body == null)
            return false;
        for(Instruction inst : body.getInstructions()) {
            if(inst.hasLabel() || inst.getOpCode() == OpCode.RETURN || inst.getOpCode() == OpCode.ARETURN)
                return false;
            if(inst.getOpCode() == OpCode.ATHROW)
                return true;
        }
        // Actually should not go here for valid bytecode
        return false;
    }

    public static boolean isSerializationMethod(MethodDefinition md) {
        return SERIALIZATION_METHODS.contains(md.getName());
    }
}
