/*******************************************************************************
 * Copyright (c) 2014 Jeff Martin.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Jeff Martin - initial API and implementation
 ******************************************************************************/
package cuchaz.enigma.bytecode;

import java.util.Collection;

import javassist.CtClass;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.ConstPool;
import javassist.bytecode.Descriptor;
import javassist.bytecode.EnclosingMethodAttribute;
import javassist.bytecode.InnerClassesAttribute;
import cuchaz.enigma.analysis.JarIndex;
import cuchaz.enigma.mapping.BehaviorEntry;
import cuchaz.enigma.mapping.ClassEntry;

public class InnerClassWriter
{
	private JarIndex m_jarIndex;
	
	public InnerClassWriter( JarIndex jarIndex )
	{
		m_jarIndex = jarIndex;
	}
	
	public void write( CtClass c )
	{
		// get the outer class name
		String obfClassName = Descriptor.toJvmName( c.getName() );
		String obfOuterClassName = m_jarIndex.getOuterClass( obfClassName );
		if( obfOuterClassName == null )
		{
			// this is an outer class
			obfOuterClassName = obfClassName;
		}
		else
		{
			// this is an inner class, rename it to outer$inner
			ClassEntry obfClassEntry = new ClassEntry( obfOuterClassName + "$" + new ClassEntry( obfClassName ).getSimpleName() );
			c.setName( obfClassEntry.getName() );
			
			BehaviorEntry caller = m_jarIndex.getAnonymousClassCaller( obfClassName );
			if( caller != null )
			{
				// write the enclosing method attribute
				c.getClassFile().addAttribute( new EnclosingMethodAttribute(
					c.getClassFile().getConstPool(),
					caller.getClassName(),
					caller.getName(),
					caller.getSignature()
				) );
			}
		}
		
		// write the inner classes if needed
		Collection<String> obfInnerClassNames = m_jarIndex.getInnerClasses( obfOuterClassName );
		if( obfInnerClassNames != null && !obfInnerClassNames.isEmpty() )
		{
			writeInnerClasses( c, obfOuterClassName, obfInnerClassNames );
		}
	}
	
	private void writeInnerClasses( CtClass c, String obfOuterClassName, Collection<String> obfInnerClassNames )
	{
		InnerClassesAttribute attr = new InnerClassesAttribute( c.getClassFile().getConstPool() );
		c.getClassFile().addAttribute( attr );
		for( String obfInnerClassName : obfInnerClassNames )
		{
			// get the new inner class name
			ClassEntry obfClassEntry = new ClassEntry( obfOuterClassName + "$" + new ClassEntry( obfInnerClassName ).getSimpleName() );
			
			// here's what the JVM spec says about the InnerClasses attribute
			// append( inner, outer of inner if inner is member of outer 0 ow, name after $ if inner not anonymous 0 ow, flags ); 
			
			// update the attribute with this inner class
			ConstPool constPool = c.getClassFile().getConstPool();
			int innerClassIndex = constPool.addClassInfo( obfClassEntry.getName() );
			int outerClassIndex = 0;
			int innerClassSimpleNameIndex = 0;
			if( !m_jarIndex.isAnonymousClass( obfInnerClassName ) )
			{
				outerClassIndex = constPool.addClassInfo( obfClassEntry.getOuterClassName() );
				innerClassSimpleNameIndex = constPool.addUtf8Info( obfClassEntry.getInnerClassName() );
			}
			
			attr.append(
				innerClassIndex,
				outerClassIndex,
				innerClassSimpleNameIndex,
				c.getClassFile().getAccessFlags() & ~AccessFlag.SUPER
			);
			
			/* DEBUG
			System.out.println( String.format( "\tOBF: %s -> ATTR: %s,%s,%s (replace %s with %s)",
				obfClassEntry,
				attr.outerClass( attr.tableLength() - 1 ),
				attr.innerClass( attr.tableLength() - 1 ),
				attr.innerName( attr.tableLength() - 1 ),
				obfInnerClassName, obfClassEntry.getName()
			) );
			*/
			
			// make sure the outer class references only the new inner class names
			c.replaceClassName( obfInnerClassName, obfClassEntry.getName() );
		}
	}
}

@@ -1761,16 +1761,71 @@
 tribute%0A
+%09%09%09%09if( caller.getName().equals( %22%3Cclinit%3E%22 ) )%0A%09%09%09%09%7B%0A%09
 %09%09%09%09c.ge
@@ -1882,24 +1882,25 @@
 ibute(%0A%09%09%09%09%09
+%09
 c.getClassFi
@@ -1917,32 +1917,33 @@
 nstPool(),%0A%09%09%09%09%09
+%09
 caller.getClassN
@@ -1943,26 +1943,162 @@
 tClassName()
+%0A%09%09%09%09%09) );%0A%09%09%09%09%7D%0A%09%09%09%09else%0A%09%09%09%09%7B%0A%09%09%09%09%09c.getClassFile().addAttribute( new EnclosingMethodAttribute(%0A%09%09%09%09%09%09c.getClassFile().getConstPool()
 ,%0A
+%09
 %09%09%09%09%09caller.
@@ -2104,16 +2104,21 @@
 .get
+Class
 Name(),%0A
 %09%09%09%09
@@ -2109,24 +2109,49 @@
 lassName(),%0A
+%09%09%09%09%09%09caller.getName(),%0A%09
 %09%09%09%09%09caller.
@@ -2169,21 +2169,28 @@
 e()%0A%09%09%09%09
+%09
 ) );%0A
+%09%09%09%09%7D%0A
 %09%09%09%7D%0A%09%09%7D

fix bug with anonymous classes in class initializers