/*
 * Copyright (c) 2005, 2010, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * An extension of BinaryCode that allows code to be printed.
 * Includes printing of disassembled byte codes, exception info,
 * local variable and line number info.
 *
 */

package ilib;

import java.io.ByteArrayInputStream;
import java.io.CharArrayWriter;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.PrintStream;
import java.util.Map;
import java.util.HashMap;

class InjectBytecodes implements RuntimeConstants {

    private final ClassReaderWriter c;
    private final PrintStream output;
    private final int length;
    private final int[] map;
    private final byte[] widening;
    private final Injector[] before = new Injector[256];
    private final Injector[] after  = new Injector[256];
    private final String className;
    private final String methodName;
    private final Map<Integer,byte[]> snippets = new HashMap<>();

    private int pos;
    private int newPos;

    private class Span {
        final int delta;
        final int target;
        final int newDelta;
        final int newTarget;

        Span(int delta) {
            this.delta = delta;
            this.target = pos + delta;
            this.newTarget = map[target];
            this.newDelta = newTarget - newPos;
        }
    }

    /**
     * Constructor
     */
    InjectBytecodes(ClassReaderWriter c, int length,
                    String className, String methodName) {
        this.c = c;
        this.output = System.out;
        this.length = length;
        this.map = new int[length + 1];
        this.widening = new byte[length + 1];
        this.className = className;
        this.methodName = methodName;
        c.markLocalPositionStart();
        for (int i = 0; i <= length; ++i) {
            map[i] = i;
        }
    }

    public void inject(int at, byte[] newCode) {
        snippets.put(new Integer(at), newCode);
        trace("external ");
        inject(at, newCode.length);
    }

    private void inject(int at, int len) {
        if (Inject.verbose) {
            traceln("Injecting " + len + " at " + at);
        }
        for (int i = at; i <= length; ++i) {
            map[i] += len;
        }
    }

    private void widen(int at, int len) {
        int delta = len - widening[at];
        if (Inject.verbose) {
            traceln();
            traceln("Widening to " + len + " at " + at);
        }
        inject(c.localPosition(), delta);  // inject at end of instruction
        widening[at] = (byte)len;          // mark at beginning of instruction
    }

    public void injectBefore(int code, Injector inj) {
        before[code] = inj;
    }

    public void injectAfter(int code, Injector inj) {
        after[code] = inj;
    }

    private void trace(String str) {
        if (Inject.verbose) {
            output.print(str);
        }
    }

    private void traceln(String str) {
        if (Inject.verbose) {
            output.println(str);
        }
    }

    private void traceln() {
        if (Inject.verbose) {
            output.println();
        }
    }

    private void trace(int i) {
        if (Inject.verbose) {
            output.print(i);
        }
    }

    /**
     * Print an integer so that it takes 'length' characters in
     * the output.  Temporary until formatting code is stable.
     */
    private void traceFixedWidthInt(int x, int length) {
        if (Inject.verbose) {
            CharArrayWriter baStream = new CharArrayWriter();
            PrintWriter pStream = new PrintWriter(baStream);
            pStream.print(x);
            String str = baStream.toString();
            for (int cnt = length - str.length(); cnt > 0; --cnt)
                trace(" ");
            trace(str);
        }
    }

    void adjustOffsets() throws IOException {
        if (Inject.verbose) {
            traceln();
            traceln("Method " + methodName);
            traceln();
        }
        c.rewind();
        while (c.localPosition() < length) {
            insertAtInstruction();
        }
        trace("Searching for adjustments...");
        c.rewind();
        while (c.localPosition() < length) {
            if (!adjustInstruction()) {
                c.rewind();
                traceln();
                traceln("Restarting adjustments after change...");
            }
        }
        // write the new bytecodes
        traceln();
        traceln();
        trace("Writing new code...");
        c.rewind();
        while (c.localPosition() < length) {
            writeInstruction();
        }
        if (!snippets.isEmpty()) {
            throw new Error("not all snippets written");
        }
    }

