/*
 * Copyright (c) 2014, 2022 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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.
 */
package com.sun.tdk.jcov.instrument.asm;

import com.sun.tdk.jcov.instrument.DataMethodEntryOnly;
import com.sun.tdk.jcov.instrument.InstrumentationOptions;
import com.sun.tdk.jcov.instrument.InstrumentationParams;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.ClassVisitor;

import static org.objectweb.asm.Opcodes.*;

import org.objectweb.asm.tree.*;

/**
 * NativeWrappingMethodAdapter
 *
 * Write the native method (renamed with a prefix) by simply passing it on to
 * the forking adapter. The forking adapter is also handed a MethodNode so that
 * it builds a tree representation of the method. Add instrumentation and a call
 * to the native to make the wrapper.
 *
 * @author Robert Field
 */
class NativeWrappingMethodAdapter extends ForkingMethodAdapter {

    private final ClassVisitor cv;
    private final MethodNode methodNode;
    private final DataMethodEntryOnly dataMethod;
    private final InstrumentationParams params;

    NativeWrappingMethodAdapter(final MethodVisitor mv,
            final MethodNode methodNode, final ClassVisitor cv,
            final DataMethodEntryOnly dataMethod, final InstrumentationParams params) {
        super(mv, methodNode);
        this.cv = cv;
        this.dataMethod = dataMethod;
        this.methodNode = methodNode;
        this.params = params;
    }

    private int computeMaxLocals(String descriptor, int accessFlags) {
        int index = 1;
        int slot = 0;

        if ((accessFlags & ACC_STATIC) == 0) {
            ++slot;
        }
        char type;
        while ((type = descriptor.charAt(index)) != ')') {
            switch (type) {
                case 'B': // byte
                case 'C': // char
                case 'I': // int
                case 'S': // short
                case 'Z': // boolean
                case 'F': // float
                case 'L': // object
                case '[': // array
                    ++slot;
                    break;
                case 'D': // double
                case 'J': // long
                    slot += 2;
                    break;
                default:
                    break;
            }
            index = nextDescriptorIndex(descriptor, index);
        }

        return slot;
    }

    private int nextDescriptorIndex(String descriptor, int index) {
        switch (descriptor.charAt(index)) {
            case 'B': // byte
            case 'C': // char
            case 'I': // int
            case 'S': // short
            case 'Z': // boolean
            case 'F': // float
            case 'D': // double
            case 'J': // long
                return index + 1;
            case 'L': // object
                int i = index + 1;
                while (descriptor.charAt(i) != ';') {
                    ++i;
                }
                return i + 1;
            case '[': // array
                return nextDescriptorIndex(descriptor, index + 1);
            default:
                break;
        }
        throw new InternalError("should not reach here");
    }

    @Override
    public void visitEnd() {
        super.visitEnd();

        InsnList instructions = methodNode.instructions;
        String descriptor = methodNode.desc;

        // set max locals and max stack
        int maxLocals = computeMaxLocals(descriptor, methodNode.access);
        methodNode.maxLocals = maxLocals;
        methodNode.maxStack = maxLocals + 4;

        // set-up args

        int slot = 0;
        if ((methodNode.access & ACC_STATIC) == 0) {
            // "this"
            instructions.add(new VarInsnNode(ALOAD, slot));
            ++slot;
        }
        char type;
        int index;
        for (index = 1;
                (type = descriptor.charAt(index)) != ')';
                index = nextDescriptorIndex(descriptor, index)) {
            switch (type) {
                case 'B': // byte
                case 'C': // char
                case 'I': // int
                case 'S': // short
                case 'Z': // boolean
                    instructions.add(new VarInsnNode(ILOAD, slot));
                    ++slot;
                    break;
                case 'F': // float
                    instructions.add(new VarInsnNode(FLOAD, slot));
                    ++slot;
                    break;
                case 'D': // double
                    instructions.add(new VarInsnNode(DLOAD, slot));
                    slot += 2;
                    break;
                case 'J': // long
                    instructions.add(new VarInsnNode(LLOAD, slot));
                    slot += 2;
                    break;
                case 'L': // object
                case '[': // array
                    instructions.add(new VarInsnNode(ALOAD, slot));
                    ++slot;
                    break;
                default:
                    break;
            }
        }

        // call the wrapped version
        int invokeOp;
        if ((methodNode.access & ACC_STATIC) == 0) {
            invokeOp = INVOKEVIRTUAL;
        } else {
            invokeOp = INVOKESTATIC;
        }
        instructions.add(new MethodInsnNode(invokeOp, dataMethod.getParent().getFullname(),
                InstrumentationOptions.nativePrefix + dataMethod.getName(), dataMethod.getVmSignature()));

        // return correct type
        switch (descriptor.charAt(index + 1)) {
            case 'B': // byte
            case 'C': // char
            case 'I': // int
            case 'S': // short
            case 'Z': // boolean
                instructions.add(new InsnNode(IRETURN));
                break;
            case 'F': // float
                instructions.add(new InsnNode(FRETURN));
                break;
            case 'D': // double
                instructions.add(new InsnNode(DRETURN));
                break;
            case 'J': // long
                instructions.add(new InsnNode(LRETURN));
                break;
            case 'L': // object
            case '[': // array
                instructions.add(new InsnNode(ARETURN));
                break;
            case 'V': // void
                instructions.add(new InsnNode(RETURN));
                break;
            default:
                break;
        }

        String[] exceptions = (String[]) methodNode.exceptions.toArray(new String[0]);
        MethodVisitor mvn = cv.visitMethod(
                methodNode.access,
                methodNode.name,
                methodNode.desc,
                methodNode.signature,
                exceptions);

        //instrument wrapper
        EntryCodeMethodAdapter ecma = new EntryCodeMethodAdapter(mvn, dataMethod, params);
        methodNode.accept(ecma);
    }
}
