package bytecodeAnnotations;

import java.io.*;
import java.nio.file.*;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;

/**
 * Dodaje wpisy „wejścia” do wszystkich metod klasy, które mają adnotację LogEntry.
 * @version 1.22 2023-12-01
 * @author Cay Horstmann
 */
public class EntryLogger extends ClassVisitor
{
   private String className;
   
   /**
    * Tworzy obiekt EntryLogger, który dodaje kod zapisu komunikatów dziennika
	* do wszystkich metod oznaczonych adnotacją należących do podanej klasy
    */
   public EntryLogger(ClassWriter writer, String className)
   {
      super(Opcodes.ASM8, writer);
      this.className = className;
   }
   
   public MethodVisitor visitMethod(int access, String methodName, String desc, 
         String signature, String[] exceptions) 
   {
      MethodVisitor mv = cv.visitMethod(access, methodName, desc, signature, exceptions);   
      return new AdviceAdapter(Opcodes.ASM8, mv, access, methodName, desc) 
      {          
         private String loggerName;
          
         public AnnotationVisitor visitAnnotation(String desc, boolean visible) 
         {
            return new AnnotationVisitor(Opcodes.ASM8) 
               {
                  public void visit(String name, Object value) 
                  {
                     if (desc.equals("LbytecodeAnnotations/LogEntry;") 
                           && name.equals("logger")) 
                        loggerName = value.toString();                    
                  }
               };
         }   
          
         public void onMethodEnter() 
         {
            if (loggerName != null) 
            {
               /*
               visitLdcInsn(loggerName);
               visitMethodInsn(INVOKESTATIC, "java/util/logging/Logger", "getLogger", 
                  "(Ljava/lang/String;)Ljava/util/logging/Logger;", false);
               visitLdcInsn(className);
               visitLdcInsn(methodName);
               visitMethodInsn(INVOKEVIRTUAL, "java/util/logging/Logger", "entering", 
                  "(Ljava/lang/String;Ljava/lang/String;)V", false);
               */
               visitLdcInsn(loggerName);
               visitMethodInsn(INVOKESTATIC, "java/lang/System", "getLogger", 
                  "(Ljava/lang/String;)Ljava/lang/System$Logger;", false);
               visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System$Logger$Level", "INFO", 
                  "Ljava/lang/System$Logger$Level;");
               visitLdcInsn("Entering {0}.{1}");

               // Tworzy tablicę obiektów klasy Object o długości 2
               mv.visitInsn(Opcodes.ICONST_2);  // długość tablicy
               mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");  // typ tablicy

               // Dodaje dwa obiekty do tablicy
               mv.visitInsn(Opcodes.DUP);  // powiela referencję do tablicy
               mv.visitInsn(Opcodes.ICONST_0);  // indeks 0
               visitLdcInsn(className);
               mv.visitInsn(Opcodes.AASTORE);  // zapis do tablicy

               mv.visitInsn(Opcodes.DUP);  // powiela referencję do tablicy
               mv.visitInsn(Opcodes.ICONST_1);  // indeks 1
               visitLdcInsn(methodName);
               mv.visitInsn(Opcodes.AASTORE);  // zapis do tablicy
                              
               visitMethodInsn(INVOKEINTERFACE, "java/lang/System$Logger", "log", 
                  "(Ljava/lang/System$Logger$Level;Ljava/lang/String;[Ljava/lang/Object;)V", 
                  true);
               loggerName = null;
            }                  
         }
      };
   }

   /**
    * Dodaje kod zapisu komunikatów dziennika do podanej klasy.
    * @param args nazwa pliku klasy
    */
   public static void main(String[] args) throws IOException
   {
      if (args.length == 0)
      {
         System.out.println("USAGE: java bytecodeAnnotations.EntryLogger classfile");
         System.exit(1);
      }
      Path path = Path.of(args[0]);
      var reader = new ClassReader(Files.newInputStream(path));
      var writer = new ClassWriter(
         ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
      var entryLogger = new EntryLogger(writer, 
         path.toString().replace(".class", "").replaceAll("[/\\\\]", "."));
      reader.accept(entryLogger, ClassReader.EXPAND_FRAMES);
      Files.write(Path.of(args[0]), writer.toByteArray());
   }   
}
