package net.ranides.assira.reflection.asm;

import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.ClassUtils;
import java.util.Arrays;

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

/**
 * Facade for ASM MethodVisitor.
 * It is compatible with reflective IClass interfaces.
 */
public class AsmMethodBuilder {

    private final AsmClassBuilder parent;

    private final MethodVisitor mv;

    private Label first;

    private Label last;

    private int vars;

    /**
     * Internal: called by AsmClassBuilder
     * It takes parent AsmClassBuilder and description of signature (name, attributes, etc)
     *
     * @param parent parent
     * @param attrs attrs
     * @param name name
     * @param params params
     * @param ret ret
     * @param exceptions exceptions
     */
    AsmMethodBuilder(AsmClassBuilder parent, IAttributes attrs, String name, Iterable<IClass<?>> params, IClass<?> ret, Iterable<IClass<?>> exceptions) {
        this.parent = parent;
        this.mv = parent.asm().visitMethod(attrs.reflective(), name, AsmSupport.vm(params, ret), AsmSupport.sig(params, ret), AsmSupport.path(exceptions));
        this.first = label();
    }

    /**
     * Returns internal ASM MethodVisitor
     *
     * @return MethodVisitor
     */
    public MethodVisitor asm() {
        return mv;
    }

    /**
     * Finishes method generation.
     *
     * @return parent class
     */
    public AsmClassBuilder end() {
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        return parent;
    }

    /**
     * Creates label to current code position
     *
     * @return created label
     */
    public Label label() {
        Label out = new Label();
        mv.visitLabel(out);
        return out;
    }

    /**
     * Creates label for current code position and assigns symbolic line number to it.
     * Code with assigned lines are easier to debug.
     *
     * @param line line
     * @return created label
     */
    public Label label(int line) {
        Label out = new Label();
        mv.visitLabel(out);
        mv.visitLineNumber(line, out);
        return out;
    }

    /**
     * Creates variable with specified type and name
     *
     * @param type type
     * @param name name
     * @return this
     */
    public AsmMethodBuilder var(IClass<?> type, String name) {
        if(last == null) {
            last = label();
        }
        mv.visitLocalVariable(name, AsmSupport.vm(type), AsmSupport.sig(type), first, last, vars++);
        return this;
    }

    /**
     * Creates variable with specified type and name.
     * It takes type & name information from IArgument class.
     *
     * @param argument description of variable
     * @return this
     */
    public AsmMethodBuilder var(IArgument argument) {
        return var(argument.type(), argument.name());
    }

    /**
     * Creates many variables at once, described by specified arguments
     *
     * @param arguments arguments
     * @return this
     */
    public AsmMethodBuilder var(IArguments arguments) {
        arguments.each(this::var);
        return this;
    }

    /**
     * OPCODE: PUSH specified value.
     * It deduces push specialization from value type.
     *
     * @param value value
     * @return this
     */
    public AsmMethodBuilder push(Object value) {
        return push(IClass.typefor(value), value);
    }

    /**
     * OPCODE: PUSH specified type and value
     *
     * @param type type
     * @param value value
     * @return this
     */
    public AsmMethodBuilder push(IClass<?> type, Object value) {
        if(null == value) {
            return ins(ACONST_NULL);
        }
        if(value instanceof IClass) {
            return ldc(Type.getType(((IClass<?>)value).raw()));
        }
        if(IClass.DOUBLE.isEquivalent(type)) {
            return pushDouble(((Number)value).doubleValue());
        }
        if(IClass.FLOAT.isEquivalent(type)) {
            return pushFloat(((Number)value).floatValue());
        }
        if(IClass.LONG.isEquivalent(type)) {
            return pushLong(((Number)value).longValue());
        }
        if(type.attribute(IAttribute.NUMBER)) {
            return pushInt((Number)value);
        }
        if(type.attribute(IAttribute.BOOLEAN)) {
            return pushInt((Boolean)value ? 1 : 0);
        }
        return ldc(value);
    }

