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

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.ranides.asm.Opcodes;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.GenericClass;
import net.ranides.assira.reflection.InspectException;

import static net.ranides.assira.asm.AsmConst.*;
import net.ranides.assira.reflection.TypeFactory;
import net.ranides.assira.reflection.inspect.ClassInspector;
import net.ranides.assira.reflection.util.ReflectFormat;

/**
 * Klasa zawierająca metody konwertujące różne wartości używane przez ASM.
 * Na przykład konwertuje nazwy klas z formatu publicznego na wewnętrzny używany
 * przez JVM.
 * <table>
 *  <tr><td>FQName</td><td>full qualified name - nazwa klasy</td></tr>
 *  <tr><td>FQPName</td><td>full qualified path name - nazwa klasy, która jako separatorów używa / zamiast .</td></tr>
 *  <tr><td>VMName</td><td>nazwa która używana jest wewnętrznie przez JVM do opisu typu.</td></tr>
 * </table>
 * @author ranides
 */
public final class AsmUtils {

    private static final Pattern RE_VMTYPE = Pattern.compile("\\[?(L.+?;|[A-Z])");

    private AsmUtils() {
        // utility class
    }

    /**
     * Tworzy obiekt {@link GenericClass} na podstawie {@code VMName}. Metoda
     * obsługuje typy generyczne - jeśli w {@code VMName} zawarta jest informacja o parametrach
     * generic type'u, to zwrócony obiekt również wszystkie te informacje zawiera.
     * @param vmtype
     * @return
     */
    public static GenericClass vm2generic(String vmtype) {
        return new VMClass(vmtype);
    }

    /**
     * Konwertuje {@code FQName} klasy do {@code FQPName}.
     * @param fqname
     * @return
     */
    public static String class2p(String fqname) {
        return fqname.replace('.', '/');
    }

    /**
     * Zwraca {@code FQPName} podanej klasy.
     * @param clazz
     * @return
     */
    public static String class2p(Class<?> clazz) {
        return clazz.getName().replace('.', '/');
    }

    /**
     * Konwertuje {@code FQPName} klasy do {@code FQName}.
     * @param path
     * @return
     */
    public static String p2name(String path) {
        return path.replace('/', '.');
    }

    /**
     * Zwraca klasę o podanej {@code FQPName}.
     * @param path
     * @return
     */
    public static Class<?> p2class(String path) {
        return ClassInspector.forName(p2name(path));
    }

    /**
     * Zwraca klasę o podanej {@code VMName}. Uwaga - dla typów generycznych gubiona
     * jest informacja o generic parameters.
     * @param vmtype
     * @return
     */
    @SuppressWarnings("PMD")
    public static Class<?> vm2class(String vmtype) {
        if(vmtype.startsWith("[")) {
            return Array.newInstance(vm2class(vmtype.substring(1)), 0).getClass();
        }
        if(vmtype.startsWith("L") && vmtype.endsWith(";")) {
            return ClassInspector.forName(vmtype.substring(1,vmtype.length()-1).replace('/', '.'));
        }
        if("Z".equals(vmtype)) {
            return boolean.class;
        }
        if("B".equals(vmtype)) {
            return byte.class;
        }
        if("C".equals(vmtype)) {
            return char.class;
        }
        if("S".equals(vmtype)) {
            return short.class;
        }
        if("I".equals(vmtype)) {
            return int.class;
        }
        if("J".equals(vmtype)) {
            return long.class;
        }
        if("F".equals(vmtype)) {
            return float.class;
        }
        if("D".equals(vmtype)) {
            return double.class;
        }
        if("V".equals(vmtype)) {
            return void.class;
        }
        throw new InspectException("Illegall type: " + vmtype);
    }

    /**
     * Konwertuje podaną listę {@code VMName} na listę klas. Uwaga - funkcja
     * nie radzi sobie z typami generycznymi.
     * @param vmtypes
     * @return
     */
    public static List<Class<?>> vm2classlist(String vmtypes) {
            List<Class<?>> ret = new ArrayList<>();

            Matcher hit = RE_VMTYPE.matcher(vmtypes);
            while( hit.find() ) {
                ret.add( AsmUtils.vm2class( hit.group(0) ));
            }

            return ret;
        }

    /**
     * Zwraca {@code VMName} podanej klasy. Uwaga - klasa nie potrafi wyciągnąć
     * informacji o parametrach typów generycznych.
     * @param clazz
     * @return
     */
    @SuppressWarnings("PMD")
    public static String class2vm(Class<?> clazz) {
        if(boolean.class.equals(clazz)) {
            return "Z";
        }
        if(byte.class.equals(clazz)) {
            return "B";
        }
        if(char.class.equals(clazz)) {
            return "C";
        }
        if(short.class.equals(clazz)) {
            return "S";
        }
        if(int.class.equals(clazz)) {
            return "I";
        }
        if(long.class.equals(clazz)) {
            return "J";
        }
        if(float.class.equals(clazz)) {
            return "F";
        }
        if(double.class.equals(clazz)) {
            return "D";
        }
        if(void.class.equals(clazz)) {
            return "V";
        }
        if(clazz.isArray()) {
            return "[" + class2vm(clazz.getComponentType());
        }
        return "L"+clazz.getName().replace('.', '/')+";";
    }

