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

package net.ranides.assira.reflection.impl;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Arrays;

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.asm.Type;
import net.ranides.assira.collection.maps.Cache;
import net.ranides.assira.reflection.util.MemberTraits;
import net.ranides.assira.trace.ExceptionUtils;

public class CDParameters {
    
    private static final Cache<Class<?>, CDParameters> CACHE = new Cache<>();
    
    final Map<Constructor<?>,String[]> cParams = new HashMap<>();
    
    final Map<Method,String[]> mParams = new HashMap<>();
    
    CDParameters(Class<?> clazz) {
        try {
            ClassReader reader = reader(clazz);
            if(null != reader) {
                PNVisitor visitor = new PNVisitor(clazz);
                reader.accept(visitor, 0);
            }
        } catch (IOException cause) {
            throw ExceptionUtils.rethrow(cause);
        }
    }
    
    public static String[] getParams(Method method) {
        Class<?> type = method.getDeclaringClass();
        return CACHE.get(type, () -> new CDParameters(type)).mParams.get(method);
    }
    
    public static String[] getParams(Constructor<?> constructor) {
        Class<?> type = constructor.getDeclaringClass();
        return CACHE.get(type, () -> new CDParameters(type)).cParams.get(constructor);
    }
    
    static ClassReader reader(Class<?> clazz) throws IOException {
        ClassLoader cc = clazz.getClassLoader();
        if(null == cc) {
            return null;
        }
        try (InputStream in = cc.getResourceAsStream(clazz.getName().replace('.', '/') + ".class")) {
            return new ClassReader(in);
        }
    }

    private class PNVisitor extends ClassVisitor {
        
        private final Map<String,Method> mMethods = new HashMap<>();
        private final Map<String,Constructor<?>> mConstructors = new HashMap<>();

        public PNVisitor(Class<?> type) {
            super(Opcodes.ASM5);

            List<Method> methods = new ArrayList<Method>(Arrays.asList(type.getMethods()));
            methods.addAll(Arrays.asList(type.getDeclaredMethods()));
            for (Method method : methods) {
                mMethods.put(method.getName() + Type.getMethodDescriptor(method), method);
            }
            
            List<Constructor<?>> constructors = new ArrayList<>();
            constructors.addAll(Arrays.asList(type.getDeclaredConstructors()));
            for (Constructor<?> constructor : constructors) {
                Type[] types = new Type[constructor.getParameterTypes().length];
                for (int j = 0; j < types.length; j++) {
                    types[j] = Type.getType(constructor.getParameterTypes()[j]);
                }
                mConstructors.put(Type.getMethodDescriptor(Type.VOID_TYPE, types), constructor);
            }
        }

        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            Constructor<?> constructor = mConstructors.get(desc);
            if (constructor != null) {
                String[] names = new String[constructor.getParameterCount()];
                cParams.put(constructor, names);
                return new LMVisitor(names, 1);
            }
            
            Method method = mMethods.get(name + desc);
            if (method != null) {
                String[] names = new String[method.getParameterCount()];
                mParams.put(method, names);
                return new LMVisitor(names, MemberTraits.isStatic(method) ? 0 : 1);
            }
            return null;
       }
        
    }
    
    @SuppressWarnings("PMD.ArrayIsStoredDirectly")
    private static class LMVisitor extends MethodVisitor {
        
        private final String[] out;
        private final int offset;
        
        public LMVisitor(String[] out, int offset) {
            super(Opcodes.ASM5);
            this.out = out;
            this.offset = offset;
        }
        
        @Override
        public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
            int i = index - offset;
            if(i>=0 && i < out.length) {
                out[i] = name;
            }
        }
        
    };
}

