/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Hashtable;
import java.util.StringTokenizer;
import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ConstantValue;
import org.apache.bcel.classfile.Deprecated;
import org.apache.bcel.classfile.ExceptionTable;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Synthetic;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.BranchHandle;
import org.apache.bcel.generic.BranchInstruction;
import org.apache.bcel.generic.CodeExceptionGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.LineNumberGen;
import org.apache.bcel.generic.LocalVariableGen;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.Select;
import org.apache.bcel.generic.TABLESWITCH;

/**
 * Disassemble Java class object into the <a href="http://jasmin.sourceforge.net">
 * Jasmin</a> format.
 *
 * @version $Id$
 * @author <A HREF="mailto:m.dahm@gmx.de">M. Dahm</A>
 */
public class JasminVisitor extends org.apache.bcel.classfile.EmptyVisitor {
  private JavaClass       clazz;
  private PrintWriter     out;
  private String          class_name;
  private ConstantPoolGen cp;

  public JasminVisitor(JavaClass clazz, OutputStream out) {
    this.clazz = clazz;
    this.out   = new PrintWriter(out);
    class_name = clazz.getClassName();
    cp = new ConstantPoolGen(clazz.getConstantPool());
  }

  /**
   * Start traversal using DefaultVisitor pattern.
   */
  public void disassemble() {
    new org.apache.bcel.classfile.DescendingVisitor(clazz, this).visit();
    out.close();
  }

  @Override
  public void visitJavaClass(JavaClass clazz) {
    out.println(";; Produced by JasminVisitor (BCEL)");
    out.println(";; http://commons.apache.org/bcel/");
    out.println(";; " + new Date() + "\n");

    out.println(".source " + clazz.getSourceFileName());
    out.println("." + Utility.classOrInterface(clazz.getAccessFlags()) + " " +
        Utility.accessToString(clazz.getAccessFlags(), true) +
        " " + clazz.getClassName().replace('.', '/'));
    out.println(".super " + clazz.getSuperclassName().replace('.', '/'));

    String[] interfaces = clazz.getInterfaceNames();

    for(int i=0; i < interfaces.length; i++) {
        out.println(".implements " + interfaces[i].replace('.', '/'));
    }

    out.print("\n");
  }

  @Override
  public void visitField(Field field) {
    out.print(".field " + Utility.accessToString(field.getAccessFlags()) +
        " \"" +field.getName() + "\"" + field.getSignature());
    if(field.getAttributes().length == 0) {
        out.print("\n");
    }
  }

  @Override
  public void visitConstantValue(ConstantValue cv) {
    out.println(" = " + cv);
  }

  private Method _method;

  /**
   * Unfortunately Jasmin expects ".end method" after each method. Thus we've to check
   * for every of the method's attributes if it's the last one and print ".end method"
   * then.
   */
  private void printEndMethod(Attribute attr) {
    Attribute[] attributes = _method.getAttributes();

    if(attr == attributes[attributes.length - 1]) {
        out.println(".end method");
    }
  }

  @Override
  public void visitDeprecated(Deprecated attribute) { printEndMethod(attribute); }

  @Override
  public void visitSynthetic(Synthetic attribute) {
    if(_method != null) {
        printEndMethod(attribute);
    }
  }

  @Override
  public void visitMethod(Method method) {
    this._method = method; // Remember for use in subsequent visitXXX calls

    out.println("\n.method " + Utility.accessToString(_method.getAccessFlags()) +
        " " + _method.getName() + _method.getSignature());

    Attribute[] attributes = _method.getAttributes();
    if((attributes == null) || (attributes.length == 0)) {
        out.println(".end method");
    }
  }

  @Override
  public void visitExceptionTable(ExceptionTable e) {
    String[] names = e.getExceptionNames();
    for(int i=0; i < names.length; i++) {
        out.println(".throws " + names[i].replace('.', '/'));
    }

    printEndMethod(e);
  }

  private Hashtable<InstructionHandle, String> map;

