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

import net.ranides.asm.Attribute;
import net.ranides.asm.Handle;
import net.ranides.asm.Label;
import net.ranides.asm.Opcodes;
import net.ranides.asm.util.ASMifiable;
import net.ranides.asm.util.Printer;
import net.ranides.assira.text.StringUtils;

/**
 * Generator kodu źródłowego ASM. Przeznaczony przede wszystkim do współpracy z
 * {@link TraceClassVisitor}, ale może być również użyty do generowania kodu ręcznie.
 * Wynik można pobrać albo za pomocą metody {@link getText} odziedziczonej po
 * {@link Printer} albo za pomocą metody {@link result}, która zwraca {@code String}
 * zamiast listy.
 *
 * @author ranides
 */
@SuppressWarnings("unchecked")
public class AsmPrinter extends Printer {

    private static final int CLOSE = -1;

    private static final int OPEN  = +1;

    private final AsmFormat format;

    private final int id;

    private final AsmBuffer buffer;

    /**
     * Tworzy nowy generator z domyślnymi ustawieniami formatowania.
     * @see AsmFormat#getDefault() 
     */
    public AsmPrinter() {
        this(AsmFormat.getDefault());
    }

    /**
     * Tworzy nowy generator z podanymi ustawieniami formatowania.
     * @param format
     */
    public AsmPrinter(AsmFormat format) {
        this(format, format.getClassWriter(), 0);
    }

    protected AsmPrinter(AsmFormat format, String name, int id) {
        super( Opcodes.ASM4 );
        this.format = format;
        this.id = id;
        this.buffer = new AsmBuffer(format, name);
    }

    /**
     * Zwraca tekst wygenerowany przez AsmPrinter.
     * @return
     */
    public String result() {
        return StringUtils.join(getText());
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        buffer.reset();
        if(null != format.getPackageName(name)) {
            buffer.append("package #p;\n", name);
        }

        buffer.append("import net.ranides.asm.*;\n\n");
        buffer.append("@SuppressWarnings(\"PMD\")\n");
        buffer.append("public class #n implements Opcodes {\n\n", name);
        buffer.append("public static byte[] dump () throws Exception {\n\n");
        buffer.append("ClassWriter #w = new ClassWriter(0);\n");
        buffer.append("AnnotationVisitor #a;\n\n", 0);

        buffer.append("#w.visit(%v, %a, %c, %c, %c, %d);\n\n", version, access | AsmConst.ACC_CLASS, name, signature, superName, interfaces);
        text.add(buffer.toString());
    }

    @Override
    public void visitSource(String file, String debug) {
        addLine("#w.visitSource(%c, %c);\n", file, debug);
    }

    @Override
    public void visitOuterClass(String owner, String name, String desc) {
        addLine("#w.visitOuterClass(%i, %c, %c);\n", owner, name, desc);
    }

    @Override
    public AsmPrinter visitClassAnnotation(String desc, boolean visible) {
        return visitAnnotation(desc, visible);
    }

    @Override
    public void visitClassAttribute(Attribute attr) {
        visitAttribute(attr);
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        addLine("#w.visitInnerClass(%c, %c, %c, %a);\n", name, outerName, innerName, access | AsmConst.ACC_INNER);
    }

    @Override
    public AsmPrinter visitField(int access, String name, String desc, String signature, Object value) {
        indent(OPEN);
        addLine("FieldVisitor #f = #w.visitField(%a, %c, %c, %c, %c);", access | AsmConst.ACC_FIELD, name, desc, signature, value);
        AsmPrinter inner = innerVisit(format.getFieldVisitor(), 0);
        indent(CLOSE);
        return inner;
    }