    /**
     * Tworzy string, który reprezentuje wewnętrzną sygnaturę argumentów metody pobierającej
     * podane typy. Nie jest to pełna sygnatura metody - jest to tylko część
     * zawierająca listę argumentów. W sygnaturze nie znajdują się informacje o
     * parametrach typów generycznych - więc i metoda takich informacji nie umieszcza.
     * @param types
     * @return
     */
    public static String sig(Class<?> types[]) {
        StringBuilder builder = new StringBuilder();
        for(Class<?> param : types) {
            builder.append( class2vm(param) );
        }
        return builder.toString();
    }

    /**
     * Tworzy string, który reprezentuje wewnętrzną sygnaturę metody pobierającej
     * podane typy i zwracającej określony wynik. Nie jest to pełna sygnatura metody,
     * ponieważ nie zawiera nazwy. W sygnaturze nie znajdują się informacje o
     * parametrach typów generycznych - więc i metoda takich informacji nie umieszcza.
     * W sygnaturze JEST zawarty typ zwracanego wyniku - więc i metoda taką informację
     * dołączą.
     * @param ret
     * @param params
     * @return
     */
    public static String sig(Class<?> ret, Class<?> params[]) {
        StringBuilder builder = new StringBuilder();
        builder.append('(');
        for(Class<?> param : params) {
            builder.append( class2vm(param) );
        }
        builder.append(')');
        builder.append( AsmUtils.class2vm(ret) );
        return builder.toString();
    }

    /**
     * Tworzy string, który reprezentuje wewnętrzną sygnaturę metody.
     * Nie jest to pełna sygnatura metody, ponieważ nie zawiera nazwy.
     * @param method
     * @return
     */
    public static String sig(Method method) {
        return sig(method.getReturnType(), method.getParameterTypes());
    }
    
    /**
     * Tworzy string, który reprezentuje wewnętrzną sygnaturę metody.
     * Nie jest to pełna sygnatura metody, ponieważ nie zawiera nazwy.
     * @param method
     * @return
     */
    public static String sig(Constructor<?> ctor) {
        return sig(void.class, ctor.getParameterTypes());
    }

    /**
     * ASM przechowuje w formie maski bitowej wszystkie atrybuty elementu (klasy,
     * metody, pola). Funkcja konwertuje tę maskę na poprawną deklarację w języku
     * Java. Oprócz deklaracji w wyniku zawarty jest również komentarz, w którym
     * są umieszczane wszystkie atrybuty specjalne, których nie da się przedstawić
     * w formie deklaracji. Na przykład:
     * <code><pre>{@code / * synthethic * / private static }</pre></code>
     *
     * <p>
     * ASM przekazuje flagi w różnych metodach visitorów, np {@code visitInnerClass} albo
     * {@code visitField}.
     * </p>
     * @param access
     * @return
     */
    public static String access2decl(int access) {
        return new AccessBuilder(access).result();
    }

    /**
     * Zwraca nazwę stałej, która odpowiada przekazanemu typowi ramki.
     * @param frame
     * @return
     */
    public static String frame2decl(int frame) {
        switch (frame) {
            case 0: return "Opcodes.TOP";
            case 1: return "Opcodes.INTEGER";
            case 2: return "Opcodes.FLOAT";
            case 3: return "Opcodes.DOUBLE";
            case 4: return "Opcodes.LONG";
            case 5: return "Opcodes.NULL";
            case 6: return "Opcodes.UNINITIALIZED_THIS";
            default:
                throw new AssertionError("Unsupported frame int value: " + frame);
        }
    }

    /**
     * Zwraca nazwę stałej, która odpowiada przekazanej wersji ASM.
     * @param version
     * @return
     */
    public static String version2decl(int version) {
        switch (version) {
            case Opcodes.V1_1: return "V1_1";
            case Opcodes.V1_2: return "V1_2";
            case Opcodes.V1_3: return "V1_3";
            case Opcodes.V1_4: return "V1_4";
            case Opcodes.V1_5: return "V1_5";
            case Opcodes.V1_6: return "V1_6";
            case Opcodes.V1_7: return "V1_7";
            default:
                return Integer.toString(version, 10);
        }
    }

    static final class VMClass implements GenericClass {

        private final Class<?> type;
        private final String name;
        private final GenericClass[] params;

