/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu)
 *
 * 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.optimize.peephole;

import proguard.classfile.*;
import proguard.classfile.attribute.visitor.AttributeNameFilter;
import proguard.classfile.constant.visitor.*;
import proguard.classfile.editor.*;
import proguard.classfile.util.*;
import proguard.classfile.visitor.*;
import proguard.optimize.KeepMarker;
import proguard.optimize.info.*;
import proguard.util.*;

import java.util.*;

/**
 * This ClassVisitor inlines the classes that it visits in a given target class,
 * whenever possible.
 *
 * @see RetargetedInnerClassAttributeRemover
 * @see TargetClassChanger
 * @see ClassReferenceFixer
 * @see MemberReferenceFixer
 * @see AccessFixer
 * @author Eric Lafortune
 */
public class ClassMerger
extends      SimplifiedVisitor
implements   ClassVisitor,
             ConstantVisitor
{
    //*
    private static final boolean DEBUG   = false;
    private static final boolean DETAILS = false;
    /*/
    private static       boolean DEBUG   = System.getProperty("cm") != null;
    private static       boolean DETAILS = System.getProperty("cmd") != null;
    //*/


    private final ProgramClass targetClass;
    private final boolean      allowAccessModification;
    private final boolean      mergeInterfacesAggressively;
    private final ClassVisitor extraClassVisitor;

    private final MemberVisitor fieldOptimizationInfoCopier = new FieldOptimizationInfoCopier();


    /**
     * Creates a new ClassMerger that will merge classes into the given target
     * class.
     * @param targetClass                 the class into which all visited
     *                                    classes will be merged.
     * @param allowAccessModification     specifies whether the access modifiers
     *                                    of classes can be changed in order to
     *                                    merge them.
     * @param mergeInterfacesAggressively specifies whether interfaces may
     *                                    be merged aggressively.
     */
    public ClassMerger(ProgramClass targetClass,
                       boolean      allowAccessModification,
                       boolean      mergeInterfacesAggressively)
    {
        this(targetClass, allowAccessModification, mergeInterfacesAggressively, null);
    }


    /**
     * Creates a new ClassMerger that will merge classes into the given target
     * class.
     * @param targetClass                 the class into which all visited
     *                                    classes will be merged.
     * @param allowAccessModification     specifies whether the access modifiers
     *                                    of classes can be changed in order to
     *                                    merge them.
     * @param mergeInterfacesAggressively specifies whether interfaces may
     *                                    be merged aggressively.
     * @param extraClassVisitor           an optional extra visitor for all
     *                                    merged classes.
     */
    public ClassMerger(ProgramClass targetClass,
                       boolean      allowAccessModification,
                       boolean      mergeInterfacesAggressively,
                       ClassVisitor extraClassVisitor)
    {
        this.targetClass                 = targetClass;
        this.allowAccessModification     = allowAccessModification;
        this.mergeInterfacesAggressively = mergeInterfacesAggressively;
        this.extraClassVisitor           = extraClassVisitor;
    }


    // Implementations for ClassVisitor.

    public void visitProgramClass(ProgramClass programClass)
    {
        //final String CLASS_NAME = "abc/Def";
        //DEBUG = programClass.getName().equals(CLASS_NAME) ||
        //        targetClass.getName().equals(CLASS_NAME);

        // TODO: Remove this when the class merger has stabilized.
        // Catch any unexpected exceptions from the actual visiting method.
        try
        {
            visitProgramClass0(programClass);
        }
        catch (RuntimeException ex)
        {
            System.err.println("Unexpected error while merging classes:");
            System.err.println("  Class        = ["+programClass.getName()+"]");
            System.err.println("  Target class = ["+targetClass.getName()+"]");
            System.err.println("  Exception    = ["+ex.getClass().getName()+"] ("+ex.getMessage()+")");

            if (DEBUG)
            {
                programClass.accept(new ClassPrinter());
                targetClass.accept(new ClassPrinter());
            }

            throw ex;
        }
    }

    public void visitProgramClass0(ProgramClass programClass)
    {
        if (!programClass.equals(targetClass) &&

            // Don't merge classes that must be preserved.
            !KeepMarker.isKept(programClass) &&
            !KeepMarker.isKept(targetClass)  &&

            // Only merge classes that haven't been retargeted yet.
            getTargetClass(programClass) == null &&
            getTargetClass(targetClass)  == null &&

            // Don't merge annotation classes, with all their introspection and
            // infinite recursion.
            (programClass.getAccessFlags() & ClassConstants.ACC_ANNOTATTION) == 0 &&

            (!DETAILS || print(programClass, "Package visibility?")) &&

            // Only merge classes if we can change the access permissions, or
            // if they are in the same package, or
            // if they are public and don't contain or invoke package visible
            // class members.
            (allowAccessModification                                                        ||
             ((programClass.getAccessFlags() &
               targetClass.getAccessFlags()  &
               ClassConstants.ACC_PUBLIC) != 0 &&
              !PackageVisibleMemberContainingClassMarker.containsPackageVisibleMembers(programClass) &&
              !PackageVisibleMemberInvokingClassMarker.invokesPackageVisibleMembers(programClass)) ||
             ClassUtil.internalPackageName(programClass.getName()).equals(
             ClassUtil.internalPackageName(targetClass.getName()))) &&

            (!DETAILS || print(programClass, "Interface/abstract/single?")) &&

            // Only merge two classes or two interfaces or two abstract classes,
            // or a single implementation into its interface.
            ((programClass.getAccessFlags() &
              (ClassConstants.ACC_INTERFACE |
               ClassConstants.ACC_ABSTRACT)) ==
             (targetClass.getAccessFlags()  &
              (ClassConstants.ACC_INTERFACE |
               ClassConstants.ACC_ABSTRACT)) ||
             (isOnlySubClass(programClass, targetClass) &&
              programClass.getSuperClass() != null      &&
              (programClass.getSuperClass().equals(targetClass) ||
               programClass.getSuperClass().equals(targetClass.getSuperClass())))) &&

            (!DETAILS || print(programClass, "Indirect implementation?")) &&

            // One class must not implement the other class indirectly.
            !indirectlyImplementedInterfaces(programClass).contains(targetClass) &&
            !targetClass.extendsOrImplements(programClass) &&

            (!DETAILS || print(programClass, "Interfaces same subinterfaces?")) &&

            // Interfaces must have exactly the same subinterfaces, not
            // counting themselves, to avoid any loops in the interface
            // hierarchy.
            ((programClass.getAccessFlags() & ClassConstants.ACC_INTERFACE) == 0 ||
             (targetClass.getAccessFlags()  & ClassConstants.ACC_INTERFACE) == 0 ||
             subInterfaces(programClass, targetClass).equals(subInterfaces(targetClass, programClass))) &&

            (!DETAILS || print(programClass, "Same initialized superclasses?")) &&

            // The two classes must have the same superclasses and interfaces
            // with static initializers.
            initializedSuperClasses(programClass).equals(initializedSuperClasses(targetClass))   &&

            (!DETAILS || print(programClass, "Same instanceofed superclasses?")) &&

            // The two classes must have the same superclasses and interfaces
            // that are tested with 'instanceof'.
            instanceofedSuperClasses(programClass).equals(instanceofedSuperClasses(targetClass)) &&

            (!DETAILS || print(programClass, "Same caught superclasses?")) &&

            // The two classes must have the same superclasses that are caught
            // as exceptions.
            caughtSuperClasses(programClass).equals(caughtSuperClasses(targetClass)) &&

            (!DETAILS || print(programClass, "Not .classed?")) &&

            // The two classes must not both be part of a .class construct.
            !(DotClassMarker.isDotClassed(programClass) &&
              DotClassMarker.isDotClassed(targetClass)) &&

            (!DETAILS || print(programClass, "No clashing fields?")) &&

            // The classes must not have clashing fields.
            !haveAnyIdenticalFields(programClass, targetClass) &&

            (!DETAILS || print(programClass, "No unwanted fields?")) &&

            // The two classes must not introduce any unwanted fields.
            !introducesUnwantedFields(programClass, targetClass) &&
            !introducesUnwantedFields(targetClass, programClass) &&

            (!DETAILS || print(programClass, "No shadowed fields?")) &&

            // The two classes must not shadow each others fields.
            !shadowsAnyFields(programClass, targetClass) &&
            !shadowsAnyFields(targetClass, programClass) &&

            (!DETAILS || print(programClass, "No clashing methods?")) &&

            // The classes must not have clashing methods.
            !haveAnyIdenticalMethods(programClass, targetClass) &&

            (!DETAILS || print(programClass, "No abstract methods?")) &&

            // The classes must not introduce abstract methods, unless
            // explicitly allowed.
            (mergeInterfacesAggressively ||
             (!introducesUnwantedAbstractMethods(programClass, targetClass) &&
              !introducesUnwantedAbstractMethods(targetClass, programClass))) &&

            (!DETAILS || print(programClass, "No overridden methods?")) &&

            // The classes must not override each others concrete methods.
            !overridesAnyMethods(programClass, targetClass) &&
            !overridesAnyMethods(targetClass, programClass) &&

            (!DETAILS || print(programClass, "No shadowed methods?")) &&

            // The classes must not shadow each others non-private methods.
            !shadowsAnyMethods(programClass, targetClass) &&
            !shadowsAnyMethods(targetClass, programClass))
        {
            // We're not actually merging the classes, but only copying the
            // contents from the source class to the target class. We'll
            // then let all other classes point to it. The shrinking step
            // will finally remove the source class.
            if (DEBUG)
            {
                System.out.println("ClassMerger ["+programClass.getName()+"] -> ["+targetClass.getName()+"]");
                System.out.println("  Source interface? ["+((programClass.getAccessFlags() & ClassConstants.ACC_INTERFACE)!=0)+"]");
                System.out.println("  Target interface? ["+((targetClass.getAccessFlags() & ClassConstants.ACC_INTERFACE)!=0)+"]");
                System.out.println("  Source subclasses ["+programClass.subClasses+"]");
                System.out.println("  Target subclasses ["+targetClass.subClasses+"]");
                System.out.println("  Source superclass ["+programClass.getSuperClass().getName()+"]");
                System.out.println("  Target superclass ["+targetClass.getSuperClass().getName()+"]");

                //System.out.println("=== Before ===");
                //programClass.accept(new ClassPrinter());
                //targetClass.accept(new ClassPrinter());
            }

            // Combine the access flags.
            int targetAccessFlags = targetClass.getAccessFlags();
            int sourceAccessFlags = programClass.getAccessFlags();

            targetClass.u2accessFlags =
                ((targetAccessFlags &
                  sourceAccessFlags) &
                 (ClassConstants.ACC_INTERFACE |
                  ClassConstants.ACC_ABSTRACT)) |
                ((targetAccessFlags |
                  sourceAccessFlags) &
                 (ClassConstants.ACC_PUBLIC      |
                  ClassConstants.ACC_SUPER       |
                  ClassConstants.ACC_ANNOTATTION |
                  ClassConstants.ACC_ENUM));

            // Copy over the superclass, if it's a non-interface class being
            // merged into an interface class.
            // However, we're currently never merging in a way that changes the
            // superclass.
            //if ((programClass.getAccessFlags() & ClassConstants.ACC_INTERFACE) == 0 &&
            //    (targetClass.getAccessFlags()  & ClassConstants.ACC_INTERFACE) != 0)
            //{
            //    targetClass.u2superClass =
            //        new ConstantAdder(targetClass).addConstant(programClass, programClass.u2superClass);
            //}

            // Copy over the interfaces that aren't present yet and that
            // wouldn't cause loops in the class hierarchy.
            // Note that the code shouldn't be iterating over the original
            // list at this point. This is why we only add subclasses in
            // a separate step.
            programClass.interfaceConstantsAccept(
                new ExceptClassConstantFilter(targetClass.getName(),
                new ImplementedClassConstantFilter(targetClass,
                new ImplementingClassConstantFilter(targetClass,
                new InterfaceAdder(targetClass)))));

            // Copy over the class members.
            MemberAdder memberAdder =
                new MemberAdder(targetClass, fieldOptimizationInfoCopier);

            programClass.fieldsAccept(memberAdder);
            programClass.methodsAccept(memberAdder);

            // Copy over the other attributes.
            programClass.attributesAccept(
                new AttributeNameFilter(new NotMatcher(
                    new OrMatcher(new FixedStringMatcher(ClassConstants.ATTR_BootstrapMethods),
                    new OrMatcher(new FixedStringMatcher(ClassConstants.ATTR_SourceFile),
                    new OrMatcher(new FixedStringMatcher(ClassConstants.ATTR_InnerClasses),
                                  new FixedStringMatcher(ClassConstants.ATTR_EnclosingMethod))))),
                new AttributeAdder(targetClass, true)));

            // Update the optimization information of the target class.
            ClassOptimizationInfo info =
                ClassOptimizationInfo.getClassOptimizationInfo(targetClass);
            if (info != null)
            {
                info.merge(ClassOptimizationInfo.getClassOptimizationInfo(programClass));
            }

            // Remember to replace the inlined class by the target class.
            setTargetClass(programClass, targetClass);

            //if (DEBUG)
            //{
            //    System.out.println("=== After ====");
            //    targetClass.accept(new ClassPrinter());
            //}

            // Visit the merged class, if required.
            if (extraClassVisitor != null)
            {
                extraClassVisitor.visitProgramClass(programClass);
            }
        }
    }


    private boolean print(ProgramClass programClass, String message)
    {
        System.out.println("Merge ["+targetClass.getName()+"] <- ["+programClass.getName()+"] "+message);

        return true;
    }


    // Small utility methods.

    /**
     * Returns whether a given class is the only subclass of another given class.
     */
    private boolean isOnlySubClass(Clazz        subClass,
                                   ProgramClass clazz)
    {
        // TODO: The list of subclasses is not up to date.
        return clazz.subClasses != null     &&
               clazz.subClasses.length == 1 &&
               clazz.subClasses[0].equals(subClass);
    }


    /**
     * Returns the set of indirectly implemented interfaces.
     */
    private Set indirectlyImplementedInterfaces(Clazz clazz)
    {
        Set set = new HashSet();

        ReferencedClassVisitor referencedInterfaceCollector =
            new ReferencedClassVisitor(
            new ClassHierarchyTraveler(false, false, true, false,
            new ClassCollector(set)));

        // Visit all superclasses and  collect their interfaces.
        clazz.superClassConstantAccept(referencedInterfaceCollector);

        // Visit all interfaces and collect their interfaces.
        clazz.interfaceConstantsAccept(referencedInterfaceCollector);

        return set;
    }


    /**
     * Returns the set of interface subclasses, not including the given class.
     */
    private Set subInterfaces(Clazz clazz, Clazz exceptClass)
    {
        Set set = new HashSet();

        // Visit all subclasses, collecting the interface classes.
        clazz.hierarchyAccept(false, false, false, true,
            new ClassAccessFilter(ClassConstants.ACC_INTERFACE, 0,
            new ExceptClassesFilter(new Clazz[] { exceptClass },
            new ClassCollector(set))));

        return set;
    }


    /**
     * Returns the set of superclasses and interfaces that are initialized.
     */
    private Set initializedSuperClasses(Clazz clazz)
    {
        Set set = new HashSet();

        // Visit all superclasses and interfaces, collecting the ones that have
        // static initializers.
        clazz.hierarchyAccept(true, true, true, false,
                              new StaticInitializerContainingClassFilter(
                              new ClassCollector(set)));

        return set;
    }


    /**
     * Returns the set of superclasses and interfaces that are used in
     * 'instanceof' tests.
     */
    private Set instanceofedSuperClasses(Clazz clazz)
    {
        Set set = new HashSet();

        // Visit all superclasses and interfaces, collecting the ones that are
        // used in an 'instanceof' test.
        clazz.hierarchyAccept(true, true, true, false,
                              new InstanceofClassFilter(
                              new ClassCollector(set)));

        return set;
    }


    /**
     * Returns the set of superclasses that are caught as exceptions.
     */
    private Set caughtSuperClasses(Clazz clazz)
    {
        // Don't bother if this isn't an exception at all.
        if (!clazz.extends_(ClassConstants.NAME_JAVA_LANG_THROWABLE))
        {
            return Collections.EMPTY_SET;
        }

        // Visit all superclasses, collecting the ones that are caught
        // (plus java.lang.Object, in the current implementation).
        Set set = new HashSet();

        clazz.hierarchyAccept(true, true, false, false,
                              new CaughtClassFilter(
                              new ClassCollector(set)));

        return set;
    }


    /**
     * Returns whether the two given classes have fields with the same
     * names and descriptors.
     */
    private boolean haveAnyIdenticalFields(Clazz clazz, Clazz targetClass)
    {
        MemberCounter counter = new MemberCounter();

        // Visit all fields, counting the with the same name and descriptor in
        // the target class.
        clazz.fieldsAccept(new SimilarMemberVisitor(targetClass, true, false, false, false,
                           counter));

        return counter.getCount() > 0;
    }


    /**
     * Returns whether the given class would introduce any unwanted fields
     * in the target class.
     */
    private boolean introducesUnwantedFields(ProgramClass programClass,
                                             ProgramClass targetClass)
    {
        // It's ok if the target class is never instantiated, without any other
        // subclasses except for maybe the source class.
        if (!InstantiationClassMarker.isInstantiated(targetClass) &&
            (targetClass.subClasses == null ||
             isOnlySubClass(programClass, targetClass)))
        {
            return false;
        }

        MemberCounter counter = new MemberCounter();

        // Count all non-static fields in the the source class.
        programClass.fieldsAccept(new MemberAccessFilter(0, ClassConstants.ACC_STATIC,
                                  counter));

        return counter.getCount() > 0;
    }


    /**
     * Returns whether the given class or its subclasses shadow any fields in
     * the given target class.
     */
    private boolean shadowsAnyFields(Clazz clazz, Clazz targetClass)
    {
        MemberCounter counter = new MemberCounter();

        // Visit all fields, counting the ones that are shadowing non-private
        // fields in the class hierarchy of the target class.
        clazz.hierarchyAccept(true, false, false, true,
                              new AllFieldVisitor(
                              new SimilarMemberVisitor(targetClass, true, true, true, false,
                              new MemberAccessFilter(0, ClassConstants.ACC_PRIVATE,
                              counter))));

        return counter.getCount() > 0;
    }


    /**
     * Returns whether the two given classes have class members with the same
     * name and descriptor.
     */
    private boolean haveAnyIdenticalMethods(Clazz clazz, Clazz targetClass)
    {
        MemberCounter counter = new MemberCounter();

        // Visit all non-abstract methods, counting the ones that are also
        // present in the target class.
        clazz.methodsAccept(new MemberAccessFilter(0, ClassConstants.ACC_ABSTRACT,
                            new SimilarMemberVisitor(targetClass, true, false, false, false,
                            new MemberAccessFilter(0, ClassConstants.ACC_ABSTRACT,
                            counter))));

        return counter.getCount() > 0;
    }


    /**
     * Returns whether the given class would introduce any abstract methods
     * in the target class.
     */
    private boolean introducesUnwantedAbstractMethods(Clazz        clazz,
                                                      ProgramClass targetClass)
    {
        // It's ok if the target class is already abstract and it has at most
        // the class as a subclass.
        if ((targetClass.getAccessFlags() &
             (ClassConstants.ACC_ABSTRACT |
              ClassConstants.ACC_INTERFACE)) != 0 &&
            (targetClass.subClasses == null ||
             isOnlySubClass(clazz, targetClass)))
        {
            return false;
        }

        MemberCounter counter   = new MemberCounter();
        Set           targetSet = new HashSet();

        // Collect all abstract methods, and similar abstract methods in the
        // class hierarchy of the target class.
        clazz.methodsAccept(new MemberAccessFilter(ClassConstants.ACC_ABSTRACT, 0,
                            new MultiMemberVisitor(new MemberVisitor[]
                            {
                                counter,
                                new SimilarMemberVisitor(targetClass, true, true, true, false,
                                                         new MemberAccessFilter(ClassConstants.ACC_ABSTRACT, 0,
                                                         new MemberCollector(targetSet)))
                            })));

        return targetSet.size() < counter.getCount();
    }


    /**
     * Returns whether the given class overrides any methods in the given
     * target class.
     */
    private boolean overridesAnyMethods(Clazz clazz, Clazz targetClass)
    {
        MemberCounter counter = new MemberCounter();

        // Visit all non-private non-static methods, counting the ones that are
        // being overridden in the class hierarchy of the target class.
        clazz.methodsAccept(new MemberAccessFilter(0, ClassConstants.ACC_PRIVATE | ClassConstants.ACC_STATIC | ClassConstants.ACC_ABSTRACT,
                            new MemberNameFilter(new NotMatcher(new FixedStringMatcher(ClassConstants.METHOD_NAME_CLINIT)),
                            new MemberNameFilter(new NotMatcher(new FixedStringMatcher(ClassConstants.METHOD_NAME_INIT)),
                            new SimilarMemberVisitor(targetClass, true, true, false, false,
                            new MemberAccessFilter(0, ClassConstants.ACC_PRIVATE | ClassConstants.ACC_STATIC | ClassConstants.ACC_ABSTRACT,
                            counter))))));

        return counter.getCount() > 0;
    }


    /**
     * Returns whether the given class or its subclasses shadow any methods in
     * the given target class.
     */
    private boolean shadowsAnyMethods(Clazz clazz, Clazz targetClass)
    {
        MemberCounter counter = new MemberCounter();

        // Visit all private methods, counting the ones that are shadowing
        // non-private methods in the class hierarchy of the target class.
        clazz.hierarchyAccept(true, false, false, true,
                              new AllMethodVisitor(
                              new MemberAccessFilter(ClassConstants.ACC_PRIVATE, 0,
                              new MemberNameFilter(new NotMatcher(new FixedStringMatcher(ClassConstants.METHOD_NAME_INIT)),
                              new SimilarMemberVisitor(targetClass, true, true, true, false,
                              new MemberAccessFilter(0, ClassConstants.ACC_PRIVATE,
                              counter))))));

        // Visit all static methods, counting the ones that are shadowing
        // non-private methods in the class hierarchy of the target class.
        clazz.hierarchyAccept(true, false, false, true,
                              new AllMethodVisitor(
                              new MemberAccessFilter(ClassConstants.ACC_STATIC, 0,
                              new MemberNameFilter(new NotMatcher(new FixedStringMatcher(ClassConstants.METHOD_NAME_CLINIT)),
                              new SimilarMemberVisitor(targetClass, true, true, true, false,
                              new MemberAccessFilter(0, ClassConstants.ACC_PRIVATE,
                              counter))))));

        return counter.getCount() > 0;
    }


    public static void setTargetClass(Clazz clazz, Clazz targetClass)
    {
        ClassOptimizationInfo info = ClassOptimizationInfo.getClassOptimizationInfo(clazz);
        if (info != null)
        {
            info.setTargetClass(targetClass);
        }
    }


    public static Clazz getTargetClass(Clazz clazz)
    {
        Clazz targetClass = null;

        // Return the last target class, if any.
        while (true)
        {
            ClassOptimizationInfo info = ClassOptimizationInfo.getClassOptimizationInfo(clazz);
            if (info == null)
            {
                return targetClass;
            }

            clazz = info.getTargetClass();
            if (clazz == null)
            {
                return targetClass;
            }

            targetClass = clazz;
        }
    }


    /**
     * This MemberVisitor copies field optimization info from copied fields.
     */
    private static class FieldOptimizationInfoCopier
    extends              SimplifiedVisitor
    implements           MemberVisitor
    {
        public void visitProgramField(ProgramClass programClass, ProgramField programField)
        {
            // Copy the optimization info from the field that was just copied.
            ProgramField copiedField = (ProgramField)programField.getVisitorInfo();
            Object       info        = copiedField.getVisitorInfo();

            programField.setVisitorInfo(info instanceof FieldOptimizationInfo ?
                new FieldOptimizationInfo((FieldOptimizationInfo)info) :
                info);
        }


        public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
        {
            // Linked methods share their optimization info.
        }
    }
}
