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

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.asm.Handle;
import net.ranides.asm.Label;
import net.ranides.asm.Type;
import net.ranides.asm.util.ASMifiable;
import net.ranides.asm.util.Printer;

/**
 * <p>Klasa do generowania String'ów używanych przez AsmPrinter. Zawiera metodę
 * "append", która jest odpowiednikiem klasycznej funkcji "printf", ale obsługuje
 * zupełnie inne rodzaje formatowania, tzn jest specializowana z myślą o konstruowaniu
 * wstawek kodu ASM.
 * </p><p>
 * Klasa raczej pomocnicza, pozwala np sformatowanie liczby 0x3002D do tekstu "V1_1"
 * za pomocą: {@code new AsmBuffer().append("%v", 0x3002D).toString() }
 * </p>
 * @author ranides
 */
public class AsmBuffer {

    @SuppressWarnings("PMD.AvoidStringBufferField")
    private final StringBuilder builder;
    private final AsmLabeler labeler = new Labeler();
    private final Map<Label, String> labels = new HashMap<>();
    private final Map<Label, String> labelsView = Collections.unmodifiableMap(labels);
    private final AsmFormat format;
    private final String visitor;

    /**
     * Tworzy nowy bufor na formatowany tekst, z domyślnymi ustawieniami formatowania.
     */
    public AsmBuffer() {
        this.builder = new StringBuilder();
        this.format = AsmFormat.getDefault();
        this.visitor = this.format.getClassWriter();
    }

    /**
     * Tworzy nowy bufor na formatowany tekst, z podanymi ustawieniami formatowania.
     * @param format
     */
    public AsmBuffer(AsmFormat format) {
        this(format, format.getClassWriter());
    }

    /**
     * Tworzy nowy bufor na formatowany tekst, z podanymi ustawieniami formatowania.
     * Nazwa writer'a używanego w wygenerowanym kodzie nie jest pobierana z
     * konfiguracji, lecz jest przekazywana jawnie jako drugi argument.
     * @param format
     * @param visitor
     */
    public AsmBuffer(AsmFormat format, String visitor) {
        this.builder = new StringBuilder();
        this.format = format;
        this.visitor = visitor;
    }

    @Override
    public String toString() {
        return builder.toString();
    }

    /**
     * Czyści zawartość bufora.
     * @return this
     */
    public AsmBuffer reset() {
        builder.setLength(0);
        return this;
    }

