/*
 * Copyright 2016, Stuart Douglas, and individual contributors as indicated
 * by the @authors tag.
 *
 * 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 org.fakereplace.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class DescriptorUtils {

    public static Method getMethod(String name, String methodDesc, Class actual) {
        try {
            return actual.getMethod(name, argumentStringToClassArray(methodDesc, actual));
        } catch (Exception e) {
            // this should not happen
            throw new RuntimeException(e);
        }

    }

    public static String getDescriptor(Method method) {
        return "(" + classArrayToDescriptorString(method.getParameterTypes()) + ")" + classToStringRepresentation(method.getReturnType());
    }

    public static String getDescriptor(Constructor<?> method) {
        return "(" + classArrayToDescriptorString(method.getParameterTypes()) + ")V";
    }

    public static int maxLocalsFromParameters(String methodDescriptor) {
        String[] params = descriptorStringToParameterArray(methodDescriptor);
        int ret = params.length;
        for(String i : params) {
            if(i.equals("J") || i.equals("D")) {
                ++ret;
            }
        }
        return ret;
    }

    /**
     * returns an array of class types based on the method parameters this allows
     * getMethod to be called based on descriptor data We also pass the class
     * that the method We assume the descriptor is well formed
     *
     */
    public static Class<?>[] argumentStringToClassArray(String methodDescriptor, Class<?> methodClass) throws ClassNotFoundException {
        int i = 1; // char 0 is a '('
        List<Class<?>> classes = new ArrayList<>();
        int arraystart = -1;
        while (methodDescriptor.charAt(i) != ')') {
            Class<?> type;
            if (methodDescriptor.charAt(i) == '[') {
                if (arraystart == -1) {
                    arraystart = i;
                }
            } else {

                if (methodDescriptor.charAt(i) == 'L') {
                    i++;
                    int start = i;
                    while (methodDescriptor.charAt(i) != ';') {
                        ++i;
                    }
                    if (arraystart == -1) {
                        String className = methodDescriptor.substring(start, i).replace('/', '.');
                        try {
                            type = methodClass.getClassLoader().loadClass(className);
                        } catch (ClassNotFoundException e) {
                            type = Class.forName(className);
                        }
                    } else {
                        // apparently the array class syntax for
                        // getting an array class is the somewhat
                        // retarded Class.forName("[Ljava.lang.String;")
                        // a weird mix of internal and external formats
                        String className = methodDescriptor.substring(arraystart, i + 1).replace("/", ".");

                        try {
                            type = methodClass.getClassLoader().loadClass(className);
                        } catch (ClassNotFoundException e) {
                            type = Class.forName(className);
                        }
                    }

                } else {

                    if (arraystart == -1) {
                        type = primitiveType(methodDescriptor.charAt(i));
                    } else {
                        String className = methodDescriptor.substring(arraystart, i + 1);
                        try {
                            type = methodClass.getClassLoader().loadClass(className);
                        } catch (ClassNotFoundException e) {
                            type = Class.forName(className);
                        }
                    }

                }

                arraystart = -1;
                classes.add(type);
            }
            ++i;
        }
        Class<?>[] ret = new Class[classes.size()];
        for (i = 0; i < ret.length; ++i) {
            ret[i] = classes.get(i);
        }
        return ret;

    }

    /**
     * returns an array of String representations of the parameter types.
     * Primitives are returned as their native representations, while clases are
     * returned in the form Ljava/lang/Integer
     * <p>
     * this is so the class I can be determined from an int for example
     *
     */
    public static String[] descriptorStringToParameterArray(String methodDescriptor) {
        int i = 1; // char 0 is a '('
        List<String> ret = new ArrayList<>();
        int arraystart = -1;
        while (methodDescriptor.charAt(i) != ')') {
            String type;
            if (methodDescriptor.charAt(i) == '[') {
                if (arraystart == -1) {
                    arraystart = i;
                }
            } else {
                if (methodDescriptor.charAt(i) == 'L') {
                    int start = i;
                    i++;
                    while (methodDescriptor.charAt(i) != ';') {
                        ++i;
                    }
                    if (arraystart == -1) {
                        type = methodDescriptor.substring(start, i);
                    } else {
                        // apparently the array class syntax for
                        // getting an array class is the somewhat
                        // retarded Class.forName("[Ljava.lang.String;")
                        // a weird mix of internal and external formats
                        type = methodDescriptor.substring(arraystart, i);
                    }
                } else {
                    if (arraystart == -1) {
                        type = methodDescriptor.charAt(i) + "";
                    } else {
                        type = methodDescriptor.substring(arraystart, i + 1);

                    }
                }
                arraystart = -1;
                ret.add(type);
            }
            ++i;
        }
        String[] r = new String[ret.size()];
        for (int j = 0; j < ret.size(); ++j) {
            r[j] = ret.get(j);
        }
        return r;

    }

    public static String methodSignitureToDescriptor(Class<?> returnType, Class<?>... params) {
        return "(" + classArrayToDescriptorString(params) +
                ")" +
                classToStringRepresentation(returnType);
    }

    public static String getReturnType(String descriptor) {
        return descriptor.substring(descriptor.lastIndexOf(')') + 1);
    }

    public static String getReturnTypeInJvmFormat(String descriptor) {
        String rt = descriptor.substring(descriptor.lastIndexOf(')') + 1);
        if (rt.charAt(0) == 'L') {
            rt = rt.substring(1);
            rt = rt.substring(0, rt.length() - 1);
        }
        return rt;
    }

    /**
     * e.g. Ljava/lang/Object; to java/lang/Object
     *
     */
    public static String getTypeStringFromDescriptorFormat(String descriptor) {
        descriptor = descriptor.substring(1);
        descriptor = descriptor.substring(0, descriptor.length() - 1);
        return descriptor;
    }

    public static String getArgumentString(String descriptor) {
        return descriptor.substring(0, descriptor.lastIndexOf(')') + 1);
    }

    public static String classArrayToDescriptorString(Class<?>... params) {
        if (params == null) {
            return "";
        }
        StringBuilder ret = new StringBuilder();
        for (Class<?> c : params) {
            ret.append(classToStringRepresentation(c));
        }
        return ret.toString();
    }

    public static String classToStringRepresentation(Class<?> c) {
        if (void.class.equals(c)) {
            return "V";
        } else if (byte.class.equals(c)) {
            return "B";
        } else if (char.class.equals(c)) {
            return "C";
        } else if (double.class.equals(c)) {
            return "D";
        } else if (float.class.equals(c)) {
            return "F";
        } else if (int.class.equals(c)) {
            return "I";
        } else if (long.class.equals(c)) {
            return "J";
        } else if (short.class.equals(c)) {
            return "S";
        } else if (boolean.class.equals(c)) {
            return "Z";
        } else if (c.isArray()) {
            return c.getName().replace(".", "/");
        } else {
            return extToInt(c.getName());
        }
    }

    static Class<?> primitiveType(char c) {
        switch (c) {
            case 'V':
                return void.class;

            case 'B':
                return byte.class;

            case 'C':
                return char.class;

            case 'D':
                return double.class;

            case 'F':
                return float.class;

            case 'I':
                return int.class;

            case 'J':
                return long.class;

            case 'S':
                return short.class;

            case 'Z':
                return boolean.class;

        }
        return null;
    }

    public static String extToInt(String className) {
        String repl = className.replace(".", "/");
        return 'L' + repl + ';';
    }

    public static boolean isPrimitive(String descriptor) {
        if (descriptor.length() == 1) {
            return true;
        }
        return false;
    }

    public static boolean isWide(String descriptor) {
        if (!isPrimitive(descriptor)) {
            return false;
        }
        char c = descriptor.charAt(0);
        if (c == 'D' || c == 'J') {
            return true;
        }
        return false;
    }
}
