package net.ranides.assira.reflection.asm;

import lombok.experimental.UtilityClass;
import net.ranides.asm.Opcodes;
import net.ranides.asm.Type;

import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.text.StrBuilder;
import net.ranides.assira.text.StringUtils;

import java.util.Arrays;
import java.util.Collection;

import static net.ranides.asm.Opcodes.*;

/**
 * Utility class with helper methods for ASM
 * It is compatible with reflective IClass interfaces.
 */
@UtilityClass
public class AsmSupport {

    private static final Type TYPE_OBJECT = Type.getType(Object.class);

    /**
     * Replaces illegal character in classname
     *
     * @param name name
     * @return string
     */
    public static String escape(String name) {
        return name.replace('/',':').replace('.',':').replace('$','#');
    }

    /**
     * Selects specialization of opcode for specified type
     *
     * @param oc opcode
     * @param type operand type
     * @return opcode
     */
    public static int opcode(int oc, IClass<?> type) {
        if(type.attribute(IAttribute.SYMBOLIC)) {
            return TYPE_OBJECT.getOpcode(oc);
        } else {
            return Type.getType(type.raw()).getOpcode(oc);
        }
    }

    /**
     * Deduces opcode for method call.
     *
     * @param owner owner
     * @param method method
     * @return opcode
     */
    public static int vcall(IClass<?> owner, IMethod method) {
        if(method!=null && method.attribute(IAttribute.STATIC)) {
            return INVOKESTATIC;
        }
        return owner.attribute(IAttribute.INTERFACE) ? INVOKEINTERFACE : INVOKEVIRTUAL;
    }

    /**
     * Deduces opcode for method call.
     *
     * @param method method
     * @return opcode
     */
    public static int vcall(IMethod method) {
        return vcall(method.parent(), method);
    }

    /**
     * Deduces opcode for method call.
     *
     * @param owner owner
     * @return opcode
     */
    public static int vcall(IClass<?> owner) {
        return vcall(owner, null);
    }

    /**
     * Converts type into type path (e.g. slashes instead of dot separators)
     *
     * @param clazz clazz
     * @return string
     */
    public static String path(IClass<?> clazz) {
        if(clazz.attributes().has(IAttribute.SYMBOLIC)) {
            return clazz.name().replace('.', '/');
        } else {
            return Type.getType(clazz.raw()).getInternalName();
        }
    }

    /**
     * Converts all types into corresponding type paths
     *
     * @param types types
     * @return paths
     */
    public static String[] path(IClass<?>[] types) {
        return Arrays.stream(types).map(AsmSupport::path).toArray(String[]::new);
    }

    /**
     * Converts all types into corresponding type paths
     *
     * @param types types
     * @return paths
     */
    public static String[] path(Iterable<IClass<?>> types) {
        return IteratorUtils.stream(types.iterator()).map(AsmSupport::path).toArray(String[]::new);
    }

    /**
     * Converts all types into corresponding type paths
     *
     * @param types types
     * @return paths
     */
    public static String[] path(Collection<IClass<?>> types) {
        return types.stream().map(AsmSupport::path).toArray(String[]::new);
    }

    /**
     * Converts type into type descriptor (used inside JVM)
     *
     * @param clazz clazz
     * @return descriptor
     */
    public static String vm(IClass<?> clazz) {
        if(clazz.attributes().has(IAttribute.SYMBOLIC)) {
            return "L"+ path(clazz)+";";
        } else {
            return Type.getType(clazz.raw()).getDescriptor();
        }
    }

    /**
     * Converts types into type descriptors (used inside JVM)
     *
     * @param types types
     * @return descriptors
     */
    public static String vm(Iterable<IClass<?>> types) {
        return StringUtils.join(types, "", AsmSupport::vm);
    }

    /**
     * Generates type descriptor for method with specified arguments and return type
     *
     * @param params params
     * @param ret ret
     * @return descriptor
     */
    public static String vm(Iterable<IClass<?>> params, IClass<?> ret) {
        return new StrBuilder()
            .open("(", ")", "").append(params, AsmSupport::vm).close()
            .append(vm(ret)).toString();
    }