    @Override
    public AsmPrinter visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        indent(OPEN);
        addLine("MethodVisitor #m = #w.visitMethod(%a, %c, %c, %c, %d);", access, name, desc, signature, exceptions);
        AsmPrinter inner = innerVisit(format.getMethodVisitor(), 0);
        indent(CLOSE);
        return inner;
    }

    @Override
    public void visitClassEnd() {
        addLine("#w.visitEnd();\n");
        addLine("return #w.toByteArray();");
        indent(CLOSE);
        indent(CLOSE);
    }

    @Override
    public void visit(final String name, final Object value) {
        addLine("#a.visit(%c, %c);", id, name, value);
    }

    @Override
    public void visitEnum(String name, String desc, String value) {
        addLine("#a.visitEnum(%c, %c, %c);", id, name, desc, value);
    }

    @Override
    public AsmPrinter visitAnnotation(String name, String desc) {
        indent(OPEN);
        addLine("AnnotationVisitor #a = #a.visitAnnotation(%c, %c);", id+1, id, name, desc);
        AsmPrinter inner = innerVisit( format.getAnnotationVisitor(id+1), id+1);
        indent(CLOSE);
        return inner;
    }

    @Override
    public AsmPrinter visitArray(final String name) {
        indent(OPEN);
        addLine("AnnotationVisitor #a = #a.visitArray(%c);", id+1, id, name);
        AsmPrinter inner = innerVisit(format.getAnnotationVisitor(id+1), id + 1);
        indent(CLOSE);
        return inner;
    }

    @Override
    public void visitAnnotationEnd() {
        addLine("#a.visitEnd();", id);
    }

    @Override
    public AsmPrinter visitFieldAnnotation(String desc, boolean visible) {
        return visitAnnotation(desc, visible);
    }

    @Override
    public void visitFieldAttribute(final Attribute attr) {
        visitAttribute(attr);
    }

    @Override
    public void visitFieldEnd() {
        addLine("#v.visitEnd();");
    }

    @Override
    public AsmPrinter visitAnnotationDefault() {
        indent(OPEN);
        addLine("#a = #v.visitAnnotationDefault();", 0);
        AsmPrinter asmifier = innerVisit(format.getAnnotationVisitor(0), 0);
        indent(CLOSE);
        return asmifier;
    }

    @Override
    public AsmPrinter visitMethodAnnotation(final String desc, final boolean visible) {
        return visitAnnotation(desc, visible);
    }


    @Override
    public AsmPrinter visitParameterAnnotation(int parameter, String desc, boolean visible) {
        indent(OPEN);
        addLine("#a = #v.visitParameterAnnotation(%i, %c, %i);", 0, parameter, desc, visible);
        AsmPrinter inner = innerVisit(format.getAnnotationVisitor(0), 0);
        indent(CLOSE);
        return inner;
    }

    @Override
    public void visitMethodAttribute(final Attribute attr) {
        visitAttribute(attr);
    }

    @Override
    public void visitCode() {
        addLine("#v.visitCode();");
    }

    @Override
    public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
        buffer.reset();
        buffer.append("#v.visitFrame(");
        switch (type) {
            case Opcodes.F_NEW:
            case Opcodes.F_FULL:
                buffer.labels().declare(nLocal, local);
                buffer.labels().declare(nStack, stack);
                if (type == Opcodes.F_NEW) {
                    buffer.append("Opcodes.F_NEW, ");
                } else {
                    buffer.append("Opcodes.F_FULL, ");
                }
                buffer.append("%i, new Object[] { %F }, %i, new Object[] { %F }", nLocal, nLocal, local, nStack, nStack, stack);
                break;
            case Opcodes.F_APPEND:
                buffer.labels().declare(nLocal, local);
                buffer.append("Opcodes.F_APPEND, %i, new Object[] { %F }, 0, null", nLocal, nLocal, local);
                break;
            case Opcodes.F_CHOP:
                buffer.append("Opcodes.F_CHOP, %i, null, 0, null", nLocal);
                break;
            case Opcodes.F_SAME:
                buffer.append("Opcodes.F_SAME, 0, null, 0, null");
                break;
            case Opcodes.F_SAME1:
                buffer.labels().declare(1, stack);
                buffer.append("Opcodes.F_SAME1, 0, null, 1, new Object[] { %F }", 1, stack);
                break;
            default:
                throw new AssertionError("unsupported frame type: " + type);
        }
        buffer.append(");\n");
        text.add(buffer.toString());
    }

    @Override
    public void visitInsn(int opcode) {
        addLine("#v.visitInsn(%o);", opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        Object opstr = (opcode == Opcodes.NEWARRAY) ? TYPES[operand] : operand;
        addLine("#v.visitIntInsn(%o, %i);", opcode, opstr);
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        addLine("#v.visitVarInsn(%o, %i);", opcode, var);
    }

    @Override
    public void visitTypeInsn(int opcode, String type) {
        addLine("#v.visitTypeInsn(%o, %c);", opcode, type);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        addLine("#v.visitFieldInsn(%o, %c, %c, %c);", opcode, owner, name, desc);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc) {
        addLine("#v.visitMethodInsn(%o, %c, %c, %c);", opcode, owner, name, desc);
    }

    @Override
    public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
        addLine("#v.visitInvokeDynamicInsn(%c, %c, %c, new Object[]{ %C });", name, desc, bsm, bsmArgs);
    }

    @Override
    public void visitJumpInsn(int opcode, Label label) {
        buffer.reset();
        buffer.labels().declare(label);
        buffer.append("#v.visitJumpInsn(%o, %l);\n", opcode, label);
        text.add(buffer.toString());
    }

    @Override
    public void visitLabel(Label label) {
        buffer.reset();
        buffer.labels().declare(label);
        buffer.append("#v.visitLabel(%l);\n", label);
        text.add(buffer.toString());
    }

    @Override
    public void visitLdcInsn(Object cst) {
        addLine("#v.visitLdcInsn(%c);", cst);
    }

    @Override
    public void visitIincInsn(int var, int increment) {
        addLine("#v.visitIincInsn(%i, %i);", var, increment);
    }

    @Override
    public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
        buffer.reset();
        buffer.labels().declare(labels);
        buffer.labels().declare(dflt);
        buffer.append("#v.visitTableSwitchInsn(%i, %i, %l, new Label[] { %L });\n", min, max, dflt, labels);
        text.add(buffer.toString());
    }

    @Override
    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        buffer.reset();
        buffer.labels().declare(labels);
        buffer.labels().declare(dflt);
        buffer.append("#v.visitLookupSwitchInsn(%l, new int[] { %I }, new Label[] { %L });\n", dflt, keys, labels);
        text.add(buffer.toString());
    }

    @Override
    public void visitMultiANewArrayInsn(String desc, int dims) {
        addLine("#v.visitMultiANewArrayInsn(%c, %i);", desc, dims);
    }

    @Override
    public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
        buffer.reset();
        buffer.labels()
            .declare(start)
            .declare(end)
            .declare(handler);
        buffer.append("#v.visitTryCatchBlock(%l, %l, %l, %c);\n", start, end, handler, type);
        text.add(buffer.toString());
    }

    @Override
    public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
        addLine("#v.visitLocalVariable(%c, %c, %c, %l, %l, %i);", name, desc, signature, start, end, index);
    }

    @Override
    public void visitLineNumber(int line, Label start) {
        addLine("#v.visitLineNumber(%i, %l);", line, start);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        addLine("#v.visitMaxs(%i, %i);", maxStack, maxLocals);
    }

    @Override
    public void visitMethodEnd() {
        addLine("#v.visitEnd();");
    }

    public AsmPrinter visitAnnotation(String desc, boolean visible) {
        indent(OPEN);
        addLine("#a = #v.visitAnnotation(%c, %i);", 0, desc, visible);
        AsmPrinter inner = innerVisit(format.getAnnotationVisitor(0), 0);
        indent(CLOSE);
        return inner;
    }

    public void visitAttribute(Attribute attr) {
        if( format.writeComments() ) {
            addLine("// ATTRIBUTE %i", attr.type);
        }

        if (attr instanceof ASMifiable) {
            indent(OPEN);
            addLine("%m#v.visitAttribute(attr);", attr);
            indent(CLOSE);
        }
    }

    protected void indent(int indent) {
        for(int i=0; i<indent; i++) {
            text.add("{\n");
        }
        for(int i=indent; i<0; i++) {
            text.add("}\n");
        }
    }

    protected AsmPrinter innerVisit(String name, int id) {
        AsmPrinter inner = new AsmPrinter(format, name, id);
        text.add(inner.getText());
        return inner;
    }

    /**
     * @see AsmBuffer#append
     * @param format
     * @param arguments
     */
    protected void addLine(String format, Object... arguments) {
        buffer.reset();
        buffer.append(format, arguments);
        buffer.append("\n");
        text.add(buffer.toString());
    }

}
