/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.backport;

import proguard.classfile.ClassConstants;
import proguard.classfile.Clazz;
import proguard.classfile.Method;
import proguard.classfile.ProgramClass;
import proguard.classfile.attribute.BootstrapMethodInfo;
import proguard.classfile.util.ClassUtil;
import proguard.classfile.util.MemberFinder;

/**
 * A small helper class that captures useful information
 * about a lambda expression as encountered in a class file.
 *
 * @author Thomas Neidhart
 */
public class LambdaExpression
{
    // The referenced class of the lambda expression.
    public ProgramClass referencedClass;

    // The referenced bootstrap method index.
    public int                 bootstrapMethodIndex;
    // The referenced bootstrap method info.
    public BootstrapMethodInfo bootstrapMethodInfo;

    // The lambda factory method type.
    public String factoryMethodDescriptor;

    // The implemented interfaces of the Lambda expression.
    public String[] interfaces;

    // The additional bridge method descriptors to be added.
    public String[] bridgeMethodDescriptors;

    // The name and descriptor of the implemented interface method.
    public String interfaceMethod;
    public String interfaceMethodDescriptor;

    // Information regarding the invoked method.
    public int    invokedReferenceKind;
    public String invokedClassName;
    public String invokedMethodName;
    public String invokedMethodDesc;

    public Clazz referencedInvokedClass;
    public Method referencedInvokedMethod;

    // The created lambda class.
    public ProgramClass lambdaClass;


    /**
     * Creates a new initialized LambdaExpression (except for the lambdaClass).
     */
    public LambdaExpression(ProgramClass        referencedClass,
                            int                 bootstrapMethodIndex,
                            BootstrapMethodInfo bootstrapMethodInfo,
                            String              factoryMethodDescriptor,
                            String[]            interfaces,
                            String[]            bridgeMethodDescriptors,
                            String              interfaceMethod,
                            String              interfaceMethodDescriptor,
                            int                 invokedReferenceKind,
                            String              invokedClassName,
                            String              invokedMethodName,
                            String              invokedMethodDesc,
                            Clazz               referencedInvokedClass,
                            Method              referencedInvokedMethod)
    {
        this.referencedClass           = referencedClass;
        this.bootstrapMethodIndex      = bootstrapMethodIndex;
        this.bootstrapMethodInfo       = bootstrapMethodInfo;
        this.factoryMethodDescriptor   = factoryMethodDescriptor;
        this.interfaces                = interfaces;
        this.bridgeMethodDescriptors   = bridgeMethodDescriptors;
        this.interfaceMethod           = interfaceMethod;
        this.interfaceMethodDescriptor = interfaceMethodDescriptor;
        this.invokedReferenceKind      = invokedReferenceKind;
        this.invokedClassName          = invokedClassName;
        this.invokedMethodName         = invokedMethodName;
        this.invokedMethodDesc         = invokedMethodDesc;
        this.referencedInvokedClass    = referencedInvokedClass;
        this.referencedInvokedMethod   = referencedInvokedMethod;
    }


    /**
     * Returns the class name of the converted anonymous class.
     */
    public String getLambdaClassName()
    {
        return String.format("%s$$Lambda$%d",
                             referencedClass.getName(),
                             bootstrapMethodIndex);
    }


    public String getConstructorDescriptor()
    {
        if (isStateless())
        {
            return ClassConstants.METHOD_TYPE_INIT;
        }
        else
        {
            int endIndex = factoryMethodDescriptor.indexOf(ClassConstants.METHOD_ARGUMENTS_CLOSE);

            return factoryMethodDescriptor.substring(0, endIndex + 1) + ClassConstants.TYPE_VOID;
        }
    }


    /**
     * Returns whether the lambda expression is serializable.
     */
    public boolean isSerializable()
    {
        for (String interfaceName : interfaces)
        {
            if (ClassConstants.NAME_JAVA_IO_SERIALIZABLE.equals(interfaceName))
            {
                return true;
            }
        }
        return false;
    }


    /**
     * Returns whether the lambda expression is actually a method reference.
     */
    public boolean isMethodReference()
    {
        return !isLambdaMethod(invokedMethodName);
    }


    /**
     * Returns whether the lambda expression is stateless.
     */
    public boolean isStateless()
    {
        // The lambda expression is stateless if the factory method does
        // not have arguments.
        return
            ClassUtil.internalMethodParameterCount(factoryMethodDescriptor) == 0;
    }


    /**
     * Returns whether the invoked method is a static interface method.
     */
    public boolean invokesStaticInterfaceMethod()
    {
        // We assume unknown classes are not interfaces.
        return invokedReferenceKind == ClassConstants.REF_invokeStatic &&
               referencedInvokedClass != null                          &&
               (referencedInvokedClass.getAccessFlags() & ClassConstants.ACC_INTERFACE) != 0;
    }


    /**
     * Returns whether an accessor method is needed to access
     * the invoked method from the lambda class.
     */
    public boolean needsAccessorMethod()
    {
        // We assume unknown classes don't need an accessor method.
        return referencedInvokedClass != null &&
               new MemberFinder().findMethod(lambdaClass,
                                             referencedInvokedClass,
                                             invokedMethodName,
                                             invokedMethodDesc) == null;
    }


    // Small Utility methods.

    private static final String LAMBDA_METHOD_PREFIX = "lambda$";

    private static boolean isLambdaMethod(String methodName)
    {
        return methodName.startsWith(LAMBDA_METHOD_PREFIX);
    }
}
