/*
 * 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 com.facebook.presto.bytecode.instruction;

import com.facebook.presto.bytecode.BytecodeNode;
import com.facebook.presto.bytecode.BytecodeVisitor;
import com.facebook.presto.bytecode.MethodDefinition;
import com.facebook.presto.bytecode.MethodGenerationContext;
import com.facebook.presto.bytecode.OpCode;
import com.facebook.presto.bytecode.ParameterizedType;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableList;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.List;

import static com.facebook.presto.bytecode.MethodDefinition.methodDescription;
import static com.facebook.presto.bytecode.OpCode.INVOKEDYNAMIC;
import static com.facebook.presto.bytecode.OpCode.INVOKEINTERFACE;
import static com.facebook.presto.bytecode.OpCode.INVOKESPECIAL;
import static com.facebook.presto.bytecode.OpCode.INVOKESTATIC;
import static com.facebook.presto.bytecode.OpCode.INVOKEVIRTUAL;
import static com.facebook.presto.bytecode.ParameterizedType.type;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.transform;
import static java.util.Objects.requireNonNull;

@SuppressWarnings("UnusedDeclaration")
public class InvokeInstruction
        implements InstructionNode
{
    //
    // Invoke Static
    //

    public static InstructionNode invokeStatic(Method method)
    {
        return invoke(INVOKESTATIC, method);
    }

    public static InstructionNode invokeStatic(MethodDefinition method)
    {
        return invoke(INVOKESTATIC, method);
    }

    public static InstructionNode invokeStatic(Class<?> target, String name, Class<?> returnType, Class<?>... parameterTypes)
    {
        return invoke(INVOKESTATIC, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeStatic(Class<?> target, String name, Class<?> returnType, Iterable<Class<?>> parameterTypes)
    {
        return invoke(INVOKESTATIC, target, name, returnType, parameterTypes);
    }

    public static InstructionNode invokeStatic(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes)
    {
        return invoke(INVOKESTATIC, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeStatic(ParameterizedType target, String name, ParameterizedType returnType, Iterable<ParameterizedType> parameterTypes)
    {
        return invoke(INVOKESTATIC, target, name, returnType, parameterTypes);
    }

    //
    // Invoke Virtual
    //

    public static InstructionNode invokeVirtual(Method method)
    {
        return invoke(INVOKEVIRTUAL, method);
    }

    public static InstructionNode invokeVirtual(MethodDefinition method)
    {
        return invoke(INVOKEVIRTUAL, method);
    }

    public static InstructionNode invokeVirtual(Class<?> target, String name, Class<?> returnType, Class<?>... parameterTypes)
    {
        return invoke(INVOKEVIRTUAL, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeVirtual(Class<?> target, String name, Class<?> returnType, Iterable<Class<?>> parameterTypes)
    {
        return invoke(INVOKEVIRTUAL, target, name, returnType, parameterTypes);
    }

    public static InstructionNode invokeVirtual(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes)
    {
        return invoke(INVOKEVIRTUAL, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeVirtual(ParameterizedType target, String name, ParameterizedType returnType, Iterable<ParameterizedType> parameterTypes)
    {
        return invoke(INVOKEVIRTUAL, target, name, returnType, parameterTypes);
    }

    //
    // Invoke Interface
    //

    public static InstructionNode invokeInterface(Method method)
    {
        return invoke(INVOKEINTERFACE, method);
    }

    public static InstructionNode invokeInterface(MethodDefinition method)
    {
        return invoke(INVOKEINTERFACE, method);
    }

    public static InstructionNode invokeInterface(Class<?> target, String name, Class<?> returnType, Class<?>... parameterTypes)
    {
        return invoke(INVOKEINTERFACE, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeInterface(Class<?> target, String name, Class<?> returnType, Iterable<Class<?>> parameterTypes)
    {
        return invoke(INVOKEINTERFACE, target, name, returnType, parameterTypes);
    }

    public static InstructionNode invokeInterface(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes)
    {
        return invoke(INVOKEINTERFACE, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeInterface(ParameterizedType target, String name, ParameterizedType returnType, Iterable<ParameterizedType> parameterTypes)
    {
        return invoke(INVOKEINTERFACE, target, name, returnType, parameterTypes);
    }

    //
    // Invoke Constructor
    //

    public static InstructionNode invokeConstructor(Constructor<?> constructor)
    {
        return invokeConstructor(constructor.getDeclaringClass(), constructor.getParameterTypes());
    }

    public static InstructionNode invokeConstructor(Class<?> target, Class<?>... parameterTypes)
    {
        return invokeConstructor(type(target), transform(ImmutableList.copyOf(parameterTypes), ParameterizedType::type));
    }

    public static InstructionNode invokeConstructor(Class<?> target, Iterable<Class<?>> parameterTypes)
    {
        return invokeConstructor(type(target), transform(parameterTypes, ParameterizedType::type));
    }

    public static InstructionNode invokeConstructor(ParameterizedType target, ParameterizedType... parameterTypes)
    {
        return invokeConstructor(target, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeConstructor(ParameterizedType target, Iterable<ParameterizedType> parameterTypes)
    {
        return invokeSpecial(target, "<init>", type(void.class), parameterTypes);
    }

    //
    // Invoke Special
    //

    public static InstructionNode invokeSpecial(Method method)
    {
        return invoke(INVOKESPECIAL, method);
    }

    public static InstructionNode invokeSpecial(MethodDefinition method)
    {
        return invoke(INVOKESPECIAL, method);
    }

    public static InstructionNode invokeSpecial(Class<?> target, String name, Class<?> returnType, Class<?>... parameterTypes)
    {
        return invoke(INVOKESPECIAL, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeSpecial(Class<?> target, String name, Class<?> returnType, Iterable<Class<?>> parameterTypes)
    {
        return invoke(INVOKESPECIAL, target, name, returnType, parameterTypes);
    }

    public static InstructionNode invokeSpecial(ParameterizedType target, String name, ParameterizedType returnType, ParameterizedType... parameterTypes)
    {
        return invoke(INVOKESPECIAL, target, name, returnType, ImmutableList.copyOf(parameterTypes));
    }

    public static InstructionNode invokeSpecial(ParameterizedType target, String name, ParameterizedType returnType, Iterable<ParameterizedType> parameterTypes)
    {
        return invoke(INVOKESPECIAL, target, name, returnType, parameterTypes);
    }

    //
    // Generic
    //

    private static InstructionNode invoke(OpCode invocationType, Method method)
    {
        return new InvokeInstruction(invocationType,
                type(method.getDeclaringClass()),
                method.getName(),
                type(method.getReturnType()),
                transform(ImmutableList.copyOf(method.getParameterTypes()), ParameterizedType::type));
    }

    private static InstructionNode invoke(OpCode invocationType, MethodDefinition method)
    {
        return new InvokeInstruction(invocationType,
                method.getDeclaringClass().getType(),
                method.getName(),
                method.getReturnType(),
                method.getParameterTypes());
    }

    private static InstructionNode invoke(OpCode invocationType, ParameterizedType target, String name, ParameterizedType returnType, Iterable<ParameterizedType> parameterTypes)
    {
        return new InvokeInstruction(invocationType,
                target,
                name,
                returnType,
                parameterTypes);
    }

    private static InstructionNode invoke(OpCode invocationType, Class<?> target, String name, Class<?> returnType, Iterable<Class<?>> parameterTypes)
    {
        return new InvokeInstruction(invocationType,
                type(target),
                name,
                type(returnType),
                transform(parameterTypes, ParameterizedType::type));
    }

    //
    // Invoke Dynamic
    //

    public static InstructionNode invokeDynamic(String name,
            ParameterizedType returnType,
            Iterable<ParameterizedType> parameterTypes,
            Method bootstrapMethod,
            Iterable<Object> bootstrapArguments)
    {
        return new InvokeDynamicInstruction(name,
                returnType,
                parameterTypes,
                bootstrapMethod,
                ImmutableList.copyOf(bootstrapArguments));
    }

    public static InstructionNode invokeDynamic(String name,
            ParameterizedType returnType,
            Iterable<ParameterizedType> parameterTypes,
            Method bootstrapMethod,
            Object... bootstrapArguments)
    {
        return new InvokeDynamicInstruction(name,
                returnType,
                parameterTypes,
                bootstrapMethod,
                ImmutableList.copyOf(bootstrapArguments));
    }

    public static InstructionNode invokeDynamic(String name,
            MethodType methodType,
            Method bootstrapMethod,
            Iterable<Object> bootstrapArguments)
    {
        return new InvokeDynamicInstruction(name,
                type(methodType.returnType()),
                transform(methodType.parameterList(), ParameterizedType::type),
                bootstrapMethod,
                ImmutableList.copyOf(bootstrapArguments));
    }

    public static InstructionNode invokeDynamic(String name,
            MethodType methodType,
            Method bootstrapMethod,
            Object... bootstrapArguments)
    {
        return new InvokeDynamicInstruction(name,
                type(methodType.returnType()),
                transform(methodType.parameterList(), ParameterizedType::type),
                bootstrapMethod,
                ImmutableList.copyOf(bootstrapArguments));
    }

    private final OpCode opCode;
    private final ParameterizedType target;
    private final String name;
    private final ParameterizedType returnType;
    private final List<ParameterizedType> parameterTypes;

    public InvokeInstruction(OpCode opCode,
            ParameterizedType target,
            String name,
            ParameterizedType returnType,
            Iterable<ParameterizedType> parameterTypes)
    {
        checkUnqualifiedName(name);
        this.opCode = opCode;
        this.target = target;
        this.name = name;
        this.returnType = returnType;
        this.parameterTypes = ImmutableList.copyOf(parameterTypes);
    }

    public OpCode getOpCode()
    {
        return opCode;
    }

    public ParameterizedType getTarget()
    {
        return target;
    }

    public String getName()
    {
        return name;
    }

    public ParameterizedType getReturnType()
    {
        return returnType;
    }

    public List<ParameterizedType> getParameterTypes()
    {
        return parameterTypes;
    }

    public String getMethodDescription()
    {
        return methodDescription(returnType, parameterTypes);
    }

    @Override
    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext)
    {
        visitor.visitMethodInsn(opCode.getOpCode(), target.getClassName(), name, getMethodDescription(), target.isInterface());
    }

    @Override
    public List<BytecodeNode> getChildNodes()
    {
        return ImmutableList.of();
    }

    @Override
    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor)
    {
        return visitor.visitInvoke(parent, this);
    }

    public static class InvokeDynamicInstruction
            extends InvokeInstruction
    {
        private final Method bootstrapMethod;
        private final List<Object> bootstrapArguments;

        public InvokeDynamicInstruction(String name,
                ParameterizedType returnType,
                Iterable<ParameterizedType> parameterTypes,
                Method bootstrapMethod,
                List<Object> bootstrapArguments)
        {
            super(INVOKEDYNAMIC, null, name, returnType, parameterTypes);
            this.bootstrapMethod = bootstrapMethod;
            this.bootstrapArguments = ImmutableList.copyOf(bootstrapArguments);
        }

        @Override
        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext)
        {
            Handle bootstrapMethodHandle = new Handle(Opcodes.H_INVOKESTATIC,
                    type(bootstrapMethod.getDeclaringClass()).getClassName(),
                    bootstrapMethod.getName(),
                    methodDescription(
                            bootstrapMethod.getReturnType(),
                            bootstrapMethod.getParameterTypes()),
                    false);

            visitor.visitInvokeDynamicInsn(getName(),
                    getMethodDescription(),
                    bootstrapMethodHandle,
                    bootstrapArguments.toArray(new Object[bootstrapArguments.size()]));
        }

        public Method getBootstrapMethod()
        {
            return bootstrapMethod;
        }

        public List<Object> getBootstrapArguments()
        {
            return bootstrapArguments;
        }

        @Override
        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor)
        {
            return visitor.visitInvokeDynamic(parent, this);
        }
    }

    private static void checkUnqualifiedName(String name)
    {
        // JVM Specification 4.2.2 Unqualified Names
        requireNonNull(name, "name is null");
        checkArgument(!name.isEmpty(), "name is empty");
        if (name.equals("<init>") || name.equals("<clinit>")) {
            return;
        }
        CharMatcher invalid = CharMatcher.anyOf(".;[/<>");
        checkArgument(invalid.matchesNoneOf(name), "invalid name: %s", name);
    }
}