    /**
     * Method with printf-like behaviour, but with ASM-specific format specifiers.
     * Formats passed {@code pattern} and appends result to buffer.
     * <table>
     *  <tr><th>symbol</th> <th>type</th> <th>description</th> <th>reference</th></tr>
     *  <tr><td>{@code %%}</td>  <td>-</td>  <td>percent sign</td><td>-</td></tr>
     *  <tr><td>{@code %a}</td>  <td>{@code int} </td><td>access </td><td> {@link AsmAccess#asText}</td></tr>
     *  <tr><td>{@code %c}</td>  <td>{@code Object}</td><td>constant </td><td> {@link AsmBuffer#appendConst}</td></tr>
     *  <tr><td>{@code %C}</td>  <td>{@code Object[]}</td><td>constants </td><td> {@link AsmBuffer#appendConsts}</td></tr>
     *  <tr><td>{@code %d}</td>  <td>{@code Object[]}</td><td>constants array</td><td> {@code String[]{...}}</td></tr>
     *  <tr><td>{@code %f}</td>  <td>{@code Object} </td><td>frame type </td><td> {@link #appendFrameType}</td></tr>
     *  <tr><td>{@code %F}</td>  <td>{@code int, Object[]} </td><td>frame types </td><td> {@link #appendFrameTypes}</td></tr>
     *  <tr><td>{@code %i}</td>  <td>{@code Object}</td><td>literal</td><td>-</td></tr>
     *  <tr><td>{@code %I}</td>  <td>{@code array}</td><td>literals</td><td>-</td></tr>
     *  <tr><td>{@code %l}</td>  <td>{@code Label} </td><td>label </td><td> {@link #appendLabel}</td></tr>
     *  <tr><td>{@code %L}</td>  <td>{@code Label[]} </td><td>labels </td><td> {@link #appendLabels}</td></tr>
     *  <tr><td>{@code %m}</td>  <td>{@code ASMifiable} </td><td>asmify </td><td> {@link ASMifiable}</td></tr>
     *  <tr><td>{@code %o}</td>  <td>{@code int} </td><td>opcode </td><td> {@link Printer#OPCODES}</td></tr>
     *  <tr><td>{@code %s}</td>  <td>{@code String}</td><td>quoted string </td><td> {@link AsmBuffer#appendStr}</td></tr>
     *  <tr><td>{@code %S}</td>  <td>{@code String[]}</td><td>quoted strings </td><td> {@link AsmBuffer#appendStrs}</td></tr>
     *  <tr><td>{@code %v}</td>  <td>{@code int} </td><td>asm version </td><td> {@link AsmifierUtils#getAsmVersion}</td></tr>
     *  <tr><th colspan="4"></th></tr>
     *  <tr><td>{@code ##}</td>  <td>-</td><td>hash sign</td><td></td></tr>
     *  <tr><td>{@code #a}</td>  <td>{@code int}</td><td>annotation visitor with id</td><td></td></tr>
     *  <tr><td>{@code #c}</td>  <td>-</td><td>current visitor</td><td></td></tr>
     *  <tr><td>{@code #f}</td>  <td>-</td><td>field visitor</td><td>{@link AsmFormat#getFieldVisitor}</td></tr>
     *  <tr><td>{@code #m}</td>  <td>-</td><td>method visitor</td><td>{@link AsmFormat#getMethodVisitor}</td></tr>
     *  <tr><td>{@code #n}</td>  <td>{@code String}</td><td>simple name</td><td>{@link AsmFormat#getSimpleName}</td></tr>
     *  <tr><td>{@code #p}</td>  <td>{@code String}</td><td>package name</td><td>{@link AsmFormat#getPackageName}</td></tr>
     *  <tr><td>{@code #w}</td>  <td>-</td><td>class writer</td><td>{@link AsmFormat#getClassWriter}</td></tr>
     * </table>
     * @param pattern
     * @param arguments
     * @return
     */
    @SuppressWarnings("PMD")
    public AsmBuffer append(String pattern, Object... arguments) {
        int current = 0;
        for(int i=0, n=pattern.length(); i<n; i++) {
            if( '#' == pattern.charAt(i) && i+1<n ) {
                switch(pattern.charAt(++i)) {
                    case '#':
                        builder.append('#');
                        break;
                    case 'a':
                        builder.append(format.getAnnotationVisitor( (Integer)arguments[current++] ));
                        break;
                    case 'f':
                        builder.append(format.getFieldVisitor());
                        break;
                    case 'm':
                        builder.append(format.getMethodVisitor());
                        break;
                    case 'n':
                        builder.append(format.getSimpleName((String)arguments[current++]));
                        break;
                    case 'p':
                        builder.append(format.getPackageName((String)arguments[current++]));
                        break;
                    case 'v':
                        builder.append(visitor);
                        break;
                    case 'w':
                        builder.append(format.getClassWriter());
                        break;
                    default:
                        builder.append("#?");
                        break;
                }
                continue;
            }
            if( '%' == pattern.charAt(i) && i+1<n ) {
                switch(pattern.charAt(++i)) {
                    case '%':
                        builder.append('%');
                        break;
                    case 'i':
                        builder.append(arguments[current++]);
                        break;
                    case 'I':
                        appendRaws(arguments[current++]);
                        break;
                    case 's':
                        appendStr(arguments[current++]);
                        break;
                    case 'S':
                        appendStrs(arguments[current++]);
                        break;
                    case 'c':
                        appendConst(arguments[current++]);
                        break;
                    case 'C':
                        appendConsts(arguments[current++]);
                        break;
                    case 'v':
                        builder.append(AsmUtils.version2decl((Integer)arguments[current++]) );
                        break;
                    case 'a':
                        builder.append(AsmUtils.access2decl((Integer)arguments[current++]) );
                        break;
                    case 'l':
                        appendLabel( arguments[current++] );
                        break;
                    case 'L':
                        appendLabels( arguments[current++] );
                        break;
                    case 'o':
                        builder.append( Printer.OPCODES[ (Integer)arguments[current++] ] );
                        break;
                    case 'f':
                        appendFrameType( arguments[current++] );
                        break;
                    case 'F': {
                        int fn = (Integer)arguments[current];
                        Object[] fv = (Object[])arguments[current+1];
                        appendFrameTypes(fn, fv);
                        current += 2;
                        break;
                    }
                    case 'm': {
                        ASMifiable value = (ASMifiable)arguments[current++];
                        StringBuffer buffer = new StringBuffer();
                        value.asmify(buffer, "attr", labelsView);
                        builder.append(buffer.toString());
                        break;
                    }
                    case 'd': {
                        List<Object> list = ArrayUtils.wrap(arguments[current++]);
                        append(list.isEmpty() ? "null" : "new String[] { %C }", list);
                        break;
                    }
                    default:
                        builder.append("%?");
                        break;
                }
                continue;
            }

            builder.append(pattern.charAt(i));
        }
        return this;
    }

