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

import proguard.classfile.*;
import proguard.classfile.attribute.*;
import proguard.classfile.attribute.visitor.AttributeVisitor;
import proguard.classfile.util.*;
import proguard.classfile.visitor.*;
import proguard.optimize.peephole.LineNumberLinearizer;

import java.io.PrintStream;
import java.util.Stack;


/**
 * This ClassVisitor prints out the renamed classes and class members with
 * their old names and new names.
 *
 * @see ClassRenamer
 *
 * @author Eric Lafortune
 */
public class MappingPrinter
extends      SimplifiedVisitor
implements   ClassVisitor,
             MemberVisitor,
             AttributeVisitor
{
    private final PrintStream ps;

    // A field serving as a return value for the visitor methods.
    private boolean printed;


    /**
     * Creates a new MappingPrinter that prints to <code>System.out</code>.
     */
    public MappingPrinter()
    {
        this(System.out);
    }


    /**
     * Creates a new MappingPrinter that prints to the given stream.
     * @param printStream the stream to which to print
     */
    public MappingPrinter(PrintStream printStream)
    {
        this.ps = printStream;
    }


    // Implementations for ClassVisitor.

    public void visitProgramClass(ProgramClass programClass)
    {
        String name    = programClass.getName();
        String newName = ClassObfuscator.newClassName(programClass);

        // Print out the class mapping.
        ps.println(ClassUtil.externalClassName(name) +
                   " -> " +
                   ClassUtil.externalClassName(newName) +
                   ":");

        // Print out the class members.
        programClass.fieldsAccept(this);
        programClass.methodsAccept(this);
    }


    // Implementations for MemberVisitor.

    public void visitProgramField(ProgramClass programClass, ProgramField programField)
    {
        String fieldName           = programField.getName(programClass);
        String obfuscatedFieldName = MemberObfuscator.newMemberName(programField);
        if (obfuscatedFieldName == null)
        {
            obfuscatedFieldName = fieldName;
        }

        // Print out the field mapping.
        ps.println("    " +
                   ClassUtil.externalType(programField.getDescriptor(programClass)) + " " +
                   fieldName +
                   " -> " +
                   obfuscatedFieldName);
    }


    public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
    {
        String methodName           = programMethod.getName(programClass);
        String obfuscatedMethodName = MemberObfuscator.newMemberName(programMethod);
        if (obfuscatedMethodName == null)
        {
            obfuscatedMethodName = methodName;
        }

        // Print out the method mapping, if it has line numbers.
        printed = false;
        programMethod.attributesAccept(programClass, this);

        // Otherwise print out the method mapping without line numbers.
        if (!printed)
        {
            ps.println("    " +
                       ClassUtil.externalMethodReturnType(programMethod.getDescriptor(programClass)) + " " +
                       methodName                                                                    + JavaConstants.METHOD_ARGUMENTS_OPEN  +
                       ClassUtil.externalMethodArguments(programMethod.getDescriptor(programClass))  + JavaConstants.METHOD_ARGUMENTS_CLOSE +
                       " -> " +
                       obfuscatedMethodName);
        }
    }


    // Implementations for AttributeVisitor.

    public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}


    public void visitCodeAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute)
    {
        codeAttribute.attributesAccept(clazz, method, this);
    }


    public void visitLineNumberTableAttribute(Clazz clazz, Method method, CodeAttribute codeAttribute, LineNumberTableAttribute lineNumberTableAttribute)
    {
        LineNumberInfo[] lineNumberTable       = lineNumberTableAttribute.lineNumberTable;
        int              lineNumberTableLength = lineNumberTableAttribute.u2lineNumberTableLength;

        String methodName           = method.getName(clazz);
        String methodDescriptor     = method.getDescriptor(clazz);
        String obfuscatedMethodName = MemberObfuscator.newMemberName(method);
        if (obfuscatedMethodName == null)
        {
            obfuscatedMethodName = methodName;
        }

        int lowestLineNumber  = lineNumberTableAttribute.getLowestLineNumber();
        int highestLineNumber = lineNumberTableAttribute.getHighestLineNumber();

        // Does the method have any local line numbers at all?
        if (lineNumberTableAttribute.getSource(codeAttribute.u4codeLength)  == null)
        {
            if (lowestLineNumber > 0)
            {
                // Print out the line number range of the method,
                // ignoring line numbers of any inlined methods.
                ps.println("    " +
                           lowestLineNumber                                                + ":" +
                           highestLineNumber                                               + ":" +
                           ClassUtil.externalMethodReturnType(method.getDescriptor(clazz)) + " " +
                           methodName                                                      + JavaConstants.METHOD_ARGUMENTS_OPEN  +
                           ClassUtil.externalMethodArguments(method.getDescriptor(clazz))  + JavaConstants.METHOD_ARGUMENTS_CLOSE +
                           " -> " +
                           obfuscatedMethodName);
            }
            else
            {
                // Print out the method mapping without line numbers.
                ps.println("    " +
                           ClassUtil.externalMethodReturnType(method.getDescriptor(clazz)) + " " +
                           methodName                                                      + JavaConstants.METHOD_ARGUMENTS_OPEN  +
                           ClassUtil.externalMethodArguments(method.getDescriptor(clazz))  + JavaConstants.METHOD_ARGUMENTS_CLOSE +
                           " -> " +
                           obfuscatedMethodName);
            }
        }

        // Print out the line numbers of any inlined methods and their
        // enclosing methods.
        Stack enclosingLineNumbers = new Stack();

        LineNumberInfo previousInfo = new LineNumberInfo(0, 0);

        for (int index = 0; index < lineNumberTableLength; index++)
        {
            LineNumberInfo info = lineNumberTable[index];

            // Are we entering or exiting an inlined block (or a merged block)?
            // We're testing on the identities out of convenience.
            String previousSource = previousInfo.getSource();
            String source         = info.getSource();
            if (source != previousSource)
            {
                // Are we entering or exiting the block?
                int previousLineNumber = previousInfo.u2lineNumber;
                int lineNumber         = info.u2lineNumber;
                if (lineNumber > previousLineNumber)
                {
                    // We're entering an inlined block.
                    // Accumulate its enclosing line numbers, so they can be
                    // printed out for each inlined block.
                    if (index > 0)
                    {
                        enclosingLineNumbers.push(previousInfo);
                    }

                    printInlinedMethodMapping(clazz.getName(),
                                              methodName,
                                              methodDescriptor,
                                              info,
                                              enclosingLineNumbers,
                                              obfuscatedMethodName);
                }
                // TODO: There appear to be cases where the stack is empty at this point, so we've added a check.
                else if (!enclosingLineNumbers.isEmpty())
                {
                    // We're exiting an inlined block.
                    // Pop its enclosing line number.
                    enclosingLineNumbers.pop();
                }
            }

            previousInfo = info;
        }

        printed = true;
    }


    // Small utility methods.

    /**
     * Prints out the mapping of the specified inlined methods and its
     * enclosing methods.
     */
    private void printInlinedMethodMapping(String         className,
                                           String         methodName,
                                           String         methodDescriptor,
                                           LineNumberInfo inlinedInfo,
                                           Stack          enclosingLineNumbers,
                                           String         obfuscatedMethodName)
    {
        String source = inlinedInfo.getSource();

        // Parse the information from the source string of the
        // inlined method.
        int separatorIndex1 = source.indexOf('.');
        int separatorIndex2 = source.indexOf('(', separatorIndex1 + 1);
        int separatorIndex3 = source.indexOf(':', separatorIndex2 + 1);
        int separatorIndex4 = source.indexOf(':', separatorIndex3 + 1);

        String inlinedClassName        = source.substring(0, separatorIndex1);
        String inlinedMethodName       = source.substring(separatorIndex1 + 1, separatorIndex2);
        String inlinedMethodDescriptor = source.substring(separatorIndex2, separatorIndex3);
        String inlinedRange            = source.substring(separatorIndex3);

        int startLineNumber = Integer.parseInt(source.substring(separatorIndex3 + 1, separatorIndex4));
        int endLineNumber   = Integer.parseInt(source.substring(separatorIndex4 + 1));

        // Compute the shifted line number range.
        int shiftedStartLineNumber = inlinedInfo.u2lineNumber;
        int shiftedEndLineNumber   = shiftedStartLineNumber + endLineNumber - startLineNumber;

        // Print out the line number range of the inlined method.
        ps.println("    " +
                   shiftedStartLineNumber                                      + ":" +
                   shiftedEndLineNumber                                        + ":" +
                   ClassUtil.externalMethodReturnType(inlinedMethodDescriptor) + " " +
                   (inlinedClassName.equals(className) ? "" :
                   ClassUtil.externalClassName(inlinedClassName)               + JavaConstants.PACKAGE_SEPARATOR)     +
                   inlinedMethodName                                           + JavaConstants.METHOD_ARGUMENTS_OPEN  +
                   ClassUtil.externalMethodArguments(inlinedMethodDescriptor)  + JavaConstants.METHOD_ARGUMENTS_CLOSE +
                   inlinedRange                                                + " -> " +
                   obfuscatedMethodName);

        // Print out the line numbers of the accumulated enclosing
        // methods.
        for (int enclosingIndex = enclosingLineNumbers.size()-1; enclosingIndex >= 0; enclosingIndex--)
        {
            LineNumberInfo enclosingInfo =
                (LineNumberInfo)enclosingLineNumbers.get(enclosingIndex);

            printEnclosingMethodMapping(className,
                                        methodName,
                                        methodDescriptor,
                                        shiftedStartLineNumber + ":" +
                                        shiftedEndLineNumber,
                                        enclosingInfo,
                                        obfuscatedMethodName);


        }
    }


    /**
     * Prints out the mapping of the specified enclosing method.
     */
    private void printEnclosingMethodMapping(String         className,
                                             String         methodName,
                                             String         methodDescriptor,
                                             String         shiftedRange,
                                             LineNumberInfo enclosingInfo,
                                             String         obfuscatedMethodName)
    {
        // Parse the information from the source string of the enclosing
        // method.
        String enclosingSource = enclosingInfo.getSource();

        String enclosingClassName;
        String enclosingMethodName;
        String enclosingMethodDescriptor;
        int    enclosingLineNumber;

        if (enclosingSource == null)
        {
            enclosingClassName        = className;
            enclosingMethodName       = methodName;
            enclosingMethodDescriptor = methodDescriptor;
            enclosingLineNumber       = enclosingInfo.u2lineNumber;
        }
        else
        {
            int enclosingSeparatorIndex1 = enclosingSource.indexOf('.');
            int enclosingSeparatorIndex2 = enclosingSource.indexOf('(', enclosingSeparatorIndex1 + 1);
            int enclosingSeparatorIndex3 = enclosingSource.indexOf(':', enclosingSeparatorIndex2 + 1);
            int enclosingSeparatorIndex4 = enclosingSource.indexOf(':', enclosingSeparatorIndex3 + 1);

            // We need the first line number to correct the shifted enclosing
            // line number back to its original range.
            int firstLineNumber = Integer.parseInt(enclosingSource.substring(enclosingSeparatorIndex3 + 1, enclosingSeparatorIndex4));

            enclosingClassName        = enclosingSource.substring(0, enclosingSeparatorIndex1);
            enclosingMethodName       = enclosingSource.substring(enclosingSeparatorIndex1 + 1, enclosingSeparatorIndex2);
            enclosingMethodDescriptor = enclosingSource.substring(enclosingSeparatorIndex2, enclosingSeparatorIndex3);
            enclosingLineNumber       = (enclosingInfo.u2lineNumber - firstLineNumber) % LineNumberLinearizer.SHIFT_ROUNDING + firstLineNumber;
        }

        // Print out the line number of the enclosing method.
        ps.println("    " +
                   shiftedRange                                                  + ":" +
                   ClassUtil.externalMethodReturnType(enclosingMethodDescriptor) + " " +
                   (enclosingClassName.equals(className) ? "" :
                   ClassUtil.externalClassName(enclosingClassName)               + JavaConstants.PACKAGE_SEPARATOR)     +
                   enclosingMethodName                                           + JavaConstants.METHOD_ARGUMENTS_OPEN  +
                   ClassUtil.externalMethodArguments(enclosingMethodDescriptor)  + JavaConstants.METHOD_ARGUMENTS_CLOSE + ":" +
                   enclosingLineNumber                                           + " -> " +
                   obfuscatedMethodName);
    }
}