        VMClass(String sigType) {
            String vmType = sigType.replaceAll("\\*", "?;").replaceAll("[\\+\\-]", "");
            int offset = vmType.indexOf('<');
            if (offset != -1) {
                final String head = vmType.substring(0, offset) + ";";
                if (head.startsWith("L")) {
                    type = AsmUtils.vm2class(head);
                    name = type.getSimpleName();
                } else {
                    // to w zasadzie nie powinno wystąpić? jeśli coś nie jest obiektem
                    // to nie może mieć parametrów.
                    name = head;
                    type = null;
                }
                final String tail = vmType.substring(offset + 1, vmType.lastIndexOf('>'));
                params = parseTail(tail);
            } else if (vmType.startsWith("?")) {
                type = null;
                name = "?";
                params = TypeFactory.EMPTY_CLASSES;
            } else if (vmType.startsWith("T")) {
                type = null;
                name = vmType.substring(1, vmType.length() - 1);
                params = TypeFactory.EMPTY_CLASSES;
            } else {
                type = AsmUtils.vm2class(vmType);
                name = type.getSimpleName();
                params = TypeFactory.EMPTY_CLASSES;
            }
        }

        private GenericClass[] parseTail(String tail) {
            final List<GenericClass> parts = new LinkedList<>();
            int last = 0;
            int deep = 0;
            for (int i = 0; i < tail.length(); i++) {
                if (tail.charAt(i) == '<') {
                    deep++;
                }
                if (tail.charAt(i) == '>') {
                    deep--;
                }
                if (tail.charAt(i) == ';' && deep == 0) {
                    parts.add(new VMClass(tail.substring(last, i) + ";"));
                    last = i + 1;
                }
            }
            return parts.toArray(new GenericClass[parts.size()]);
        }

        @Override
        public String name() {
            return name;
        }
        @Override
        public Class<?> rawType() {
            return ValueUtils.or(type, Object.class);
        }
        @Override
        public GenericClass[] params() {
            return params;
        }
        @Override
        public boolean isParametrised() {
            return params!=null && params.length > 0;
        }
        @Override
        public boolean isResolved() {
            return type != null;
        }
        @Override
        public String toString() {
            return ReflectFormat.signature(this);
        }
        @Override
        public Type type() {
            if(!isResolved()) {
                return TypeFactory.UNKNOWN_TYPE;
            }
            if(!isParametrised()) {
                return rawType();
            }
            return TypeFactory.reflect(this);
        }
    }

    private static final class AccessBuilder {

        @SuppressWarnings("PMD.AvoidStringBufferField")
        private final StringBuilder builder;
        private final int access;
        private boolean empty;
        private boolean comment;


        AccessBuilder(final int access) {
            this.builder = new StringBuilder();
            this.empty = true;
            this.comment = false;
            this.access = access;
        }

        @SuppressWarnings({"PMD.NPathComplexity", "PMD.CyclomaticComplexity"})
        String result() {
            if (has(Opcodes.ACC_BRIDGE) && !has(ACC_CLASS | ACC_FIELD)) {
                comment("brigde");
            }
            if (has(Opcodes.ACC_VARARGS) && !has(ACC_CLASS | ACC_FIELD)) {
                comment("var-args");
            }
            if (has(Opcodes.ACC_SYNTHETIC)) {
                comment("synthethic");
            }
            if (has(Opcodes.ACC_DEPRECATED)) {
                comment("deprecated");
            }


            if (has(Opcodes.ACC_PUBLIC)) {
                append("public");
            }
            if (has(Opcodes.ACC_PRIVATE)) {
                append("private");
            }
            if (has(Opcodes.ACC_PROTECTED)) {
                append("protected");
            }
            if (has(Opcodes.ACC_STATIC)) {
                append("static");
            }
            if (has(Opcodes.ACC_ABSTRACT)) {
                append("abstract");
            }
            if (has(Opcodes.ACC_FINAL)) {
                append("final");
            }
            if (has(Opcodes.ACC_SYNCHRONIZED) && !has(ACC_CLASS)) {
                append("synchronized");
            }
            if (has(Opcodes.ACC_VOLATILE) && has(ACC_FIELD)) {
                append("volatile");
            }
            if (has(Opcodes.ACC_TRANSIENT) && has(ACC_FIELD)) {
                append("transient");
            }
            if (has(Opcodes.ACC_NATIVE) && !has(ACC_CLASS | ACC_FIELD)) {
                append("native");
            }
            if (has(Opcodes.ACC_STRICT)) {
                append("strictfp");
            }

            if (has(Opcodes.ACC_ENUM) && has(ACC_CLASS | ACC_FIELD | ACC_INNER)) {
                append("enum");
            } else if (has(Opcodes.ACC_ANNOTATION) && has(ACC_CLASS | ACC_INNER)) {
                append("@interface");
            } else if (has(Opcodes.ACC_INTERFACE)) {
                append("interface");
            } else if (has(ACC_CLASS | ACC_INNER)) {
                append("class");
            }
            return builder.toString();
        }

        private void append(CharSequence value) {
            if (comment) {
                builder.append(" */ ");
            }
            if (!empty) {
                builder.append(' ');
            }
            builder.append(value);
            empty = false;
            comment = false;
        }

        private void comment(CharSequence value) {
            if (!comment) {
                builder.append("/* ");
            } else {
                builder.append(' ');
            }
            builder.append(value);
            comment = true;
            empty = true;
        }

        private boolean has(int value) {
            return (access & value) != 0;
        }
    }

}
