/*
 * 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.configuration;


import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * This class can be injected in applications to log information about reflection
 * being used in the application code, and suggest appropriate ProGuard rules for
 * keeping the reflected classes, methods and/or fields.
 *
 * @author Johan Leys
 */
public class ConfigurationLogger implements Runnable
{
    public static final boolean LOG_ONCE = true;

    private static final String LOG_TAG = "ProGuard";

    public static final String CLASS_MAP_FILENAME = "classmap.txt";

    private static final String EMPTY_LINE = "\u00a0\n";

    // Set with missing class names.
    private static final Set<String> missingClasses = new HashSet<String>();

    // Map from class name to missing constructors.
    private static final Map<String, Set<MethodSignature>> missingConstructors = new HashMap<String, Set<MethodSignature>>();
    // Set of classes on which getConstructors or getDeclaredConstructors is invoked.
    private static final Set<String>                       constructorListingClasses = new HashSet<String>();

    // Map from class name to missing method signatures.
    private static final Map<String, Set<MethodSignature>> missingMethods       = new HashMap<String, Set<MethodSignature>>();
    // Set of classes on which getMethods or getDeclaredMethods is invoked.
    private static final Set<String>                       methodListingClasses = new HashSet<String>();

    // Map from class name to missing field names.
    private static final Map<String, Set<String>> missingFields       = new HashMap<String, Set<String>>();
    // Set of classes on which getFields or getDeclaredFields is invoked.
    private static final Set<String>              fieldListingCLasses = new HashSet<String>();

    // Map from obfuscated class name to original class name.
    private static Map<String, String> classNameMap;

    // Set of classes that have renamed or removed methods.
    private static Set<String>         classesWithObfuscatedMethods;

    // Set of classes that have renamed or removed fields.
    private static Set<String>         classesWithObfuscatedFields;

    private static Method logMethod;

    // Try to find the Android logging class.
    static
    {
        try
        {
            Class<?> logClass = Class.forName("android.util.Log");
            logMethod = logClass.getMethod("w", String.class, String. class);
        }
        catch (Exception e) {}
    }

    // Classes.

    /**
     * Log a failed call to Class.forName().
     *
     * @param callingClassName
     * @param missingClassName
     */
    public static void logForName(String callingClassName,
                                  String missingClassName)
    {
        logMissingClass(callingClassName, "Class", "forName", missingClassName);
    }

    /**
     * Log a failed call to ClassLoader.loadClass().
     *
     * @param callingClassName
     * @param missingClassName
     */
    public static void logLoadClass(String callingClassName,
                                  String missingClassName)
    {
        logMissingClass(callingClassName, "ClassLoader", "loadClass", missingClassName);
    }


    /**
     * Log a failed call to Class.forName().
     *
     * @param callingClassName
     * @param missingClassName
     */
    public static void logMissingClass(String callingClassName,
                                  String invokedClassName,
                                  String invokedMethodName,
                                  String missingClassName)
    {
        if (!LOG_ONCE || !missingClasses.contains(missingClassName))
        {
            missingClasses.add(missingClassName);
            log(
                "The class '" + originalClassName(callingClassName) + "' is calling " + invokedClassName + "." + invokedMethodName + " to retrieve\n" +
                "the class '" + missingClassName + "', but the latter could not be found.\n" +
                "It may have been obfuscated or shrunk.\n" +
                "You should consider preserving the class with its original name,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepClassRule(missingClassName) + "\n" +
                EMPTY_LINE);
        }
    }


    // Constructors.


    /**
     * Log a failed call to Class.getDeclaredConstructor().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param constructorParameters
     */
    public static void logGetDeclaredConstructor(String  invokingClassName,
                                                 Class   reflectedClass,
                                                 Class[] constructorParameters)
    {
        logGetConstructor(invokingClassName, "getDeclaredConstructor", reflectedClass, constructorParameters);
    }


    /**
     * Log a failed call to Class.getConstructor().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param constructorParameters
     */
    public static void logGetConstructor(String  invokingClassName,
                                         Class   reflectedClass,
                                         Class[] constructorParameters)
    {
        logGetConstructor(invokingClassName, "getConstructor", reflectedClass, constructorParameters);
    }