    /**
     * Generates type descriptor for method
     *
     * @param method method
     * @return descriptor
     */
    public static String vm(IMethod method) {
        return Type.getType(method.reflective()).getDescriptor();
    }

    /**
     * Generates type descriptor for constructor method
     *
     * @param ctor ctor
     * @return descriptor
     */
    public static String vm(IConstructor<?> ctor) {
        return Type.getType(ctor.reflective()).getDescriptor();
    }

    /**
     * Generates generic type signature for type
     *
     * @param clazz clazz
     * @return generic descriptor
     */
    public static String sig(IClass<?> clazz) {
        return isGeneric(clazz) ? sig0(clazz) : null;
    }

    /**
     * Generates generic type signatures for types
     *
     * @param types types
     * @return generic descriptors
     */
    public static String sig(Iterable<IClass<?>> types) {
        return isGeneric(types) ? StringUtils.join(types, "", AsmSupport::sig0) : null;
    }

    /**
     * Generates generic type descriptor for method
     *
     * @param params params
     * @param ret ret
     * @return generic descriptor
     */
    public static String sig(Iterable<IClass<?>> params, IClass<?> ret) {
        return isGeneric(params) || isGeneric(ret) ? sig0(params, ret) : null;
    }

    /**
     * Generates generic type descriptor for method
     *
     * @param method method
     * @return generic descriptor
     */
    public static String sig(IMethod method) {
        return isGeneric(method) ? sig0(method.arguments().types(), method.returns()) : null;
    }

    /**
     * Generates generic type descriptor for constructor method
     *
     * @param ctor ctor
     * @return generic descriptor
     */
    public static String sig(IConstructor<?> ctor) {
        return isGeneric(ctor) ? sig0(ctor.arguments().types(), IClass.VOID) : null;
    }

    private static String sig0(Iterable<IClass<?>> params, IClass<?> ret) {
        return new StrBuilder()
            .open("(", ")", "")
            .append(params, AsmSupport::sig0)
            .close()
            .append(sig0(ret))
            .toString();
    }

    private static String sig0(IClass<?> clazz) {
        if(!isGeneric(clazz)) {
            return vm(clazz);
        }
        return new StrBuilder()
            .append("L")
            .append(path(clazz))
            .open("<", ">", "")
            .append(clazz.params(), AsmSupport::sig0)
            .close()
            .toString();
    }

    private static boolean isGeneric(Iterable<? extends IElement> elements) {
        return IteratorUtils.stream(elements.iterator()).anyMatch(AsmSupport::isGeneric);
    }

    private static boolean isGeneric(IElement element) {
        return element.attributes().has(IAttribute.PARAMETERIZED);
    }

    /**
     * Returns type tag for array with specified component.
     * type tag is used by NEWARRAY opcode.
     *
     * @param raw array component
     * @return array type tag
     */
    public static int arrayType(Class<?> raw) {
        if(boolean.class.equals(raw)) {
            return Opcodes.T_BOOLEAN;
        }
        if(char.class.equals(raw)) {
            return Opcodes.T_CHAR;
        }
        if(float.class.equals(raw)) {
            return Opcodes.T_FLOAT;
        }
        if(double.class.equals(raw)) {
            return Opcodes.T_DOUBLE;
        }
        if(byte.class.equals(raw)) {
            return Opcodes.T_BYTE;
        }
        if(short.class.equals(raw)) {
            return Opcodes.T_SHORT;
        }
        if(int.class.equals(raw)) {
            return Opcodes.T_INT;
        }
        if(long.class.equals(raw)) {
            return Opcodes.T_LONG;
        }
        throw new IllegalArgumentException("Invalid array component: " + raw);
    }

    /**
     * Returns size allocated on VM stack for specified type.
     *
     * @param type type
     * @return int
     */
    public static int sizeof(IClass<?> type) {
        return type.attribute(IAttribute.SYMBOLIC) ? 1 : Type.getType(type.raw()).getSize();
    }

}
