package org.groovy.debug.hotswap;

import com.tonicsystems.jarjar.asm.*;

import java.lang.String;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.security.ProtectionDomain;

/**
 * Inspired by GroovyEclipse hot-swap hack (http://jira.codehaus.org/browse/GRECLIPSE-588)
 * Removes all timestamp-related Groovy fields on class loading
 * Also clears Groovy's call site cache
 *
 * @author Andy Clement
 */
@SuppressWarnings({"UtilityClassWithoutPrivateConstructor", "UnusedDeclaration"})
public class ResetAgent {
  private static final String timeStampFieldStart = "__timeStamp__239_neverHappen";
  private static final byte[] timeStampFieldStartBytes;
  
  static {
    timeStampFieldStartBytes = new byte[timeStampFieldStart.length()];
    for (int i = 0; i < timeStampFieldStart.length(); i++) {
      timeStampFieldStartBytes[i] = (byte)timeStampFieldStart.charAt(i);
    }
  }

  private static boolean initialized;

  public static void premain(String options, Instrumentation inst) {
    // Handle duplicate agents
    if (initialized) {
      return;
    }
    initialized = true;
    inst.addTransformer(new ClassFileTransformer() {
      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
          if (className == null || className.indexOf('$') >= 0) {
            // non-toplevel Groovy classes don't have timestamp fields, so don't Java classes and lambdas
            // let's not care about presumably rare dollar-named Groovy classes
            return null;
          }

          if (classBeingRedefined != null) {
            Field callSiteArrayField = classBeingRedefined.getDeclaredField("$callSiteArray");
            callSiteArrayField.setAccessible(true);
            callSiteArrayField.set(null, null);
          }
          return removeTimestampField(classfileBuffer);
        } catch (Throwable ignored) {
          return null;
        }
      }

    });
  }

  private static boolean matches(byte[] array, byte[] subArray, int start) {
    for (int i = 1; i < subArray.length; i++) {
      if (array[start + i] != subArray[i]) {
        return false;
      }
    }
    return true;
  }

  private static boolean containsSubArray(byte[] array, byte[] subArray) {
    int maxLength = array.length - subArray.length;
    for (int i = 0; i < maxLength; i++) {
      if (array[i] == subArray[0] && matches(array, subArray, i)) {
        return true;
      }
    }
    return false;
  }

  private static byte[] removeTimestampField(byte[] newBytes) {
    if (!containsSubArray(newBytes, timeStampFieldStartBytes)) {
      return null;
    }
    

    final boolean[] changed = new boolean[]{false};
    final ClassWriter writer = new ClassWriter(0);
    new ClassReader(newBytes).accept(new TimestampFieldRemover(writer, changed), 0);
    if (changed[0]) {
      return writer.toByteArray();
    }
    return null;
  }

  private static class TimestampFieldRemover extends ClassAdapter {
    private final boolean[] changed;

    public TimestampFieldRemover(ClassWriter writer, boolean[] changed) {
      super(writer);
      this.changed = changed;
    }

    @Override
    public FieldVisitor visitField(int i, String name, String s1, String s2, Object o) {
      if (name.startsWith(timeStampFieldStart)) {
        //remove the field
        changed[0] = true;
        return null;
      }
      return super.visitField(i, name, s1, s2, o);
    }

    @Override
    public MethodVisitor visitMethod(int i, String name, String s1, String s2, String[] strings) {
      final MethodVisitor mw = super.visitMethod(i, name, s1, s2, strings);
      if ("<clinit>".equals(name)) {
        //remove field's static initialization
        return new MethodAdapter(mw) {
          @Override
          public void visitFieldInsn(int opCode, String s, String name, String desc) {
            if (name.startsWith(timeStampFieldStart) && opCode == Opcodes.PUTSTATIC) {
              visitInsn(Type.LONG_TYPE.getDescriptor().equals(desc) ? Opcodes.POP2 : Opcodes.POP);
            } else {
              super.visitFieldInsn(opCode, s, name, desc);
            }
          }
        };
      }
      return mw;
    }
  }
}