    /**
     * Log a failed call to one of the constructor retrieving methods on Class.
     *
     * @param invokingClassName
     * @param invokedMethodName
     * @param reflectedClass
     * @param constructorParameters
     */
    public static void logGetConstructor(String  invokingClassName,
                                         String  invokedMethodName,
                                         Class   reflectedClass,
                                         Class[] constructorParameters)
    {
        MethodSignature signature = new MethodSignature("<init>", constructorParameters);

        Set<MethodSignature> constructors = missingConstructors.get(reflectedClass.getName());
        if (constructors == null)
        {
            constructors = new HashSet<MethodSignature>();
            missingConstructors.put(reflectedClass.getName(), constructors);
        }

        if ((!LOG_ONCE || !constructors.contains(signature)) && !isLibraryClass(reflectedClass))
        {
            constructors.add(signature);
            log(
                "The class '" + originalClassName(invokingClassName) + "' is calling Class." + invokedMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) + "' to retrieve\n" +
                "the constructor with signature (" + originalSignature(signature) + "), but the latter could not be found.\n" +
                "It may have been obfuscated or shrunk.\n" +
                "You should consider preserving the constructor, with a setting like:\n" +
                EMPTY_LINE +
                keepConstructorRule(reflectedClass.getName(), signature) + "\n" +
                EMPTY_LINE);
        }
    }


    /**
     * Log a call to Class.getDeclaredConstructors().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetDeclaredConstructors(String invokingClassName,
                                                  Class  reflectedClass    )
    {
        logGetConstructors(invokingClassName, reflectedClass, "getDeclaredConstructors");
    }


    /**
     * Log a call to Class.getConstructors().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetConstructors(String invokingClassName,
                                          Class  reflectedClass    )
    {
        logGetConstructors(invokingClassName, reflectedClass, "getConstructors");
    }


    /**
     * Log a call to one of the constructor listing methods on Class.
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param reflectedMethodName
     */
    private static void logGetConstructors(String invokingClassName,
                                           Class  reflectedClass,
                                           String reflectedMethodName)
    {
        initializeMappings();
        if (classesWithObfuscatedMethods.contains(reflectedClass.getName()) &&
            !constructorListingClasses.contains(reflectedClass.getName()) &&
            !isLibraryClass(reflectedClass))
        {
            constructorListingClasses.add(reflectedClass.getName());
            log(
                "The class '" + originalClassName(invokingClassName) + "' is calling Class." + reflectedMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) + "' to retrieve its constructors.\n" +
                "You might consider preserving all constructors with their original names,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepAllConstructorsRule(reflectedClass) + "\n" +
                EMPTY_LINE);
        }
    }


    // Methods.


    /**
     * Log a failed call to Class.getDeclaredMethod().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param reflectedMethodName
     * @param methodParameters
     */
    public static void logGetDeclaredMethod(String  invokingClassName,
                                            Class   reflectedClass,
                                            String  reflectedMethodName,
                                            Class[] methodParameters    )
    {
        logGetMethod(invokingClassName, "getDeclaredMethod", reflectedClass, reflectedMethodName, methodParameters);
    }


    /**
     * Log a failed call to Class.getMethod().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param reflectedMethodName
     * @param methodParameters
     */
    public static void logGetMethod(String  invokingClassName,
                                    Class   reflectedClass,
                                    String  reflectedMethodName,
                                    Class[] methodParameters    )
    {
        logGetMethod(invokingClassName, "getMethod", reflectedClass, reflectedMethodName, methodParameters);
    }


    /**
     * Log a failed call to one of the method retrieving methods on Class.
     * @param invokingClassName
     * @param invokedReflectionMethodName
     * @param reflectedClass
     * @param reflectedMethodName
     * @param methodParameters
     */
    private static void logGetMethod(String  invokingClassName,
                                     String  invokedReflectionMethodName,
                                     Class   reflectedClass,
                                     String  reflectedMethodName,
                                     Class[] methodParameters    )
    {
        Set<MethodSignature> methods = missingMethods.get(reflectedClass.getName());
        if (methods == null)
        {
            methods = new HashSet<MethodSignature>();
            missingMethods.put(reflectedClass.getName(), methods);
        }

        MethodSignature signature = new MethodSignature(reflectedMethodName, methodParameters);
        if (!methods.contains(signature) && !isLibraryClass(reflectedClass))
        {
            methods.add(signature);
            log(
                "The class '" + originalClassName(invokingClassName) +
                "' is calling Class." + invokedReflectionMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) +
                "' to retrieve the method\n" +
                reflectedMethodName + "(" + originalSignature(signature) + "),\n" +
                "but the latter could not be found. It may have been obfuscated or shrunk.\n" +
                "You should consider preserving the method with its original name,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepMethodRule(reflectedClass.getName(), reflectedMethodName, signature) + "\n" +
                EMPTY_LINE);
        }
    }


    /**
     * Log a call to Class.getDeclaredMethods().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetDeclaredMethods(String invokingClassName,
                                             Class  reflectedClass    )
    {
        logGetMethods(invokingClassName, "getDeclaredMethods", reflectedClass);
    }


    /**
     * Log a call to Class.getMethods().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetMethods(String invokingClassName,
                                     Class  reflectedClass    )
    {
        logGetMethods(invokingClassName, "getMethods", reflectedClass);
    }


    /**
     * Log a call to one of the method listing methods on Class.
     *
     * @param invokingClassName
     * @param invokedReflectionMethodName
     * @param reflectedClass
     */
    private static void logGetMethods(String invokingClassName,
                                      String invokedReflectionMethodName,
                                      Class  reflectedClass     )
    {
        initializeMappings();
        if (classesWithObfuscatedMethods.contains(reflectedClass.getName()) &&
            !methodListingClasses.contains(reflectedClass.getName()) &&
            !isLibraryClass(reflectedClass))
        {
            methodListingClasses.add(reflectedClass.getName());
            log(
                "The class '" + originalClassName(invokingClassName) +
                "' is calling Class." + invokedReflectionMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) +
                "' to retrieve its methods.\n" +
                "You might consider preserving all methods with their original names,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepAllMethodsRule(reflectedClass) + "\n" +
                EMPTY_LINE);
        }
    }


    // Fields.


    /**
     * Log a failed call to Class.getField().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param reflectedFieldName
     */
    public static void logGetField(String invokingClassName,
                                   Class  reflectedClass,
                                   String reflectedFieldName)
    {
        logGetField(invokingClassName, "getField", reflectedClass, reflectedFieldName);
    }


    /**
     * Log a failed call to Class.getDeclaredField().
     *
     * @param invokingClassName
     * @param reflectedClass
     * @param reflectedFieldName
     */
    public static void logGetDeclaredField(String invokingClassName,
                                           Class  reflectedClass,
                                           String reflectedFieldName)
    {
        logGetField(invokingClassName, "getDeclaredField", reflectedClass, reflectedFieldName);
    }


    /**
     * Log a failed call to one of the field retrieving methods of Class.
     *
     * @param invokingClassName
     * @param invokedReflectionMethodName
     * @param reflectedClass
     * @param reflectedFieldName
     */
    private static void logGetField(String invokingClassName,
                                    String invokedReflectionMethodName,
                                    Class  reflectedClass,
                                    String reflectedFieldName )
    {
        Set<String> fields = missingFields.get(reflectedClass.getName());
        if (fields == null)
        {
            fields = new HashSet<String>();
            missingFields.put(reflectedClass.getName(), fields);
        }

        if ((!LOG_ONCE || !fields.contains(reflectedFieldName)) &&
            !isLibraryClass(reflectedClass))
        {
            fields.add(reflectedFieldName);
            log(
                "The class '" + originalClassName(invokingClassName) +
                "' is calling Class." + invokedReflectionMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) +
                "' to retrieve the field '" + reflectedFieldName + "',\n" +
                "but the latter could not be found. It may have been obfuscated or shrunk.\n" +
                "You should consider preserving the field with its original name,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepFieldRule(reflectedClass.getName(), reflectedFieldName) + "\n" +
                EMPTY_LINE);
        }
    }


    /**
     * Log a call to Class.getDeclaredFields().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetDeclaredFields(String invokingClassName,
                                            Class  reflectedClass    )
    {
        logGetFields(invokingClassName, "getDeclaredFields", reflectedClass);
    }


    /**
     * Log a call to Class.getFields().
     *
     * @param invokingClassName
     * @param reflectedClass
     */
    public static void logGetFields(String invokingClassName,
                                    Class  reflectedClass    )
    {
        logGetFields(invokingClassName, "getFields", reflectedClass);
    }


    /**
     * Log a call to one of the field listing methods on Class.
     *
     * @param invokingClassName
     * @param invokedReflectionMethodName
     * @param reflectedClass
     */
    private static void logGetFields(String invokingClassName,
                                     String invokedReflectionMethodName,
                                     Class  reflectedClass     )
    {
        initializeMappings();
        if (classesWithObfuscatedFields.contains(reflectedClass.getName()) &&
            !fieldListingCLasses.contains(reflectedClass.getName()) &&
            !isLibraryClass(reflectedClass))
        {
            fieldListingCLasses.add(reflectedClass.getName());
            log(
                "The class '" + originalClassName(invokingClassName) +
                "' is calling Class." + invokedReflectionMethodName + "\n" +
                "on class '" + originalClassName(reflectedClass) +
                "' to retrieve its fields.\n" +
                "You might consider preserving all fields with their original names,\n" +
                "with a setting like:\n" +
                EMPTY_LINE +
                keepAllFieldsRule(reflectedClass) + "\n" +
                EMPTY_LINE);
        }
    }


    // Implementations for Runnable.

    public void run()
    {
        printConfiguration();
    }


    private static void printConfiguration()
    {
        log("The following settings may help solving issues related to\n" +
            "missing classes, methods and/or fields:\n");

        for (String clazz : missingClasses)
        {
            log(keepClassRule(clazz) + "\n");
        }

        for (String clazz : missingConstructors.keySet())
        {
            for (MethodSignature constructor : missingConstructors.get(clazz))
            {
                log(keepConstructorRule(clazz, constructor) + "\n");
            }
        }

        for (String clazz : missingMethods.keySet())
        {
            for (MethodSignature method : missingMethods.get(clazz))
            {
                log(keepMethodRule(clazz, method.name, method) + "\n");
            }
        }

        for (String clazz : missingFields.keySet())
        {
            for (String field : missingFields.get(clazz))
            {
                log(keepFieldRule(clazz, field) + "\n");
            }
        }
    }


    // ProGuard rules.

    private static String keepClassRule(String className)
    {
        return "-keep class " + className;
    }


    private static String keepConstructorRule(String          className,
                                              MethodSignature constructorParameters)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    public <init>(" + originalSignature(constructorParameters) + ");\n" +
               "}";
    }


    private static String keepMethodRule(String          className,
                                         String          methodName,
                                         MethodSignature constructorParameters)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    *** " + methodName + "(" + originalSignature(constructorParameters) + ");\n" +
               "}";
    }


    private static String keepFieldRule(String className,
                                        String fieldName)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    *** " + fieldName + ";\n" +
               "}";
    }


    private static String keepAllConstructorsRule(Class className)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    <init>(...);\n" +
               "}";
    }


    private static String keepAllMethodsRule(Class className)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    <methods>;\n" +
               "}";
    }


    private static String keepAllFieldsRule(Class className)
    {
        return "-keepclassmembers class " + originalClassName(className) + " {\n" +
               "    <fields>;\n" +
               "}";
    }


    private static String originalClassName(Class className)
    {
        return originalClassName(className.getName());
    }


    private static String originalClassName(String className)
    {
        initializeMappings();
        String originalClassName = classNameMap.get(className);
        return originalClassName != null ? originalClassName : className;
    }


    /**
     * Simple heuristic to see if the given class is a library class or not.
     *
     * @param clazz
     * @return
     */
    private static boolean isLibraryClass(Class clazz)
    {
        return clazz.getClassLoader() == String.class.getClassLoader();
    }


    /**
     * Log a message, either on the Android Logcat, if available, or on the
     * Standard error outputstream otherwise.
     *
     * @param message the message to be logged.
     */
    private static void log(String message)
    {
        if (logMethod != null)
        {
            try
            {
                logMethod.invoke(null, LOG_TAG, message);
            }
            catch (Exception e)
            {
                System.err.println(message);
            }
        }
        else
        {
            System.err.println(message);
        }
    }


    private  static void initializeMappings()
    {
        if (classNameMap == null)
        {
            classNameMap                 = new HashMap<String, String> ();
            classesWithObfuscatedMethods = new HashSet<String>         ();
            classesWithObfuscatedFields  = new HashSet<String>         ();

            String line;
            try
            {
                BufferedReader reader =
                    new BufferedReader(
                        new InputStreamReader(
                            ConfigurationLogger.class.getClassLoader().getResourceAsStream(CLASS_MAP_FILENAME)));

                while ((line = reader.readLine()) != null)
                {
                    StringTokenizer tokenizer            = new StringTokenizer(line, ",");
                    String          originalClassName    = tokenizer.nextToken();
                    String          obfuscatedClassName  = tokenizer.nextToken();
                    boolean         hasObfuscatedMethods = tokenizer.nextToken().equals("1");
                    boolean         hasObfuscatedFields  = tokenizer.nextToken().equals("1");

                    classNameMap.put(obfuscatedClassName, originalClassName);

                    if (hasObfuscatedMethods)
                    {
                        classesWithObfuscatedMethods.add(obfuscatedClassName);
                    }

                    if (hasObfuscatedFields)
                    {
                        classesWithObfuscatedFields.add(obfuscatedClassName);
                    }
                }
                reader.close();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }


    private static String originalSignature(MethodSignature signature)
    {
        StringBuilder stringBuilder = new StringBuilder();
        boolean       first         = true;
        for (String clazz : signature.parameters)
        {
            if (first)
            {
                first = false;
            }
            else
            {
                stringBuilder.append(",");
            }
            stringBuilder.append(originalClassName(clazz));
        }
        return stringBuilder.toString();
    }


    public static class MethodSignature
    {
        private String   name;
        private String[] parameters;


        public MethodSignature(String name, Class[] parameters)
        {
            this.name       = name;
            this.parameters = new String[parameters.length];
            for (int i = 0; i < parameters.length; i++)
            {
                this.parameters[i] = parameters[i].getName();
            }
        }


        // Implementations for Object.

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            MethodSignature that = (MethodSignature)o;

            if (!name.equals(that.name)) return false;
            return Arrays.equals(parameters, that.parameters);
        }


        public int hashCode()
        {
            int result = name.hashCode();
            result = 31 * result + Arrays.hashCode(parameters);
            return result;
        }
    }
}