    private AsmMethodBuilder pushDouble(Number value) {
        if(value.doubleValue() == 0.0) {
            return ins(DCONST_0);
        }
        if(value.doubleValue() == 1.0) {
            return ins(DCONST_1);
        }
        return ldc(value.doubleValue());
    }

    private AsmMethodBuilder pushFloat(Number value) {
        if(value.floatValue() == 0.0) {
            return ins(FCONST_0);
        }
        if(value.floatValue() == 1.0) {
            return ins(FCONST_1);
        }
        if(value.floatValue() == 2.0) {
            return ins(FCONST_2);
        }
        return ldc(value.floatValue());
    }

    private AsmMethodBuilder pushLong(long value) {
        if(value == 0) {
            return ins(LCONST_0);
        }
        if(value == 1) {
            return ins(LCONST_1);
        }
        return ldc(value);
    }

    private AsmMethodBuilder pushInt(Number value) {
        switch (value.intValue()) {
            case -1:
                return ins(ICONST_M1);
            case 0:
                return ins(ICONST_0);
            case 1:
                return ins(ICONST_1);
            case 2:
                return ins(ICONST_2);
            case 3:
                return ins(ICONST_3);
            case 4:
                return ins(ICONST_4);
            case 5:
                return ins(ICONST_5);
        }
        if(value.byteValue() == value.longValue()) {
            mv.visitIntInsn(BIPUSH, value.byteValue());
            return this;
        }
        if(value.shortValue() == value.longValue()) {
            mv.visitIntInsn(SIPUSH, value.shortValue());
            return this;
        }
        return ldc(value.intValue());
    }

    public void pop() {
        mv.visitInsn(POP);
    }

    /**
     * OPCODE: ILOAD
     *
     * @param type type
     * @param index index
     * @return this
     */
    public AsmMethodBuilder load(IClass<?> type, int index) {
        mv.visitVarInsn(AsmSupport.opcode(ILOAD, type), index);
        return this;
    }

    /**
     * OPCODE: ILOAD for types specified by arguments, starting from [index]
     *
     * @param arguments arguments
     * @param index index
     * @return this
     */
    public AsmMethodBuilder load(IArguments arguments, int index) {
        for(IArgument a : arguments) {
            load(a.type(), index++);
        }
        return this;
    }

    /**
     * OPCODE: ISTORE
     *
     * @param type type
     * @param index index
     * @return this
     */
    public AsmMethodBuilder store(IClass<?> type, int index) {
        mv.visitVarInsn(AsmSupport.opcode(ISTORE, type), index);
        return this;
    }

    /**
     * OPCODE: IALOAD
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder loadArray(IClass<?> type) {
        return ins(IALOAD, type);
    }

    /**
     * OPCODE: IASTORE
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder storeArray(IClass<?> type) {
        return ins(IASTORE, type);
    }

    /**
     * OPCODE: RETURN
     *
     * @return this
     */
    public AsmMethodBuilder returns() {
        mv.visitInsn(Opcodes.RETURN);
        return this;
    }

    /**
     * OPCODE: IRETURN
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder returns(IClass<?> type) {
        return ins(IRETURN, type);
    }

    /**
     * OPCODE: PUTFIELD
     *
     * @param owner owner
     * @param type type
     * @param name name
     * @return this
     */
    public AsmMethodBuilder putField(IClass<?> owner, IClass<?> type, String name) {
        return ocField(PUTFIELD, owner, type, name);
    }

    /**
     * OPCODE: PUTFIELD
     *
     * @param field field
     * @return this
     */
    public AsmMethodBuilder putField(IField field) {
        return ocField(PUTFIELD, field);
    }

    /**
     * OPCODE: GETFIELD
     *
     * @param owner owner
     * @param type type
     * @param name name
     * @return this
     */
    public AsmMethodBuilder getField(IClass<?> owner, IClass<?> type, String name) {
        return ocField(GETFIELD, owner, type, name);
    }