    /**
     * Walk one instruction inserting instrumentation at specified instructions
     */
    private void insertAtInstruction() throws IOException {
        pos = c.localPosition();
        int opcode = c.readU1();
        if (opcode == opc_wide) {
            // no support for instrumenting wide instructions
            int wopcode = c.readU1();
            int lvIndex = c.readU2();
            switch (wopcode) {
            case opc_aload: case opc_astore:
            case opc_fload: case opc_fstore:
            case opc_iload: case opc_istore:
            case opc_lload: case opc_lstore:
            case opc_dload: case opc_dstore:
            case opc_ret:
                break;

            case opc_iinc:
                c.readS2();
                break;
            default:
                throw new Error("Invalid wide opcode: " + wopcode);
            }
        } else {
            Injector inj;

            inj = before[opcode];
            if (inj != null) {
                inject(pos, inj.bytecodes(className, methodName, pos));
            }

            switch (opcode) {
            case opc_tableswitch:{
                int header = (pos+1+3) & (~3);        // 4byte boundry
                c.skip(header - (pos+1));             // skip old padding

                c.readU4();
                int low = c.readU4();
                int high = c.readU4();
                c.skip((high+1-low) * 4);
                break;
            }

            case opc_lookupswitch:{
                int header = (pos+1+3) & (~3);        // 4byte boundry
                c.skip(header - (pos+1));             // skip padding

                c.readU4();
                int npairs = c.readU4();
                c.skip(npairs * 8);
                break;
            }

            default: {
                int instrLen = opcLengths[opcode];
                c.skip(instrLen-1);
            }
            }
            inj = after[opcode];
            if (inj != null) {
                pos = c.localPosition();
                inject(pos, inj.bytecodes(className, methodName, pos));
            }
        }
    }

    /**
     * Walk one instruction adjusting for insertions
     */
    private boolean adjustInstruction() throws IOException {
        pos = c.localPosition();
        newPos = map[pos];
        int opcode = c.readU1();
        if (Inject.verbose) {
            traceln();
            traceFixedWidthInt(pos, 4);
            traceFixedWidthInt(newPos, 4);
            trace(" ");
        }
        if (opcode == opc_wide) {
            int wopcode = c.readU1();
            int lvIndex = c.readU2();
            if (Inject.verbose) {
                trace(opcNames[wopcode] + "_w ");
            }
            switch (wopcode) {
            case opc_aload: case opc_astore:
            case opc_fload: case opc_fstore:
            case opc_iload: case opc_istore:
            case opc_lload: case opc_lstore:
            case opc_dload: case opc_dstore:
            case opc_ret:
                trace(lvIndex);
                break;

            case opc_iinc:
                int constVal = c.readS2();
                if (Inject.verbose) {
                    trace(lvIndex + " " + constVal);
                }
                break;
            default:
                throw new Error("Invalid wide opcode: " + wopcode);
            }
        } else {
            if (Inject.verbose) {
                trace(opcNames[opcode]);
            }
            switch (opcode) {

            case opc_tableswitch:{
                int widened = widening[pos];
                int header = (pos+1+3) & (~3);        // 4byte boundry
                int newHeader = (newPos+1+3) & (~3);  // 4byte boundry

                c.skip(header - (pos+1));             // skip old padding

                Span defaultSkip = new Span(c.readU4());
                int low = c.readU4();
                int high = c.readU4();
                if (Inject.verbose) {
                    trace(" " + low + " to " + high);
                    trace(": default= [was] " + defaultSkip.target);
                    trace(" [now] " + defaultSkip.newTarget);
                    for (int i = low; i <= high; ++i) {
                        Span jump = new Span(c.readU4());
                        traceln("");
                        trace('\t');
                        traceFixedWidthInt(i, 5);
                        trace(": " + jump.newTarget);
                    }
                } else {
                    c.skip((high+1-low) * 4);
                }
                int newPadding = newHeader - newPos;
                int oldPadding = header - pos;
                int deltaPadding = newPadding - oldPadding;
                if (widened != deltaPadding) {
                    widen(pos, deltaPadding);
                    return false;       // cause restart
                }
                break;
            }

            case opc_lookupswitch:{
                int widened = widening[pos];
                int header = (pos+1+3) & (~3);        // 4byte boundry
                int newHeader = (newPos+1+3) & (~3);  // 4byte boundry

                c.skip(header - (pos+1));             // skip old padding

                Span defaultSkip = new Span(c.readU4());
                int npairs = c.readU4();
                if (Inject.verbose) {
                    trace(" npairs: " + npairs);
                    trace(": default= [was] " + defaultSkip.target);
                    trace(" [now] " + defaultSkip.newTarget);
                    for (int i = 0; i< npairs; ++i) {
                        int match = c.readU4();
                        Span jump = new Span(c.readU4());
                        traceln("");
                        trace('\t');
                        traceFixedWidthInt(match, 5);
                        trace(": " + jump.newTarget);
                    }
                } else {
                    c.skip(npairs * 8);
                }
                int newPadding = newHeader - newPos;
                int oldPadding = header - pos;
                int deltaPadding = newPadding - oldPadding;
                if (widened != deltaPadding) {
                    widen(pos, deltaPadding);
                    return false;       // cause restart
                }
                break;
            }

            case opc_jsr: case opc_goto:
            case opc_ifeq: case opc_ifge: case opc_ifgt:
            case opc_ifle: case opc_iflt: case opc_ifne:
            case opc_if_icmpeq: case opc_if_icmpne: case opc_if_icmpge:
            case opc_if_icmpgt: case opc_if_icmple: case opc_if_icmplt:
            case opc_if_acmpeq: case opc_if_acmpne:
            case opc_ifnull: case opc_ifnonnull: {
                int widened = widening[pos];
                Span jump = new Span(c.readS2());
                if (widened == 0) {  // not yet widened
                    int newDelta = jump.newDelta;
                    if ((newDelta < -32768) || (newDelta > 32767)) {
                        switch (opcode) {
                        case opc_jsr: case opc_goto:
                            widen(pos, 2); // will convert to wide
                            break;
                        default:
                            widen(pos, 5); // will inject goto_w
                            break;
                        }
                        return false;       // cause restart
                    }
                }
                if (Inject.verbose) {
                    trace(" [was] " + jump.target + " ==> " +
                          " [now] " + jump.newTarget);
                }
                break;
            }

            case opc_jsr_w:
            case opc_goto_w: {
                Span jump = new Span(c.readU4());
                if (Inject.verbose) {
                    trace(" [was] " + jump.target +
                          " [now] " + jump.newTarget);
                }
                break;
            }

            default: {
                int instrLen = opcLengths[opcode];
                c.skip(instrLen-1);
                break;
            }
            }
        }
        return true;     // successful return
    }


