import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.StringTokenizer;
import java.util.Vector;

public class ClassOracle {

  public static void main(String args[]) {
    if (args.length != 1) {
      System.out.println("Usage:\n    ClassOracle <filename>\n");
      System.exit(1);
    }

    ClassOracle co = new ClassOracle(args[0]);
    // System.out.println(co + "\n");
    System.out.println(co.getFQCN());
  }  

  public static final byte[] cafebabe = {(byte) 0xCA, 
                                         (byte) 0xFE, 
                                         (byte) 0xBA, 
                                         (byte) 0xBE};

  public String file;

  public int minor_v;
  public int major_v;
  public int constant_pool_size;
  public ConstantPool cp;
  public int access_flags;
  public int this_class;
  public int super_class;

  public String getFQCN() {
    return getClassname().replaceAll("/", ".");
  }

  public String getClassname() {
      Class_info info = (Class_info) cp.getEntry(this_class);
      Utf8_info name_info = (Utf8_info) cp.getEntry(info.name_index);
      return name_info.utf8;
  }

  public ClassOracle(String filename) {
    this.file = filename;
    parse(filename);
  }

    /**
     * Given a file path and a fully qualified class name this will return the
     * portion of the directory that is the classpath or null if the class
     * isn't located in the correct directory structure.
     *
     * @param classFileName path to the class (including the filename)
     * @param fqcn the fully qualified class name (package + classname)
     * @return the classpath OR null
     */
    public static String getClasspath(String classFileName, String fqcn) {
        if (classFileName.endsWith(".class")) {
            classFileName = classFileName.substring(0, classFileName.length() - 6);
        }

        StringTokenizer stk_path = new StringTokenizer(classFileName, File.separator);
        StringTokenizer stk_fqcn = new StringTokenizer(fqcn, ".");
        Vector<String> vec_path = new Vector<String>();
        Vector<String> vec_fqcn = new Vector<String>();


        while(stk_path.hasMoreTokens()) {
            vec_path.add(stk_path.nextToken());
            System.out.println(vec_path.lastElement());
        }
        while(stk_fqcn.hasMoreTokens()) {
            vec_fqcn.add(stk_fqcn.nextToken());
            System.out.println(vec_fqcn.lastElement());
        }

        // check to see if the location of the file is correct wrt the package
        for (int i = (vec_fqcn.size() - 1); i >= 0; i--) {
            if (!vec_fqcn.get(i).equals(vec_path.lastElement())) {
                return null;
            }
            vec_path.remove(vec_path.size() - 1);
        }

        // now build the path
        String cpath = File.separator;
        for(String s : vec_path) {
            cpath += s + File.separator;
        }

        return cpath;
    }
  