  @Override
  public void visitCode(Code code) {
    int label_counter = 0;

    out.println(".limit stack " + code.getMaxStack());
    out.println(".limit locals " + code.getMaxLocals());

    MethodGen           mg  = new MethodGen(_method, class_name, cp);
    InstructionList     il  = mg.getInstructionList();
    InstructionHandle[] ihs = il.getInstructionHandles();

    /* Pass 1: Give all referenced instruction handles a symbolic name, i.e. a
     * label.
     */
    map = new Hashtable<InstructionHandle, String>();

    for(int i=0; i < ihs.length; i++) {
      if(ihs[i] instanceof BranchHandle) {
    BranchInstruction bi = (BranchInstruction)ihs[i].getInstruction();
    
    if(bi instanceof Select) { // Special cases LOOKUPSWITCH and TABLESWITCH
      InstructionHandle[] targets = ((Select)bi).getTargets();
      
      for(int j=0; j < targets.length; j++) {
        put(targets[j], "Label" + label_counter++ + ":");
    }
    }

    InstructionHandle ih = bi.getTarget();
    put(ih, "Label" + label_counter++ + ":");
      }
    }

    LocalVariableGen[] lvs = mg.getLocalVariables();
    for(int i=0; i < lvs.length; i++) {
      InstructionHandle ih = lvs[i].getStart();
      put(ih, "Label" + label_counter++ + ":");
      ih = lvs[i].getEnd();
      put(ih, "Label" + label_counter++ + ":");    
    }
    
    CodeExceptionGen[] ehs = mg.getExceptionHandlers();
    for(int i=0; i < ehs.length; i++) {
      CodeExceptionGen  c  = ehs[i];
      InstructionHandle ih = c.getStartPC();

      put(ih, "Label" + label_counter++ + ":");    
      ih = c.getEndPC();
      put(ih, "Label" + label_counter++ + ":");    
      ih = c.getHandlerPC();
      put(ih, "Label" + label_counter++ + ":");    
    }

    LineNumberGen[] lns = mg.getLineNumbers();
    for(int i=0; i < lns.length; i++) {
      InstructionHandle ih = lns[i].getInstruction();
      put(ih, ".line " + lns[i].getSourceLine());
    }
 
    /* Pass 2: Output code.
     */
    for(int i=0; i < lvs.length; i++) {
      LocalVariableGen l = lvs[i];
      out.println(".var " + l.getIndex() + " is " + l.getName() + " " +
          l.getType().getSignature() + 
          " from " + get(l.getStart()) + 
          " to " + get(l.getEnd()));
    }

    out.print("\n");
    
    for(int i=0; i < ihs.length; i++) {
      InstructionHandle ih   = ihs[i];
      Instruction       inst = ih.getInstruction();
      String            str  = map.get(ih);
      
      if(str != null) {
        out.println(str);
    }

      if(inst instanceof BranchInstruction) {
    if(inst instanceof Select) { // Special cases LOOKUPSWITCH and TABLESWITCH
      Select              s       = (Select)inst;
      int[]               matchs  = s.getMatchs();
      InstructionHandle[] targets = s.getTargets();
      
      if(s instanceof TABLESWITCH) {
        out.println("\ttableswitch " + matchs[0] + " " +
            matchs[matchs.length - 1]);
        
        for(int j=0; j < targets.length; j++) {
            out.println("\t\t" + get(targets[j]));
        }

      } else { // LOOKUPSWITCH
        out.println("\tlookupswitch ");

        for(int j=0; j < targets.length; j++) {
            out.println("\t\t" + matchs[j] + " : " + get(targets[j]));
        }
      }

      out.println("\t\tdefault: " + get(s.getTarget())); // Applies for both
    } else {
      BranchInstruction bi  = (BranchInstruction)inst;
      ih  = bi.getTarget();
      str = get(ih);
      out.println("\t" + Constants.OPCODE_NAMES[bi.getOpcode()] + " " + str);
    }
      } else {
        out.println("\t" + inst.toString(cp.getConstantPool()));
    }
    }
    
    out.print("\n");

    for(int i=0; i < ehs.length; i++) {
      CodeExceptionGen c = ehs[i];
      ObjectType caught = c.getCatchType();
      String class_name = (caught == null)?  // catch any exception, used when compiling finally
    "all" : caught.getClassName().replace('.', '/');

      out.println(".catch " + class_name + " from " +
          get(c.getStartPC()) + " to " + get(c.getEndPC()) +
          " using " + get(c.getHandlerPC()));
    }

    printEndMethod(code);
  }
 
  private String get(InstructionHandle ih) {
    String str = new StringTokenizer(map.get(ih), "\n").nextToken();
    return str.substring(0, str.length() - 1);
  }

  private void put(InstructionHandle ih, String line) {
    String str = map.get(ih);

    if(str == null) {
        map.put(ih, line);
    } else {
      if(line.startsWith("Label") || str.endsWith(line)) {
        return;
    }

      map.put(ih, str + "\n" + line); // append
    }
  }    

  public static void main(String[] argv) { 
    JavaClass   java_class;

    try {
      if(argv.length == 0) {
        System.err.println("disassemble: No input files specified");
    } else {
    for(int i=0; i < argv.length; i++) {
      if((java_class = Repository.lookupClass(argv[i])) == null) {
        java_class = new ClassParser(argv[i]).parse();
    }

          String class_name = java_class.getClassName();
          int    index      = class_name.lastIndexOf('.');
      String path       = class_name.substring(0, index + 1).replace('.', File.separatorChar);
      class_name = class_name.substring(index + 1);

      if(!path.equals("")) {
        File f = new File(path);
        f.mkdirs();
      }

      String name = path + class_name + ".j";
      FileOutputStream out = new FileOutputStream(name);
      new JasminVisitor(java_class, out).disassemble();
      System.out.println("File dumped to: " + name);
    }
      }
    } catch(Exception e) {
      e.printStackTrace();
    }
  }        
}