    /**
     * Walk one instruction writing the transformed instruction.
     */
    private void writeInstruction() throws IOException {
        pos = c.localPosition();
        newPos = map[pos];
        byte[] newCode = snippets.remove(new Integer(pos));
        if (newCode != null) {
            traceln();
            traceFixedWidthInt(pos, 4);
            trace(" ... -- Inserting new code");
            c.writeBytes(newCode);
        }
        int opcode = c.readU1();
        if (Inject.verbose) {
            traceln();
            traceFixedWidthInt(pos, 4);
            traceFixedWidthInt(newPos, 4);
            trace(" ");
        }
        if (opcode == opc_wide) {
            int wopcode = c.readU1();
            int lvIndex = c.readU2();
            if (Inject.verbose) {
                trace(opcNames[wopcode] + "_w ");
            }
            c.writeU1(opcode);
            c.writeU1(wopcode);
            c.writeU2(lvIndex);
            switch (wopcode) {
            case opc_aload: case opc_astore:
            case opc_fload: case opc_fstore:
            case opc_iload: case opc_istore:
            case opc_lload: case opc_lstore:
            case opc_dload: case opc_dstore:
            case opc_ret:
                trace(lvIndex);
                break;

            case opc_iinc:
                int constVal = c.readS2();
                c.writeU2(constVal);  // ??? U vs S
                if (Inject.verbose) {
                    trace(lvIndex + " " + constVal);
                }
                break;
            default:
                throw new Error("Invalid wide opcode: " + wopcode);
            }
        } else {
            if (Inject.verbose) {
                trace(opcNames[opcode]);
            }
            switch (opcode) {

            case opc_tableswitch:{
                int header = (pos+1+3) & (~3);   // 4byte boundry
                int newHeader = (newPos+1+3) & (~3); // 4byte boundry

                c.skip(header - (pos+1));             // skip old padding

                Span defaultSkip = new Span(c.readU4());
                int low = c.readU4();
                int high = c.readU4();

                c.writeU1(opcode);                   // copy instruction
                for (int i = newPos+1; i < newHeader; ++i) {
                    c.writeU1(0);                    // write new padding
                }
                c.writeU4(defaultSkip.newDelta);
                c.writeU4(low);
                c.writeU4(high);

                if (Inject.verbose) {
                    trace(" " + low + " to " + high);
                    trace(": default= [was] " + defaultSkip.target);
                    trace(" [now] " + defaultSkip.newTarget);
                }
                for (int i = low; i <= high; ++i) {
                    Span jump = new Span(c.readU4());
                    c.writeU4(jump.newDelta);
                    if (Inject.verbose) {
                        traceln("");
                        trace('\t');
                        traceFixedWidthInt(i, 5);
                        trace(": " + jump.newTarget);
                    }
                }
                break;
            }

            case opc_lookupswitch:{
                int header = (pos+1+3) & (~3);   // 4byte boundry
                int newHeader = (newPos+1+3) & (~3); // 4byte boundry

                c.skip(header - (pos+1));             // skip old padding

                Span defaultSkip = new Span(c.readU4());
                int npairs = c.readU4();
                if (Inject.verbose) {
                    trace(" npairs: " + npairs);
                    trace(": default= [was] " + defaultSkip.target);
                    trace(" [now] " + defaultSkip.newTarget);
                }
                c.writeU1(opcode);                   // copy instruction
                for (int i = newPos+1; i < newHeader; ++i) {
                    c.writeU1(0);                    // write new padding
                }
                c.writeU4(defaultSkip.newDelta);
                c.writeU4(npairs);
                for (int i = 0; i< npairs; ++i) {
                    int match = c.readU4();
                    Span jump = new Span(c.readU4());
                    c.writeU4(match);
                    c.writeU4(jump.newDelta);
                    if (Inject.verbose) {
                        traceln("");
                        trace('\t');
                        traceFixedWidthInt(match, 5);
                        trace(": " + jump.newTarget);
                    }
                }
                break;
            }

            case opc_jsr: case opc_goto:
            case opc_ifeq: case opc_ifge: case opc_ifgt:
            case opc_ifle: case opc_iflt: case opc_ifne:
            case opc_if_icmpeq: case opc_if_icmpne: case opc_if_icmpge:
            case opc_if_icmpgt: case opc_if_icmple: case opc_if_icmplt:
            case opc_if_acmpeq: case opc_if_acmpne:
            case opc_ifnull: case opc_ifnonnull: {
                int widened = widening[pos];
                Span jump = new Span(c.readS2());
                int newOpcode = opcode;   // default to unchanged
                if (widened == 0) {        // not widened
                    c.writeU1(opcode);    // rewrite instruction
                    c.writeU2(jump.newDelta);
                } else if (widened == 2) { // wide form
                    switch (opcode) {
                    case opc_jsr:
                        newOpcode = opc_jsr_w;
                        break;
                    case opc_goto:
                        newOpcode = opc_jsr_w;
                        break;
                    default:
                        throw new Error("unexpected opcode: " +
                                   opcode);
                    }
                    c.writeU1(newOpcode);      // write wide instruction
                    c.writeU4(jump.newDelta);  // write new and wide delta
                } else if (widened == 5) {      // insert goto_w
                    switch (opcode) {
                    case opc_ifeq:
                        newOpcode = opc_ifne;
                        break;
                    case opc_ifge:
                        newOpcode = opc_iflt;
                        break;
                    case opc_ifgt:
                        newOpcode = opc_ifle;
                        break;
                    case opc_ifle:
                        newOpcode = opc_ifgt;
                        break;
                    case opc_iflt:
                        newOpcode = opc_ifge;
                        break;
                    case opc_ifne:
                        newOpcode = opc_ifeq;
                        break;
                    case opc_if_icmpeq:
                        newOpcode = opc_if_icmpne;
                        break;
                    case opc_if_icmpne:
                        newOpcode = opc_if_icmpeq;
                        break;
                    case opc_if_icmpge:
                        newOpcode = opc_if_icmplt;
                        break;
                    case opc_if_icmpgt:
                        newOpcode = opc_if_icmple;
                        break;
                    case opc_if_icmple:
                        newOpcode = opc_if_icmpgt;
                        break;
                    case opc_if_icmplt:
                        newOpcode = opc_if_icmpge;
                        break;
                    case opc_if_acmpeq:
                        newOpcode = opc_if_acmpne;
                        break;
                    case opc_if_acmpne:
                        newOpcode = opc_if_acmpeq;
                        break;
                    case opc_ifnull:
                        newOpcode = opc_ifnonnull;
                        break;
                    case opc_ifnonnull:
                        newOpcode = opc_ifnull;
                        break;
                    default:
                        throw new Error("unexpected opcode: " +
                                   opcode);
                    }
                    c.writeU1(newOpcode); // write inverse branch
                    c.writeU2(3 + 5);     // beyond if and goto_w
                    c.writeU1(opc_goto_w);// add a goto_w
                    c.writeU4(jump.newDelta);  // write new and wide delta
                } else {
                    throw new Error("unexpected widening");
                }

                if (Inject.verbose) {
                    trace(" [was] " + jump.target + " ==> " +
                          opcNames[newOpcode] +
                          " [now] " + jump.newTarget);
                }
                break;
            }

            case opc_jsr_w:
            case opc_goto_w: {
                Span jump = new Span(c.readU4());
                c.writeU1(opcode);        // instruction itself
                c.writeU4(jump.newDelta);
                if (Inject.verbose) {
                    trace(" [was] " + jump.target +
                          " [now] " + jump.newTarget);
                }
                break;
            }

            default: {
                int instrLen = opcLengths[opcode];
                c.writeU1(opcode);        // instruction itself
                c.copy(instrLen-1);
            }
            }
        }
    }