    private AsmBuffer appendRaws(Object value) {
        List<Object> list = ArrayUtils.wrap(value);
        for(int i=0, n=list.size(); i<n; i++) {
            if(i>0) { builder.append(", "); }
            builder.append(list.get(i));
        }
        return this;
    }

    @SuppressWarnings("PMD")
    private AsmBuffer appendStr(Object value) {
        final String text = String.valueOf(value);
        builder.append('\"');
        for (int i = 0; i < text.length(); ++i) {
            char c = text.charAt(i);
            if (c == '\n') {
                builder.append("\\n");
            } else if (c == '\r') {
                builder.append("\\r");
            } else if (c == '\\') {
                builder.append("\\\\");
            } else if (c == '"') {
                builder.append("\\\"");
            } else if (c < 0x20 || c > 0x7f) {
                builder.append("\\u");
                if (c < 0x10) {
                    builder.append("000");
                } else if (c < 0x100) {
                    builder.append("00");
                } else if (c < 0x1000) {
                    builder.append('0');
                }
                builder.append(Integer.toString(c, 16));
            } else {
                builder.append(c);
            }
        }
        builder.append('\"');
        return this;
    }

    private AsmBuffer appendStrs(Object value) {
        List<Object> list = ArrayUtils.wrap(value);
        for(int i=0, n=list.size(); i<n; i++) {
            if(i>0) { builder.append(", "); }
            appendStr( String.valueOf(list.get(i)) );
        }
        return this;
    }

    private AsmBuffer appendConsts(Object value) {
        List<Object> list = ArrayUtils.wrap(value);
        for(int i=0, n=list.size(); i<n; i++) {
            if(i>0) { builder.append(", "); }
            appendConst( list.get(i) );
        }
        return this;
    }

