/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package dodola.anole.lib;

import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.LabelNode;

import java.util.List;

/**
 * A redirection is the part of an instrumented method that calls out to a different implementation.
 */
public abstract class Redirection {
    /**
     * The name of the method we redirect to.
     */
    private final String name;

    /**
     * The position where this redirection should happen.
     */
    private final LabelNode label;


    Redirection(LabelNode label, String name) {
        this.name = name;
        this.label = label;
    }

    /**
     * Adds the instructions to do a generic redirection.
     * <p/>
     * Note that the generated bytecode does not have a direct translation to code, but as an
     * example, the following code block gets inserted.
     * <code>
     * if ($change != null) {
     * $change.access$dispatch($name, new object[] { arg0, ... argsN })
     * $anyCodeInsertedbyRestore
     * }
     * $originalMethodBody
     * </code>
     *
     * @param mv     the method visitor to add the instructions to.
     * @param change the local variable containing the alternate implementation.
     * @param args   the type of the local variable that need to be forwarded.
     */
    void redirect(GeneratorAdapter mv, int change, List<Type> args) {
        // code to check if a new implementation of the current class is available.
        Label l0 = new Label();
        mv.loadLocal(change);
        mv.visitJumpInsn(Opcodes.IFNULL, l0);
        mv.loadLocal(change);
        mv.push(name);

        // create an array of objects capable of containing all the parameters and optionally the "this"
        createLocals(mv, args);

        // we need to maintain the stack index when loading parameters from, as for long and double
        // values, it uses 2 stack elements, all others use only 1 stack element.
        int stackIndex = 0;
        for (int arrayIndex = 0; arrayIndex < args.size(); arrayIndex++) {
            Type arg = args.get(arrayIndex);
            // duplicate the array of objects reference, it will be used to store the value in.
            mv.dup();
            // index in the array of objects to store the boxed parameter.
            mv.push(arrayIndex);
            // Pushes the appropriate local variable on the stack
            redirectLocal(mv, stackIndex, arg);
            // potentially box up intrinsic types.
            mv.box(arg);
            mv.arrayStore(Type.getType(Object.class));
            // stack index must progress according to the parameter type we just processed.
            stackIndex += arg.getSize();
        }

        // now invoke the generic dispatch method.
        mv.invokeInterface(IncrementalVisitor.CHANGE_TYPE, Method.getMethod("Object access$dispatch(String, Object[])"));

        // Restore the state after the redirection
        restore(mv, args);
        // jump label for classes without any new implementation, just invoke the original
        // method implementation.
        mv.visitLabel(l0);
    }

    /**
     * Creates and pushes to the stack the array to hold all the parameters to redirect, and
     * optionally this.
     */
    protected void createLocals(GeneratorAdapter mv, List<Type> args) {
        mv.push(args.size());
        mv.newArray(Type.getType(Object.class));
    }

    /**
     * After the redirection is called, this methods handles restoring the state given
     * the return values of the redirection.
     */
    protected abstract void restore(GeneratorAdapter mv, List<Type> args);

    /**
     * Pushes in the stack the value that should be redirected for the given local.
     */
    protected void redirectLocal(GeneratorAdapter mv, int local, Type arg) {
        mv.visitVarInsn(arg.getOpcode(Opcodes.ILOAD), local);
    }

    public LabelNode getPosition() {
        return label;
    }
}