    /**
     * OPCODE: GETFIELD
     *
     * @param field field
     * @return this
     */
    public AsmMethodBuilder getField(IField field) {
        return ocField(GETFIELD, field);
    }

    /**
     * OPCODE: PUTSTATIC
     *
     * @param owner owner
     * @param type type
     * @param name name
     * @return this
     */
    public AsmMethodBuilder putStatic(IClass<?> owner, IClass<?> type, String name) {
        return ocField(PUTSTATIC, owner, type, name);
    }

    /**
     * OPCODE: PUTSTATIC
     *
     * @param field field
     * @return this
     */
    public AsmMethodBuilder putStatic(IField field) {
        return ocField(PUTSTATIC, field);
    }

    /**
     * OPCODE: GETSTATIC
     *
     * @param owner owner
     * @param type type
     * @param name name
     * @return this
     */
    public AsmMethodBuilder getStatic(IClass<?> owner, IClass<?> type, String name) {
        return ocField(GETSTATIC, owner, type, name);
    }

    /**
     * OPCODE: GETSTATIC
     *
     * @param field field
     * @return this
     */
    public AsmMethodBuilder getStatic(IField field) {
        return ocField(GETSTATIC, field);
    }

    private AsmMethodBuilder ocField(int oc, IField field) {
        return ocField(oc, field.parent(), field.type(), field.name());
    }

    private AsmMethodBuilder ocField(int oc, IClass<?> owner, IClass<?> type, String name) {
        mv.visitFieldInsn(oc, AsmSupport.path(owner), name, AsmSupport.vm(type));
        return this;
    }

    /**
     * OPCODE: INVOKESPECIAL
     *
     * @param constructor constructor
     * @return this
     */
    public AsmMethodBuilder invokeInit(IConstructor<?> constructor) {
        return invokeInit(constructor.parent(), constructor.arguments().types());
    }

    /**
     * OPCODE: INVOKESPECIAL
     *
     * @param owner owner
     * @param params params
     * @return this
     */
    public AsmMethodBuilder invokeInit(IClass<?> owner, Iterable<IClass<?>> params) {
        mv.visitMethodInsn(INVOKESPECIAL, AsmSupport.path(owner), "<init>", AsmSupport.vm(params, IClass.VOID), owner.attributes().has(IAttribute.INTERFACE));
        return this;
    }

    /**
     * OPCODE: INVOKESPECIAL
     *
     * @param owner owner
     * @param name name
     * @param params params
     * @param ret ret
     * @return this
     */
    public AsmMethodBuilder invokeSpecial(IClass<?> owner, String name, Iterable<IClass<?>> params, IClass<?> ret) {
        return invoke(INVOKESPECIAL, owner, name, params, ret);
    }

    /**
     * OPCODE: INVOKESPECIAL
     *
     * @param owner owner
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeSpecial(IClass<?> owner, IMethod method) {
        return invoke(INVOKESPECIAL, owner, method);
    }

    /**
     * OPCODE: INVOKESPECIAL
     *
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeSpecial(IMethod method) {
        return invoke(INVOKESPECIAL, method);
    }

    /**
     * OPCODE: virtual call (INVOKEINTERFACE, INVOKEVIRTUAL)
     * Opcode is deduced from owner type.
     *
     * @param owner owner
     * @param name name
     * @param params params
     * @param ret ret
     * @return this
     */
    public AsmMethodBuilder invokeVirtual(IClass<?> owner, String name, Iterable<IClass<?>> params, IClass<?> ret) {
        return invoke(AsmSupport.vcall(owner), owner, name, params, ret);
    }