  private String parse(String filename) {
    try {
      FileInputStream fis = new FileInputStream(filename);
      byte[] u2 = new byte[2];
      byte[] u4 = new byte[4];

      fis.read(u4);    
      byte_equal(u4, cafebabe);

      fis.read(u2);
      minor_v = packShort(u2);

      fis.read(u2);
      major_v = packShort(u2);

      fis.read(u2);
      constant_pool_size = packShort(u2);

      cp = new ConstantPool(constant_pool_size, fis);

      fis.read(u2);
      access_flags = packShort(u2);

      fis.read(u2);
      this_class = packShort(u2);

      fis.read(u2);
      super_class = packShort(u2);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

  public String toString() {
    String s = "";

    s += file + " {\n";
    s += "    magic number: 0xCAFEBABE\n";
    s += "    minor_version: " + minor_v + "\n";
    s += "    major_version: " + major_v + "\n";
    s += "    cp size: " + constant_pool_size + "\n";
    s += "    Constant Pool {\n";
    for (int i = 1; i < cp.size; i++) {
        CPEntry e = cp.data[i];
        s += "        " + i + ": " + e + "\n";
    }
    s += "    }\n";
    s += "    access_flags: " + Integer.toHexString(access_flags) + "\n";
    s += "    this_class: " + this_class + "\n";
    s += "    super_class: " + super_class + "\n";
    s += "}";

    return s;
  }

  public static int packShort(byte[] byte_array) {
    int r;
    char[] b = new char[2];
    b[0] = (char) (0x00ff & ((int) byte_array[0]));
    b[1] = (char) (0x00ff & ((int) byte_array[1]));

    r = 0x0;
    
    r |= b[0];
    r <<= 8;
    r |= b[1];

    return r;
  }

  public static long packInt(byte[] c) {
    long i;

    char[] b = new char[4];
    b[0] = (char) c[0];
    b[1] = (char) c[1];
    b[2] = (char) c[2];
    b[3] = (char) c[3];

    i = 0x0;

    i |= b[0];
    i <<= 8;
    i |= b[1];
    i <<= 8;
    i |= b[2];
    i <<= 8;
    i |= b[3];

    return i;
  }

  private boolean byte_equal(byte b, byte b2) {
    return b == b2;
  }

  private boolean byte_equal(byte[] b, byte[] b2) {
    if (b.length != b2.length) 
      return false;

    for (int i = 0; i < b.length; i++)
      if (b[i] != b2[i])
        return false;

    return true;
  }
}

class ConstantPool {
    int size;
    CPEntry[] data;

    ConstantPool(int size, InputStream is) {
        this.size = size;
        data = new CPEntry[size];
        parse(is);
    }

    CPEntry getEntry(int i) {
        return data[i];
    }

    void parse(InputStream is) {
        try {
        short type;
        byte u1;
        byte[] u2 = new byte[2];
        byte[] u4 = new byte[4];

        for (int i = 1; i < size; i++) {
            CPEntry entry = null;
            u1 = (byte) is.read();
            type = u1;
            switch (type) {
                case 1:
                    entry = new Utf8_info(is);
                    break;
                case 3:
                    entry = new Integer_info(is);
                    break;
                case 4:
                    entry = new Float_info(is);
                    break;
                case 5:
                    entry = new Long_info(is);
                    i++;
                    break;
                case 6:
                    entry = new Double_info(is);
                    i++;
                    break;
                case 7:
                    entry = new Class_info(is);
                    break;
                case 8:
                    entry = new String_info(is);
                    break;
                case 9:
                    entry = new Fieldref_info(is);
                    break;
                case 10:
                    entry = new Methodref_info(is);
                    break;
                case 11:
                    entry = new InterfaceMethodref_info(is);
                    break;
                case 12:
                    entry = new NameAndType_info(is);
                    break;
            }
            data[i] = entry;
            entry = null;
        }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

abstract class CPEntry {
    byte type;
    abstract int getInfoSize();
    byte getType() { return type; }
}

abstract class XRef_info extends CPEntry {
    int class_index;
    int name_and_type_index;
    int getInfoSize() { return 4; }

    void build(InputStream is) {
        try {
            byte[] u2 = new byte[2];
            is.read(u2);
            class_index = ClassOracle.packShort(u2);

            is.read(u2);
            name_and_type_index = ClassOracle.packShort(u2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return "class_index: " + class_index + ", name_and_type_index: " + name_and_type_index + "]";
    }
}

class Methodref_info extends XRef_info {
    Methodref_info(InputStream is) {
        build(is);
    }
    public String toString() { return "[Methodref_info, "  + super.toString(); }
}
class Fieldref_info extends XRef_info {
    Fieldref_info(InputStream is) {
        build(is);
    }
    public String toString() { return "[Fieldref_info, "  + super.toString(); }
}
class InterfaceMethodref_info extends XRef_info {
    InterfaceMethodref_info(InputStream is) {
        build(is);
    }
    public String toString() { return "[InterfaceMethodref_info, "  + super.toString(); }
}

class Class_info extends CPEntry {
    int name_index;
    int getInfoSize() { return 2; }
    Class_info(InputStream is) {
        try {
            byte[] u2 = new byte[2];
            is.read(u2);
            name_index = ClassOracle.packShort(u2);
        } catch (Exception e) {
        }
    }
    public String toString() { return "[Class_info, name_index: " + name_index + "]"; }
}

class Utf8_info extends CPEntry {
    int len = 0;
    String utf8;
    int getInfoSize() { return 2 + len; }

    Utf8_info(InputStream is) {
        try {
            byte[] u2 = new byte[2];
            is.read(u2);
            len = ClassOracle.packShort(u2);

            byte[] utf8_b = new byte[len];
            is.read(utf8_b);

            utf8 = new String(utf8_b);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return "[Utf8_info, len: " + len+ ", string: "+ utf8 + "]";
    }
}

class NameAndType_info extends CPEntry {
    int name_index;
    int desc_index;
    int getInfoSize() { return 4; }

    NameAndType_info(InputStream is) {
        try {
            byte[] u2 = new byte[2];
            is.read(u2);
            name_index = ClassOracle.packShort(u2);

            is.read(u2);
            desc_index = ClassOracle.packShort(u2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return "[NameAndType_info, name_index: " + name_index + ", desc_index: " + desc_index + "]";
    }
}

class String_info extends CPEntry {
    int index;
    int getInfoSize() { return 2; }
    String_info(InputStream is) {
        try {
            byte[] u2 = new byte[2];
            is.read(u2);
            index = ClassOracle.packShort(u2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public String toString() { return "[String_info, string_index: " + index + "]"; }
}

class FourByteNumeric extends CPEntry {
    int getInfoSize() { return 4; }
    byte[] build(InputStream is) {
        try {
            byte[] u4 = new byte[4];
            is.read(u4);
            return u4;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

class Integer_info extends FourByteNumeric {
    long value;
    Integer_info(InputStream is) {
        byte[] b = build(is);
        value = ClassOracle.packInt(b);
    }
    public String toString() { return "[Integer, value: " + value + "]"; }
}

class Float_info extends FourByteNumeric {
    float value;
    Float_info(InputStream is) {
        byte[] b = build(is);
    }
    public String toString() { return "[Float, value: eaten ]"; }
}

class EightByteNumeric extends CPEntry {
    int getInfoSize() { return 8; }
    byte[] build(InputStream is) {
        try {
            byte[] u8 = new byte[8];
            is.read(u8);
            return u8;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

class Long_info extends EightByteNumeric {
    long value;
    Long_info(InputStream is) {
        byte[] b = build(is);
        // eat it
    }
    public String toString() { return "[Long, value: eaten ]"; }
}

class Double_info extends EightByteNumeric {
    double value;
    Double_info(InputStream is) {
        byte[] b = build(is);
    }
    public String toString() { return "[Double, value: eaten ]"; }
}