    /**
     * Copy the exception table for this method code
     */
    void copyExceptionTable() throws IOException {
        int tableLength = c.copyU2();   // exception table len
        if (tableLength > 0) {
            traceln();
            traceln("Exception table:");
            traceln(" from:old/new  to:old/new target:old/new type");
            for (int tcnt = tableLength; tcnt > 0; --tcnt) {
                int startPC = c.readU2();
                int newStartPC = map[startPC];
                c.writeU2(newStartPC);
                int endPC = c.readU2();
                int newEndPC = map[endPC];
                c.writeU2(newEndPC);
                int handlerPC = c.readU2();
                int newHandlerPC = map[handlerPC];
                c.writeU2(newHandlerPC);
                int catchType = c.copyU2();
                if (Inject.verbose) {
                    traceFixedWidthInt(startPC, 6);
                    traceFixedWidthInt(newStartPC, 6);
                    traceFixedWidthInt(endPC, 6);
                    traceFixedWidthInt(newEndPC, 6);
                    traceFixedWidthInt(handlerPC, 6);
                    traceFixedWidthInt(newHandlerPC, 6);
                    trace("    ");
                    if (catchType == 0)
                        traceln("any");
                    else {
                        traceln("" + catchType);
                    }
                }
            }
        }
    }

    /**
     * Copy the line number table for this method code
     */
    void copyLineNumberAttr() throws IOException {
        // name index already read
        c.copy(4);                      // attr len
        int tableLength = c.copyU2();   // line table len
        if (tableLength > 0) {
            if (Inject.verbose) {
                traceln();
                traceln("Line numbers for method " + methodName);
            }
            for (int tcnt = tableLength; tcnt > 0; --tcnt) {
                int startPC = c.readU2();
                int newStartPC = map[startPC];
                c.writeU2(newStartPC);
                int lineNumber = c.copyU2();
                if (Inject.verbose) {
                    traceln("   line " + lineNumber +
                            ": [was] " + startPC +
                            " [now] " + newStartPC);
                }
            }
        }
    }

    /**
     * Copy the local variable table for this method code
     */
    void copyLocalVarAttr() throws IOException {
        // name index already read
        c.copy(4);                      // attr len
        int tableLength = c.copyU2();   // local var table len
        if (tableLength > 0) {
            if (Inject.verbose) {
                traceln();
                traceln("Local variables for method " + methodName);
            }
            for (int tcnt = tableLength; tcnt > 0; --tcnt) {
                int startPC = c.readU2();
                int newStartPC = map[startPC];
                c.writeU2(newStartPC);
                int length = c.readU2();
                int endPC = startPC + length;
                int newEndPC = map[endPC];
                int newLength = newEndPC - newStartPC;
                c.writeU2(newLength);
                int nameIndex = c.copyU2();
                int descriptorIndex = c.copyU2();
                int index = c.copyU2();
                if (Inject.verbose) {
                    trace("   ");
                    trace(descriptorIndex);
                    trace(" ");
                    trace(nameIndex);
                    traceln("  pc= [was] " + startPC + " [now] " + newStartPC +
                            ", length= [was] " + length + " [now] " + newLength +
                            ", slot=" + index);
                }
            }
        }
    }
}
