// --------------------------------------------------------------------------------------------------
//  Copyright (c) 2016 Microsoft Corporation
//  
//  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
//  associated documentation files (the "Software"), to deal in the Software without restriction,
//  including without limitation the rights to use, copy, modify, merge, publish, distribute,
//  sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//  
//  The above copyright notice and this permission notice shall be included in all copies or
//  substantial portions of the Software.
//  
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
//  NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// --------------------------------------------------------------------------------------------------

package com.microsoft.Malmo;

import net.minecraft.launchwrapper.IClassTransformer;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

public class OverclockingClassTransformer implements IClassTransformer
{
    enum transformType { SERVER, RENDERER }
    
    @Override
    public byte[] transform(String name, String transformedName, byte[] basicClass)
    {
        boolean isObfuscated = !name.equals(transformedName);
        if (transformedName.equals("net.minecraft.server.MinecraftServer"))
            return transform(basicClass, isObfuscated, transformType.SERVER);
        else if (transformedName.equals("net.minecraft.client.Minecraft"))
            return transform(basicClass, isObfuscated, transformType.RENDERER);
        else
            return basicClass;
    }

    private static byte[] transform(byte[] serverClass, boolean isObfuscated, transformType type)
    {
        System.out.println("MALMO: Attempting to transform MinecraftServer");
        try
        {
            ClassNode cnode = new ClassNode();
            ClassReader creader = new ClassReader(serverClass);
            creader.accept(cnode, 0);
            
            switch (type)
            {
            case SERVER:
                overclockServer(cnode, isObfuscated);
                break;
            case RENDERER:
                overclockRenderer(cnode, isObfuscated);
                break;
            }
            
            ClassWriter cwriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
            cnode.accept(cwriter);
            return cwriter.toByteArray();
        }
        catch (Exception e)
        {
            System.out.println("MALMO FAILED to transform MinecraftServer - overclocking not available!");
        }
        return serverClass;
    }
    
    private static void overclockServer(ClassNode node, boolean isObfuscated)
    {
        // We're attempting to replace this code (from the heart of MinecraftServer.run):
        /*       
            {
                while (i > 50L)
                {
                    i -= 50L;
                    this.tick();
                }
            }
    
            Thread.sleep(Math.max(1L, 50L - i));
        */

        // With this:
        /*       
        {
            while (i > TimeHelper.serverTickLength)
            {
                i -= TimeHelper.serverTickLength;
                this.tick();
            }
        }

        Thread.sleep(Math.max(1L, TimeHelper.serverTickLength - i));
    */
        // This allows us to alter the tick length via TimeHelper.
        
        final String methodName = isObfuscated ? "minecraft" : "run";
        final String methodDescriptor = "()V"; // No params, returns void.

        System.out.println("MALMO: Found MinecraftServer, attempting to transform it");

        for (MethodNode method : node.methods)
        {
            if (method.name.equals(methodName) && method.desc.equals(methodDescriptor))
            {
                System.out.println("MALMO: Found MinecraftServer.run() method, attempting to transform it");
                for (AbstractInsnNode instruction : method.instructions.toArray())
                {
                    if (instruction.getOpcode() == Opcodes.LDC)
                    {
                        Object cst = ((LdcInsnNode)instruction).cst;
                        if ((cst instanceof Long) && (Long)cst == 50)
                        {
                            System.out.println("MALMO: Transforming LDC");
                            AbstractInsnNode replacement = new FieldInsnNode(Opcodes.GETSTATIC, "com/microsoft/Malmo/Utils/TimeHelper", "serverTickLength", "J");
                            method.instructions.set(instruction, replacement);
                        }
                    }
                }
            }
        }
    }
    
    private static void overclockRenderer(ClassNode node, boolean isObfuscated)
    {
        // We're attempting to turn this line from Minecraft.runGameLoop:
        //          this.updateDisplay();
        // into this:
        //          TimeHelper.updateDisplay();
        // TimeHelper's method then decides whether or not to pass the call on to Minecraft.updateDisplay().
        
        final String methodName = isObfuscated ? "av" : "runGameLoop";
        final String methodDescriptor = "()V"; // No params, returns void.

        System.out.println("MALMO: Found Minecraft, attempting to transform it");

        for (MethodNode method : node.methods)
        {
            if (method.name.equals(methodName) && method.desc.equals(methodDescriptor))
            {
                System.out.println("MALMO: Found MinecraftServer.run() method, attempting to transform it");
                for (AbstractInsnNode instruction : method.instructions.toArray())
                {
                    if (instruction.getOpcode() == Opcodes.INVOKEVIRTUAL)
                    {
                        MethodInsnNode visitMethodNode = (MethodInsnNode)instruction;
                        if (visitMethodNode.name.equals("updateDisplay"))
                        {
                            visitMethodNode.owner = "com/microsoft/Malmo/Utils/TimeHelper";
                            visitMethodNode.setOpcode(Opcodes.INVOKESTATIC);
                            method.instructions.remove(visitMethodNode.getPrevious());  // ALOAD 0 not needed for static invocation.
                            System.out.println("MALMO: Hooked into call to Minecraft.updateDisplay()");
                        }
                    }
                }
            }
        }
    }
}

@@ -4290,37 +4290,8 @@
 me =
- isObfuscated ? %22minecraft%22 :
  %22ru
@@ -5937,17 +5937,17 @@
 ted ? %22a
-v
+s
 %22 : %22run
@@ -6329,34 +6329,36 @@
 nd Minecraft
-Server.run
+.runGameLoop
 () method, a
@@ -6725,16 +6725,37 @@
 .equals(
+isObfuscated ? %22h%22 : 
 %22updateD
@@ -6882,16 +6882,194 @@
 elper%22;%0A
+                            if (isObfuscated)%0A                            %7B%0A                                visitMethodNode.name = %22updateDisplay%22;%0A                            %7D%0A
         

Fix invalid deobfuscated names in transformer