    @SuppressWarnings("PMD")
    private void appendConst(Object value) {
        if (value == null) {
            builder.append("null");
        } else if (value instanceof String) {
            appendStr( (String)value );
        } else if (value instanceof Type) {
            builder.append("Type.getType(\"");
            builder.append(((Type) value).getDescriptor());
            builder.append("\")");
        } else if (value instanceof Handle) {
            builder.append("new Handle(");
            Handle handle = (Handle) value;
            builder.append("Opcodes.").append( Printer.HANDLE_TAG[handle.getTag()]).append(", \"");
            builder.append(handle.getOwner()).append("\", \"");
            builder.append(handle.getName()).append("\", \"");
            builder.append(handle.getDesc()).append("\")");
        } else if (value instanceof Byte) {
            builder.append("new Byte((byte)").append(value).append(')');
        } else if (value instanceof Boolean) {
            builder.append(((Boolean) value).booleanValue() ? "Boolean.TRUE" : "Boolean.FALSE");
        } else if (value instanceof Short) {
            builder.append("new Short((short)").append(value).append(')');
        } else if (value instanceof Character) {
            int c = ((Character) value).charValue();
            builder.append("new Character((char)").append(c).append(')');
        } else if (value instanceof Integer) {
            builder.append("new Integer(").append(value).append(')');
        } else if (value instanceof Float) {
            builder.append("new Float(\"").append(value).append("\")");
        } else if (value instanceof Long) {
            builder.append("new Long(").append(value).append("L)");
        } else if (value instanceof Double) {
            builder.append("new Double(\"").append(value).append("\")");
        } else if (value instanceof byte[]) {
            byte[] array = (byte[]) value;
            builder.append("new byte[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]);
            }
            builder.append('}');
        } else if (value instanceof boolean[]) {
            boolean[] array = (boolean[]) value;
            builder.append("new boolean[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]);
            }
            builder.append('}');
        } else if (value instanceof short[]) {
            short[] array = (short[]) value;
            builder.append("new short[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append("(short)").append(array[i]);
            }
            builder.append('}');
        } else if (value instanceof char[]) {
            char[] array = (char[]) value;
            builder.append("new char[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append("(char)").append((int) array[i]);
            }
            builder.append('}');
        } else if (value instanceof int[]) {
            int[] array = (int[]) value;
            builder.append("new int[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]);
            }
            builder.append('}');
        } else if (value instanceof long[]) {
            long[] array = (long[]) value;
            builder.append("new long[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]).append('L');
            }
            builder.append('}');
        } else if (value instanceof float[]) {
            float[] array = (float[]) value;
            builder.append("new float[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]).append('f');
            }
            builder.append('}');
        } else if (value instanceof double[]) {
            double[] array = (double[]) value;
            builder.append("new double[] {");
            for (int i = 0; i < array.length; i++) {
                builder.append(i == 0 ? "" : ",").append(array[i]).append('d');
            }
            builder.append('}');
        }
    }

    private void appendFrameTypes(final int size, final Object[] frames) {
        for (int i = 0; i < size; ++i) {
            if(i>0) { builder.append(", "); }
            appendFrameType(frames[i]);
        }
    }

    private void appendFrameType(Object value) {
        if (value instanceof String) {
            appendConst(value);
        }
        else if (value instanceof Integer) {
            builder.append( AsmUtils.frame2decl((Integer)value) );
        }
        else {
            appendLabel(value);
        }
    }

    private AsmBuffer appendLabel(Object value) {
        builder.append(labels.get( (Label)value ));
        return this;
    }

    private AsmBuffer appendLabels(Object value) {
        Label[] values = (Label[])value;
        for(int i=0, n=values.length; i<n; i++) {
            if(i>0) { builder.append(", "); }
            appendLabel(values[i]);
        }
        return this;
    }

    /**
     * Zwraca obiekt przechowujący informacje o wszystkich etykietach
     * zdefiniowanych w kodzie generowanym przez AsmBuffer. {@link AsmLabeler}
     * może być również używany do definiowania nowych etykiet w bieżącym AsmBufferze.
     * @return
     */
    public AsmLabeler labels() {
        return labeler;
    }

    private final class Labeler extends AsmLabeler {

        @Override
        public AsmLabeler declare(Label value) {
            if( !labels.containsKey(value) ) {
                String name = format.getLabelName( labels.size() );
                labels.put(value, name);
                append("Label %i = new Label();\n", name);
            }
            return this;
        }

        @Override
        public Map<Label, String> map() {
            return labelsView;
        }

    }

}