    /**
     * OPCODE: virtual call (INVOKEINTERFACE, INVOKEVIRTUAL, INVOKESTATIC)
     * Opcode is deduced from method and owner.
     * Parameters are deduced from method.
     *
     * @param owner owner
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeVirtual(IClass<?> owner, IMethod method) {
        return invoke(AsmSupport.vcall(owner, method), owner, method);
    }

    /**
     * OPCODE: virtual call (INVOKEINTERFACE, INVOKEVIRTUAL, INVOKESTATIC)
     * Opcode, owner & parameters are deduced from method.
     *
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeVirtual(IMethod method) {
        return invoke(AsmSupport.vcall(method), method);
    }

    /**
     * OPCODE: INVOKESTATIC
     *
     * @param owner owner
     * @param name name
     * @param params params
     * @param ret ret
     * @return this
     */
    public AsmMethodBuilder invokeStatic(IClass<?> owner, String name, Iterable<IClass<?>> params, IClass<?> ret) {
        return invoke(INVOKESTATIC, owner, name, params, ret);
    }

    /**
     * OPCODE: INVOKESTATIC
     *
     * @param owner owner
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeStatic(IClass<?> owner, IMethod method) {
        return invoke(INVOKESTATIC, owner, method);
    }

    /**
     * OPCODE: INVOKESTATIC
     *
     * @param method method
     * @return this
     */
    public AsmMethodBuilder invokeStatic(IMethod method) {
        return invoke(INVOKESTATIC, method);
    }

    /**
     * OPCODE: invoke dynamic (more than one opcode)
     *
     * @param bootstrap bootstrap
     * @param name name
     * @param params params
     * @param ret ret
     * @param args args
     * @return this
     */
    public AsmMethodBuilder invokeDynamic(IMethod bootstrap, String name, Iterable<IClass<?>> params, IClass<?> ret, Object[] args) {
        Handle bootstrapHandle = new Handle(H_INVOKESTATIC, AsmSupport.path(bootstrap.parent()), bootstrap.name(), AsmSupport.vm(bootstrap), false);
        mv.visitInvokeDynamicInsn(name, AsmSupport.vm(params, ret), bootstrapHandle, args);
        return this;
    }

    private AsmMethodBuilder invoke(int oc, IMethod method) {
        return invoke(oc, method.parent(), method);
    }

    private AsmMethodBuilder invoke(int oc, IClass<?> owner, IMethod method) {
        return invoke(oc, owner, method.name(), method.arguments().types(), method.returns());
    }

    private AsmMethodBuilder invoke(int oc, IClass<?> owner, String name, Iterable<IClass<?>> params, IClass<?> ret) {
        mv.visitMethodInsn(oc, AsmSupport.path(owner), name, AsmSupport.vm(params, ret), owner.attributes().has(IAttribute.INTERFACE));
        return this;
    }

    /**
     * OPCODE: new (NEWARRAY, ANEWARRAY, NEW)
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder newObject(IClass<?> type) {
        if(type.attribute(IAttribute.ARRAY)) {
            if(type.component().attribute(IAttribute.PRIMITIVE)) {
                mv.visitIntInsn(NEWARRAY, AsmSupport.arrayType(type.component().raw()));
            } else {
                mv.visitTypeInsn(ANEWARRAY, AsmSupport.path(type.component()));
            }
        } else {
            mv.visitTypeInsn(NEW, AsmSupport.path(type));
        }
        return this;
    }

    /**
     * OPCODE: CHECKCAST
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder checkCast(IClass<?> type) {
        mv.visitTypeInsn(CHECKCAST, AsmSupport.path(type));
        return this;
    }

    /**
     * OPCODE: INSTANCEOF
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder isInstance(IClass<?> type) {
        mv.visitTypeInsn(INSTANCEOF, AsmSupport.path(type));
        return this;
    }

    /**
     * OPCODE: static call of boxing method
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder box(IClass<?> type) {
        IClass<?> wrapper = ClassUtils.box(type);
        return invokeStatic(wrapper, "valueOf", Arrays.asList(type), wrapper);
    }

    private AsmMethodBuilder ldc(Object v) {
        mv.visitLdcInsn(v);
        return this;
    }

    private AsmMethodBuilder ins(int oc) {
        mv.visitInsn(oc);
        return this;
    }

    private AsmMethodBuilder ins(int oc, IClass<?> type) {
        mv.visitInsn(AsmSupport.opcode(oc, type));
        return this;
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder add(IClass<?> type) {
        return ins(IADD, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder sub(IClass<?> type) {
        return ins(ISUB, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder div(IClass<?> type) {
        return ins(IDIV, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder rem(IClass<?> type) {
        return ins(IREM, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder neg(IClass<?> type) {
        return ins(INEG, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder shl(IClass<?> type) {
        return ins(ISHR, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder shr(IClass<?> type) {
        return ins(ISHL, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder ushr(IClass<?> type) {
        return ins(IUSHR, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder and(IClass<?> type) {
        return ins(IAND, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder or(IClass<?> type) {
        return ins(IOR, type);
    }

    /**
     * OPCODE: arithmetic
     *
     * @param type type of operands
     * @return this
     */
    public AsmMethodBuilder xor(IClass<?> type) {
        return ins(IXOR, type);
    }

