/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.asm;

import net.ranides.assira.reflection.util.MemberUtils;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
import net.ranides.asm.ClassReader;
import net.ranides.asm.ClassVisitor;
import net.ranides.asm.Label;
import net.ranides.asm.MethodVisitor;
import net.ranides.asm.Opcodes;
import net.ranides.assira.collection.list.ListAdapter;
import net.ranides.assira.generic.Function;
import net.ranides.assira.generic.Wrapper;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.reflection.InspectException;
import net.ranides.assira.reflection.GenericParam;
import net.ranides.assira.text.StringUtils;

/**
 * @author ranides
 */
public final class MethodInspectorVM {

    private MethodInspectorVM() {
        // utility class
    }

    /*
     * @todo (prev # 5) refactor ASM
     *
     * @todo (prev # 6) MethodVarReader
     * niech visitor zbiera za jednym zamachem informacje o wszystkich metodach
     * i trzyma w mapie. niech visitor radzi sobie z metodami przeładowanymi
     *
     * @todo (prev # 6) getSignature(Method)
     *      Type Class#method(Type1 a, Type2 b, Type c)
     *
     *  @todo (prev # 6) getFQSignature(Metod)
     *      FQType FQClass#method(FQType1 a, FQType2 b, FQType c)
     */

    /**
     * Ustala typ zwracany przez przekazaną metodę, zwracając w miarę możliwości
     * szczegółowe informacje o typach generycznych.
     * @param method
     * @return
     */
    public static GenericClass typeof(final Method method) {
        try {
            final String smethod = AsmUtils.sig(method);
            final Wrapper<String> result = Wrapper.make();
            final ClassReader creader = new ClassReader(method.getDeclaringClass().getName());

            creader.accept(new ClassVisitor(Opcodes.ASM4) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String signature, String generic, String[] exceptions) {
                    if(name.equals(method.getName()) && smethod.equals(signature)) {
                        result.set(generic);
                    }
                    return null;
                }
            },0);
            if(null == result.get()) {
                return TypeFactory.construct(method.getReturnType());
            } else {
                return AsmUtils.vm2generic(result.get().substring(result.get().lastIndexOf(")")+1));
            }
        } catch (IOException cause) {
            throw new InspectException(cause);
        }
    }
    
    public static Class<?> typeof(Constructor<?> ctor) {
        return ctor.getDeclaringClass();
    }

    /**
     * Zwraca listę argumentów pobieranych przez metodę. Reflection API pozwala
     * jedynie ustalić typy argumentów, bez nazw. Nie wspiera również typów generycznych.
     * Poniższa metoda umożliwia ustalenie zarówno nazw, jak i parametrów typów
     * generycznych.
     * @param method
     * @return
     */
    public static List<GenericParam> params(Method method) {
        List<GenericParam> list = ivariables(
            method.getDeclaringClass(), 
            method.getName(), 
            AsmUtils.sig(method)
        );
        if(MemberUtils.isStatic(method)) {
            return list.subList(0, method.getParameterTypes().length);
        } else {
            return list.subList(1, method.getParameterTypes().length+1);
        }
    }
    
    public static List<GenericParam> params(Constructor<?> ctor) {
        List<GenericParam> list = ivariables(
            ctor.getDeclaringClass(), "<init>", 
            AsmUtils.sig(ctor)
        );
        return list.subList(1, ctor.getParameterTypes().length+1);
    }
    
    /**
     * Zwraca listę zmiennych zdefiniowanych w metodzie. Argumenty funkcji również
     * są na poziomie bytecode'u reprezentowane jako zmienne. Kolejność zmiennych
     * odpowiada kolejności deklaracji.
     * @param method
     * @return
     */
    public static List<GenericParam> variables(Method method) {
        List<GenericParam> list = ivariables(
            method.getDeclaringClass(), 
            method.getName(), 
            AsmUtils.sig(method)
        );
        if(MemberUtils.isStatic(method)) {
            return list.subList(method.getParameterTypes().length, list.size());
        } else {
            return list.subList(method.getParameterTypes().length+1, list.size());
        }
    }
    
    public static List<GenericParam> variables(final Constructor<?> ctor) {
        List<GenericParam> list = ivariables(
            ctor.getDeclaringClass(), 
            "<init>", 
            AsmUtils.sig(ctor)
        );
        return list.subList(ctor.getParameterTypes().length+1, list.size());
    }
    
    private static List<GenericParam> ivariables(Class<?> clazz, final String cname, final String sig) {
        try {
            final TreeMap<Integer, VMVariable> variables = new TreeMap<>();
            final ClassReader creader = new ClassReader(clazz.getName());

            creader.accept(new ClassVisitor(Opcodes.ASM4) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String signature, String generic, String[] exceptions) {
                    if(!cname.equals(name) ||! sig.equals(signature)) {
                        return null;
                    }
                    return new MethodVisitor(Opcodes.ASM4) {
                        @Override
                        public void visitLocalVariable(String name, String type, String generic, Label start, Label end, int index) {
                            variables.put(index, new VMVariable(name, type, generic));
                        }
                    };
                }
            },0);
            return new ArrayList<GenericParam>(variables.values());
        } catch (IOException cause) {
            throw new InspectException(cause);
        }
    }

    /**
     * Zwraca listę wszystkich metod zdefiniowanych w podanej klasie.
     * @param clazz
     * @return
     */
    public static List<VMMethod> methods(final Class<?> clazz) {
        // @todo (prev # 3) use that in MethodInspector
        // it should be better than reflection #getDeclaredMethods
        // it returns parameter names & better generic types
        try {
            final List<VMMethodBuilder> target = new ArrayList<>(clazz.getDeclaredMethods().length+2);

            new ClassReader(clazz.getName()).accept(new ClassVisitor(Opcodes.ASM4) {
                @Override
                public MethodVisitor visitMethod(int access, String name, String signature, String generic, String[] exceptions) {
                    final VMMethodBuilder vmm = new VMMethodBuilder(clazz, name, signature, access);
                    target.add(vmm);
                    return new MethodVisitor(Opcodes.ASM4) {
                        @Override
                        public void visitLocalVariable(String name, String type, String generic, Label start, Label end, int index) {
                            vmm.vars.put(index, new VMVariable(name, type, generic));
                        }
                    };
                }
            },0);

            return ListAdapter.apply(target, new Function<VMMethod, VMMethodBuilder>(){
                @Override
                public VMMethod apply(VMMethodBuilder source) {
                    return source.construct();
                }
            });

        } catch (IOException cause) {
            throw new InspectException(cause);
        }
    }

    private static final class VMMethodBuilder {
        public final Class<?> clazz;
        public final String name;
        public final GenericClass result;
        public final TreeMap<Integer, VMVariable> vars;
        public final List<Class<?>> vmparams;
        public final int access;

        public VMMethodBuilder(Class<?> clazz, String name, String sig, int access) {
            this.clazz = clazz;
            this.name = name;
            this.result = AsmUtils.vm2generic( sig.substring(sig.lastIndexOf(")")+1) );
            this.vars = new TreeMap<>();
            this.vmparams = AsmUtils.vm2classlist( StringUtils.between(sig, "(", ")") );
            this.access = access;
        }

        public VMMethod construct() {
            return new VMMethod(this);
        }

    }

    public static final class VMMethod {

        public final Class<?> owner;
        public final String name;
        public final GenericClass result;
        public final List<GenericParam> params;
        public final int modifiers;

        VMMethod(VMMethodBuilder builder) {
            int n = builder.vmparams.size();
            this.params = new ArrayList<>(n);
            int shift = Modifier.isStatic(builder.access) ? 0 : 1;

            for(int i=0; i<n; i++) {
                if( builder.vars.containsKey(i) ) {
                    this.params.add( builder.vars.get(i+shift) );
                } else {
                    this.params.add( new VMVariable(i, builder.vmparams.get(i)) );
                }
            }

            this.name = builder.name;
            this.result = builder.result;
            this.modifiers = builder.access;
            this.owner = builder.clazz;
        }

//
//        @Override
//        public boolean isSynthetic() {
//            return 0 != (modifiers & Opcodes.ACC_SYNTHETIC);
//        }

    }

    private static final class VMVariable implements GenericParam {

        public final String name;
        public final GenericClass type;

        public VMVariable(int index, Class<?> clazz) {
            this.name = "v" + index;
            this.type = TypeFactory.construct(clazz);
        }

        public VMVariable(String name, String type, String generic) {
            this.name = name;
            if(null != generic) {
                this.type = AsmUtils.vm2generic(generic);
            } else {
                this.type = TypeFactory.construct(AsmUtils.vm2class(type));
            }
        }

        @Override
        public String name() {
            return name;
        }

        @Override
        public GenericClass type() {
            return type;
        }

        @Override
        public String toString() {
            return type() + " " + name;
        }


    }

}