    /**
     * OPCODE: DUP
     *
     * @return this
     */
    public AsmMethodBuilder dup() {
        return ins(DUP);
    }

    /**
     * OPCODE: DUP
     *
     * @param type type
     * @return this
     */
    public AsmMethodBuilder dup(IClass<?> type) {
        return dup(type, 0);
    }

    /**
     * OPCODE: DUP
     *
     * @param x x
     * @return this
     */
    public AsmMethodBuilder dup(int x) {
        return dup(IClass.INT, x);
    }

    /**
     * OPCODE: DUP
     *
     * @param type type
     * @param x x
     * @return this
     */
    public AsmMethodBuilder dup(IClass<?> type, int x) {
        switch (x) {
            case 0: ins(AsmSupport.sizeof(type) == 2 ? DUP2 : DUP);
            case 1: ins(AsmSupport.sizeof(type) == 2 ? DUP2_X1 : DUP_X1);
            case 2: ins(AsmSupport.sizeof(type) == 2 ? DUP2_X2 : DUP_X2);
        }
        throw new UnsupportedOperationException("DUP_X"+x + " does not exist.");
    }

    /**
     * OPCODE: widening or narrowing conversion from source to target
     * For example: D2F, D2L, F2D, etc ...
     *
     * @param source type
     * @param target type
     * @return this
     */
    public AsmMethodBuilder convert(IClass<?> source, IClass<?> target) {
        if(source.equals(IClass.DOUBLE)) {
            if(target.equals(IClass.FLOAT)) {
                return ins(D2F);
            }
            if(target.equals(IClass.LONG)) {
                return ins(D2L);
            }
            if(target.attribute(IAttribute.NUMBER)) {
                return ins(D2I);
            }
        }
        if(source.equals(IClass.FLOAT)) {
            if(target.equals(IClass.DOUBLE)) {
                return ins(F2D);
            }
            if(target.equals(IClass.LONG)) {
                return ins(F2D);
            }
            if(target.attribute(IAttribute.NUMBER)) {
                return ins(F2I);
            }
        }
        if(source.equals(IClass.LONG)) {
            if(target.equals(IClass.DOUBLE)) {
                return ins(L2D);
            }
            if(target.equals(IClass.FLOAT)) {
                return ins(L2F);
            }
            if(target.attribute(IAttribute.NUMBER)) {
                return ins(L2I);
            }
        }
        if(source.equals(IClass.INT)) {
            if(target.equals(IClass.BOOL)) {
                return ins(I2B);
            }
            if(target.equals(IClass.CHAR)) {
                return ins(I2C);
            }
            if(target.equals(IClass.DOUBLE)) {
                return ins(I2D);
            }
            if(target.equals(IClass.FLOAT)) {
                return ins(I2F);
            }
            if(target.equals(IClass.LONG)) {
                return ins(I2L);
            }
            if(target.equals(IClass.SHORT)) {
                return ins(I2S);
            }
        }
        throw new UnsupportedOperationException("Unsupported conversion " + source + " => " + target);
    }
}
